Thanks for using the cryptography module! Though end to end encryption means that the server doesn’t even have access to the data. The encryption keys, messages etc are generated by each client and they send their public key to each other (using the server) and then the encrypted messages are passed to each other so all the server receives is encrypted jargon.
So it’s not actually possible to do e2e if you want the server to be able to send or read the data, this is more transport encryption (data encrypted in transit between server and clients)
Proper end to end encryption gives me a headache > https://kerkour.com/signal-protocol-pqxdh-rust
--!strict
--!optimize 2
--!native
local Cryptography = require(game.ReplicatedStorage.Cryptography)
local Hashing = Cryptography.Hashing
local Encryption = Cryptography.Encryption
local Utilities = Cryptography.Utilities
local Verification = Cryptography.Verification
local MlKem = Verification.MlKEM
local function ConcatBuffers(...): buffer
local Buffers = {...}
local TotalLength = 0
for _, Buffer in Buffers do
TotalLength += buffer.len(Buffer)
end
local Result = buffer.create(TotalLength)
local Offset = 0
for _, Buffer in Buffers do
local Length = buffer.len(Buffer)
buffer.copy(Result, Offset, Buffer, 0, Length)
Offset += Length
end
return Result
end
local function Ed25519SkToX25519(Ed25519SecretKey: buffer): buffer
return Verification.EdDSA.Convert.ConvertPrivateKey(Ed25519SecretKey)
end
local function Ed25519PkToX25519(Ed25519PublicKey: buffer): buffer
local X25519Public = Verification.EdDSA.Convert.ConvertPublicKey(Ed25519PublicKey)
if not X25519Public then
error("Failed to convert Ed25519 public key to X25519")
end
return X25519Public
end
local function Kdf(Dh1: buffer, Dh2: buffer, Dh3: buffer, MlkemSharedSecret: buffer): buffer
local KdfPrefix = buffer.create(32)
buffer.fill(KdfPrefix, 0, 0xFF, 32)
local Input = ConcatBuffers(KdfPrefix, Dh1, Dh2, Dh3, MlkemSharedSecret)
local Output = Hashing.Blake3.Digest(Input, 32)
return Utilities.Conversions.FromHex(Output)
end
local function KdfWithOneTime(Dh1: buffer, Dh2: buffer, Dh3: buffer, Dh4: buffer, MlkemSharedSecret: buffer): buffer
local KdfPrefix = buffer.create(32)
buffer.fill(KdfPrefix, 0, 0xFF, 32)
local Input = ConcatBuffers(KdfPrefix, Dh1, Dh2, Dh3, Dh4, MlkemSharedSecret)
local Output = Hashing.Blake3.Digest(Input, 32)
return Utilities.Conversions.FromHex(Output)
end
local function KdfRootKey(Key: buffer, SharedSecret: buffer): (buffer, buffer)
local Input = ConcatBuffers(Key, SharedSecret)
local Output = Hashing.Blake3.Digest(Input, 64)
local OutputBuffer = Utilities.Conversions.FromHex(Output)
local RootKey = buffer.create(32)
local ChainKey = buffer.create(32)
buffer.copy(RootKey, 0, OutputBuffer, 0, 32)
buffer.copy(ChainKey, 0, OutputBuffer, 32, 32)
return RootKey, ChainKey
end
local function KdfChainKey(Key: buffer): (buffer, buffer)
local Output = Hashing.Blake3.Digest(Key, 64)
local OutputBuffer = Utilities.Conversions.FromHex(Output)
local ChainKey = buffer.create(32)
local MessageKey = buffer.create(32)
buffer.copy(ChainKey, 0, OutputBuffer, 0, 32)
buffer.copy(MessageKey, 0, OutputBuffer, 32, 32)
return ChainKey, MessageKey
end
export type User = {
Name: string,
IdentityPrivateKey: buffer,
IdentityPublicKey: buffer,
X25519PrekeyPrivate: buffer,
X25519PrekeyPublic: buffer,
X25519PrekeySignature: buffer,
OneTimePrekeys: {[string]: buffer},
OneTimePrekeyPublics: {[string]: buffer},
MlkemPrekeyDecapKey: buffer,
MlkemPrekeyEncapKey: buffer,
MlkemPrekeySignature: buffer,
PqOneTimePrekeys: {[string]: buffer},
PqOneTimePrekeyPublics: {[string]: buffer},
PqOneTimePrekeySignatures: {[string]: buffer},
SendingX25519Private: buffer,
SendingX25519Public: buffer,
ReceivingX25519Public: buffer?,
RootKey: buffer,
ChainKeySending: buffer,
ChainKeyReceiving: buffer,
SendingCounter: number,
ReceivingCounter: number,
}
export type PqxdhInitOutput = {
SecretKey: buffer,
Message: PqxdhInitMessage,
}
export type PqxdhInitMessage = {
PeerIdentityPublicKey: buffer,
EphemeralX25519PublicKey: buffer,
MlkemCiphertext: buffer,
OneTimePrekeyId: string?,
PqOneTimePrekeyId: string?,
}
export type Message = {
Header: MessageHeader,
Ciphertext: buffer,
}
export type MessageHeader = {
X25519PublicKey: buffer,
Counter: number,
Nonce: buffer,
}
local User = {}
User.__index = User
function User.New(Name: string): User
local IdentityPrivateKey = Utilities.CSPRNG.Ed25519Random()
local IdentityPublicKey = Verification.EdDSA.PublicKey(IdentityPrivateKey)
local X25519PrekeyRaw = Utilities.CSPRNG.Ed25519Random()
local X25519PrekeyPrivate = Verification.EdDSA.X25519.Mask(X25519PrekeyRaw)
local X25519PrekeyPublic = Verification.EdDSA.X25519.PublicKey(X25519PrekeyPrivate)
local X25519PrekeySignature = Verification.EdDSA.Sign(IdentityPrivateKey, IdentityPublicKey, X25519PrekeyPublic)
local OneTimePrekeys = {}
local OneTimePrekeyPublics = {}
for I = 1, 100 do
local OneTimePrekeyRaw = Utilities.CSPRNG.Ed25519Random()
local OneTimePrekeyPrivate = Verification.EdDSA.X25519.Mask(OneTimePrekeyRaw)
local OneTimePrekeyPublic = Verification.EdDSA.X25519.PublicKey(OneTimePrekeyPrivate)
local KeyId = `OneTime_{I}`
OneTimePrekeys[KeyId] = OneTimePrekeyPrivate
OneTimePrekeyPublics[KeyId] = OneTimePrekeyPublic
end
local MlkemPrekeyEncapKey, MlkemPrekeyDecapKey = MlKem.MLKEM_1024.GenerateKeys()
local MlkemPrekeySignature = Verification.EdDSA.Sign(IdentityPrivateKey, IdentityPublicKey, MlkemPrekeyEncapKey)
local PqOneTimePrekeys = {}
local PqOneTimePrekeyPublics = {}
local PqOneTimePrekeySignatures = {}
for I = 1, 100 do
local PqPrekeyEncapKey, PqPrekeyDecapKey = MlKem.MLKEM_1024.GenerateKeys()
local PqKeyId = `PQ_OneTime_{I}`
local PqPrekeySignature = Verification.EdDSA.Sign(IdentityPrivateKey, IdentityPublicKey, PqPrekeyEncapKey)
PqOneTimePrekeys[PqKeyId] = PqPrekeyDecapKey
PqOneTimePrekeyPublics[PqKeyId] = PqPrekeyEncapKey
PqOneTimePrekeySignatures[PqKeyId] = PqPrekeySignature
end
local SendingX25519Raw = Utilities.CSPRNG.Ed25519Random()
local SendingX25519Private = Verification.EdDSA.X25519.Mask(SendingX25519Raw)
local SendingX25519Public = Verification.EdDSA.X25519.PublicKey(SendingX25519Private)
return {
Name = Name,
IdentityPrivateKey = IdentityPrivateKey,
IdentityPublicKey = IdentityPublicKey,
X25519PrekeyPrivate = X25519PrekeyPrivate,
X25519PrekeyPublic = X25519PrekeyPublic,
X25519PrekeySignature = X25519PrekeySignature,
OneTimePrekeys = OneTimePrekeys,
OneTimePrekeyPublics = OneTimePrekeyPublics,
MlkemPrekeyDecapKey = MlkemPrekeyDecapKey,
MlkemPrekeyEncapKey = MlkemPrekeyEncapKey,
MlkemPrekeySignature = MlkemPrekeySignature,
PqOneTimePrekeys = PqOneTimePrekeys,
PqOneTimePrekeyPublics = PqOneTimePrekeyPublics,
PqOneTimePrekeySignatures = PqOneTimePrekeySignatures,
SendingX25519Private = SendingX25519Private,
SendingX25519Public = SendingX25519Public,
ReceivingX25519Public = nil,
RootKey = buffer.create(32),
ChainKeySending = buffer.create(32),
ChainKeyReceiving = buffer.create(32),
SendingCounter = 0,
ReceivingCounter = 0,
} :: User
end
function User.RefillOneTimePrekeys(User: User)
local CurrentCount = 0
for _ in User.OneTimePrekeys do
CurrentCount += 1
end
local CurrentPqCount = 0
for _ in User.PqOneTimePrekeys do
CurrentPqCount += 1
end
if CurrentCount < 10 then
local BaseIndex = 0
for Key in User.OneTimePrekeys do
local Index = tonumber(string.match(Key, "OneTime_(%d+)"))
if Index and Index > BaseIndex then
BaseIndex = Index
end
end
for I = BaseIndex + 1, BaseIndex + 50 do
local OneTimePrekeyRaw = Utilities.CSPRNG.Ed25519Random()
local OneTimePrekeyPrivate = Verification.EdDSA.X25519.Mask(OneTimePrekeyRaw)
local OneTimePrekeyPublic = Verification.EdDSA.X25519.PublicKey(OneTimePrekeyPrivate)
local KeyId = `OneTime_{I}`
User.OneTimePrekeys[KeyId] = OneTimePrekeyPrivate
User.OneTimePrekeyPublics[KeyId] = OneTimePrekeyPublic
end
print(`Refilled X25519 one-time prekeys for {User.Name}, now has {CurrentCount + 50} keys`)
end
if CurrentPqCount < 10 then
local BasePqIndex = 0
for Key in User.PqOneTimePrekeys do
local Index = tonumber(string.match(Key, "PQ_OneTime_(%d+)"))
if Index and Index > BasePqIndex then
BasePqIndex = Index
end
end
for I = BasePqIndex + 1, BasePqIndex + 50 do
local PqPrekeyEncapKey, PqPrekeyDecapKey = MlKem.MLKEM_1024.GenerateKeys()
local PqKeyId = `PQ_OneTime_{I}`
local PqPrekeySignature = Verification.EdDSA.Sign(User.IdentityPrivateKey, User.IdentityPublicKey, PqPrekeyEncapKey)
User.PqOneTimePrekeys[PqKeyId] = PqPrekeyDecapKey
User.PqOneTimePrekeyPublics[PqKeyId] = PqPrekeyEncapKey
User.PqOneTimePrekeySignatures[PqKeyId] = PqPrekeySignature
end
print(`Refilled ML-KEM one-time prekeys for {User.Name}, now has {CurrentPqCount + 50} keys`)
end
end
local PQXDH = {}
function PQXDH.Init(Alice: User, Bob: User): PqxdhInitOutput
local X25519Valid = Verification.EdDSA.Verify(Bob.IdentityPublicKey, Bob.X25519PrekeyPublic, Bob.X25519PrekeySignature)
if not X25519Valid then
error("failed to verify X25519 prekey")
end
local MlkemValid = Verification.EdDSA.Verify(Bob.IdentityPublicKey, Bob.MlkemPrekeyEncapKey, Bob.MlkemPrekeySignature)
if not MlkemValid then
error("failed to verify ML-KEM-1024 prekey")
end
local EphemeralX25519Raw = Utilities.CSPRNG.Ed25519Random()
local EphemeralX25519Private = Verification.EdDSA.X25519.Mask(EphemeralX25519Raw)
local EphemeralX25519Public = Verification.EdDSA.X25519.PublicKey(EphemeralX25519Private)
local AvailablePqOneTimeKeys = {}
for KeyId, _ in Bob.PqOneTimePrekeyPublics do
table.insert(AvailablePqOneTimeKeys, KeyId)
end
local PqPrekeyToUse: buffer
local PqOneTimePrekeyId: string?
if #AvailablePqOneTimeKeys > 0 then
PqOneTimePrekeyId = AvailablePqOneTimeKeys[1]
local KeyId = PqOneTimePrekeyId :: string
PqPrekeyToUse = Bob.PqOneTimePrekeyPublics[KeyId]
local PqPrekeySignature = Bob.PqOneTimePrekeySignatures[KeyId]
local PqPrekeyValid = Verification.EdDSA.Verify(Bob.IdentityPublicKey, PqPrekeyToUse, PqPrekeySignature)
if not PqPrekeyValid then
error("failed to verify PQ one-time prekey")
end
else
PqPrekeyToUse = Bob.MlkemPrekeyEncapKey
PqOneTimePrekeyId = nil
end
local RandomMessage = Utilities.CSPRNG.RandomBytes(32)
local MlkemCiphertext, MlkemSharedSecret = MlKem.MLKEM_1024.Encapsulate(PqPrekeyToUse, RandomMessage)
if not MlkemCiphertext or not MlkemSharedSecret then
return error("failed to encapsulate with ML-KEM-1024")
end
local AliceIdentitySecretKeyX25519 = Ed25519SkToX25519(Alice.IdentityPrivateKey)
local BobIdentityPublicKeyX25519 = Ed25519PkToX25519(Bob.IdentityPublicKey)
local Dh1, _ = Verification.EdDSA.X25519.Exchange(AliceIdentitySecretKeyX25519, Bob.X25519PrekeyPublic)
local Dh2, _ = Verification.EdDSA.X25519.Exchange(EphemeralX25519Private, BobIdentityPublicKeyX25519)
local Dh3, _ = Verification.EdDSA.X25519.Exchange(EphemeralX25519Private, Bob.X25519PrekeyPublic)
local OneTimePrekeyId = nil
local SecretKey
local AvailableOneTimeKeys = {}
for KeyId, _ in Bob.OneTimePrekeyPublics do
table.insert(AvailableOneTimeKeys, KeyId)
end
if #AvailableOneTimeKeys > 0 then
OneTimePrekeyId = AvailableOneTimeKeys[1]
local OneTimePrekeyPublic = Bob.OneTimePrekeyPublics[OneTimePrekeyId]
local Dh4, _ = Verification.EdDSA.X25519.Exchange(EphemeralX25519Private, OneTimePrekeyPublic)
SecretKey = KdfWithOneTime(Dh1, Dh2, Dh3, Dh4, MlkemSharedSecret)
else
SecretKey = Kdf(Dh1, Dh2, Dh3, MlkemSharedSecret)
end
local InitMessage: PqxdhInitMessage = {
PeerIdentityPublicKey = Alice.IdentityPublicKey,
EphemeralX25519PublicKey = EphemeralX25519Public,
MlkemCiphertext = MlkemCiphertext,
OneTimePrekeyId = OneTimePrekeyId,
PqOneTimePrekeyId = PqOneTimePrekeyId,
}
return {
SecretKey = SecretKey,
Message = InitMessage,
}
end
function PQXDH.Complete(Bob: User, Message: PqxdhInitMessage): buffer
local PqPrekeyToDecapsulate
if Message.PqOneTimePrekeyId then
PqPrekeyToDecapsulate = Bob.PqOneTimePrekeys[Message.PqOneTimePrekeyId]
if not PqPrekeyToDecapsulate then
print(`PQ one-time prekey {Message.PqOneTimePrekeyId} not found, using last-resort prekey`)
PqPrekeyToDecapsulate = Bob.MlkemPrekeyDecapKey
end
else
PqPrekeyToDecapsulate = Bob.MlkemPrekeyDecapKey
end
local MlkemSharedSecret = MlKem.MLKEM_1024.Decapsulate(PqPrekeyToDecapsulate, Message.MlkemCiphertext)
local AliceIdentityPublicKeyX25519 = Ed25519PkToX25519(Message.PeerIdentityPublicKey)
local BobIdentitySecretKeyX25519 = Ed25519SkToX25519(Bob.IdentityPrivateKey)
local Dh1, _ = Verification.EdDSA.X25519.Exchange(Bob.X25519PrekeyPrivate, AliceIdentityPublicKeyX25519)
local Dh2, _ = Verification.EdDSA.X25519.Exchange(BobIdentitySecretKeyX25519, Message.EphemeralX25519PublicKey)
local Dh3, _ = Verification.EdDSA.X25519.Exchange(Bob.X25519PrekeyPrivate, Message.EphemeralX25519PublicKey)
local SecretKey
if Message.OneTimePrekeyId then
local OneTimePrekeyPrivate = Bob.OneTimePrekeys[Message.OneTimePrekeyId]
if OneTimePrekeyPrivate then
local Dh4, _ = Verification.EdDSA.X25519.Exchange(OneTimePrekeyPrivate, Message.EphemeralX25519PublicKey)
SecretKey = KdfWithOneTime(Dh1, Dh2, Dh3, Dh4, MlkemSharedSecret)
Bob.OneTimePrekeys[Message.OneTimePrekeyId] = nil
Bob.OneTimePrekeyPublics[Message.OneTimePrekeyId] = nil
print(`Used and deleted X25519 one-time prekey: {Message.OneTimePrekeyId}`)
else
print(`X25519 one-time prekey {Message.OneTimePrekeyId} not found, using standard 3-DH`)
SecretKey = Kdf(Dh1, Dh2, Dh3, MlkemSharedSecret)
end
else
SecretKey = Kdf(Dh1, Dh2, Dh3, MlkemSharedSecret)
end
if Message.PqOneTimePrekeyId then
if Bob.PqOneTimePrekeys[Message.PqOneTimePrekeyId] then
Bob.PqOneTimePrekeys[Message.PqOneTimePrekeyId] = nil
Bob.PqOneTimePrekeyPublics[Message.PqOneTimePrekeyId] = nil
Bob.PqOneTimePrekeySignatures[Message.PqOneTimePrekeyId] = nil
print(`Used and deleted ML-KEM one-time prekey: {Message.PqOneTimePrekeyId}`)
end
end
User.RefillOneTimePrekeys(Bob)
return SecretKey
end
local DoubleRatchet = {}
function DoubleRatchet.InitAlice(Alice: User, SharedKey: buffer, BobX25519PublicKey: buffer)
Alice.ReceivingX25519Public = BobX25519PublicKey
local SharedSecret, _ = Verification.EdDSA.X25519.Exchange(Alice.SendingX25519Private, BobX25519PublicKey)
Alice.RootKey, Alice.ChainKeySending = KdfRootKey(SharedKey, SharedSecret)
end
function DoubleRatchet.InitBob(Bob: User, SharedKey: buffer)
buffer.copy(Bob.RootKey, 0, SharedKey, 0, 32)
end
function DoubleRatchet.SendMessage(Sender: User, MessageText: string, AdditionalData: buffer): Message
local NewChainKeySending, MessageKey = KdfChainKey(Sender.ChainKeySending)
Sender.ChainKeySending = NewChainKeySending
local Nonce = Utilities.CSPRNG.RandomBytes(12)
local Header = {
X25519PublicKey = Sender.SendingX25519Public,
Counter = Sender.SendingCounter,
Nonce = Nonce,
}
local PlaintextBuffer = buffer.fromstring(MessageText)
local Ciphertext, Tag = Encryption.AEAD.Encrypt(PlaintextBuffer, MessageKey, Nonce, AdditionalData)
local CombinedCiphertext = ConcatBuffers(Ciphertext, Tag)
print(`> {Sender.Name} is sending [{Sender.SendingCounter}]: {MessageText}`)
local Message = {
Header = Header,
Ciphertext = CombinedCiphertext,
}
Sender.SendingCounter += 1
return Message
end
function DoubleRatchet.ReceiveMessage(Receiver: User, Message: Message, AdditionalData: buffer)
if Receiver.ReceivingX25519Public ~= Message.Header.X25519PublicKey then
Receiver.ReceivingX25519Public = Message.Header.X25519PublicKey
Receiver.RootKey, Receiver.ChainKeyReceiving = KdfRootKey(
Receiver.RootKey,
(Verification.EdDSA.X25519.Exchange(Receiver.SendingX25519Private, Message.Header.X25519PublicKey))
)
local SendingX25519Raw = Utilities.CSPRNG.Ed25519Random()
Receiver.SendingX25519Private = Verification.EdDSA.X25519.Mask(SendingX25519Raw)
Receiver.SendingX25519Public = Verification.EdDSA.X25519.PublicKey(Receiver.SendingX25519Private)
Receiver.RootKey, Receiver.ChainKeySending = KdfRootKey(
Receiver.RootKey,
(Verification.EdDSA.X25519.Exchange(Receiver.SendingX25519Private, Message.Header.X25519PublicKey))
)
end
local ChainKeyReceiving, MessageKey = KdfChainKey(Receiver.ChainKeyReceiving)
Receiver.ChainKeyReceiving = ChainKeyReceiving
local CiphertextLen = buffer.len(Message.Ciphertext)
local Ciphertext = buffer.create(CiphertextLen - 16)
local Tag = buffer.create(16)
buffer.copy(Ciphertext, 0, Message.Ciphertext, 0, CiphertextLen - 16)
buffer.copy(Tag, 0, Message.Ciphertext, CiphertextLen - 16, 16)
local PlaintextBuffer = Encryption.AEAD.Decrypt(Ciphertext, MessageKey, Message.Header.Nonce, Tag, AdditionalData)
if PlaintextBuffer then
local MessagePlaintext = buffer.tostring(PlaintextBuffer)
print(`< {Receiver.Name} received [{Receiver.ReceivingCounter}]: {MessagePlaintext}`)
Receiver.ReceivingCounter += 1
else
error("Failed to decrypt message")
end
end
local function Demo()
local Alice = User.New("Alice")
local Bob = User.New("Bob")
local OneTimeCount = 0
for _ in Bob.OneTimePrekeyPublics do
OneTimeCount += 1
end
local PqOneTimeCount = 0
for _ in Bob.PqOneTimePrekeyPublics do
PqOneTimeCount += 1
end
print(`Bob starts with {OneTimeCount} X25519 one-time prekeys and {PqOneTimeCount} ML-KEM one-time prekeys`)
local InitResult = PQXDH.Init(Alice, Bob)
local BobSecretKey = PQXDH.Complete(Bob, InitResult.Message)
local AliceSecretHex = Utilities.Conversions.ToHex(InitResult.SecretKey)
local BobSecretHex = Utilities.Conversions.ToHex(BobSecretKey)
print(`Alice Secret Key: {AliceSecretHex}`)
print(`Bob Secret Key: {BobSecretHex}`)
if AliceSecretHex == BobSecretHex then
print("PQXDH done")
print(`Shared Secret: {AliceSecretHex:sub(1, 16)}...`)
if InitResult.Message.OneTimePrekeyId then
print(`Used X25519 one-time prekey: {InitResult.Message.OneTimePrekeyId}`)
else
print("No X25519 one-time prekey used")
end
if InitResult.Message.PqOneTimePrekeyId then
print(`Used ML-KEM one-time prekey: {InitResult.Message.PqOneTimePrekeyId}`)
else
print("Used ML-KEM last-resort prekey")
end
else
error("PQXDH failed")
end
local RemainingOneTimeKeys = 0
for _ in Bob.OneTimePrekeyPublics do
RemainingOneTimeKeys += 1
end
local RemainingPqOneTimeKeys = 0
for _ in Bob.PqOneTimePrekeyPublics do
RemainingPqOneTimeKeys += 1
end
print(`Bob now has {RemainingOneTimeKeys} X25519 and {RemainingPqOneTimeKeys} ML-KEM one-time prekeys remaining`)
DoubleRatchet.InitAlice(Alice, InitResult.SecretKey, Bob.SendingX25519Public)
DoubleRatchet.InitBob(Bob, InitResult.SecretKey)
local AdditionalData = ConcatBuffers(Alice.IdentityPublicKey, Bob.IdentityPublicKey)
local Message1 = DoubleRatchet.SendMessage(Alice, "Hello", AdditionalData)
DoubleRatchet.ReceiveMessage(Bob, Message1, AdditionalData)
local Message2 = DoubleRatchet.SendMessage(Bob, "World!", AdditionalData)
DoubleRatchet.ReceiveMessage(Alice, Message2, AdditionalData)
local Message3 = DoubleRatchet.SendMessage(Alice, "im alice", AdditionalData)
DoubleRatchet.ReceiveMessage(Bob, Message3, AdditionalData)
local Message4 = DoubleRatchet.SendMessage(Bob, "im bob", AdditionalData)
DoubleRatchet.ReceiveMessage(Alice, Message4, AdditionalData)
for I = 1, 5 do
local TestAlice = User.New(`Alice{I}`)
local TestResult = PQXDH.Init(TestAlice, Bob)
PQXDH.Complete(Bob, TestResult.Message)
task.wait()
end
local FinalRemainingX25519Keys = 0
for _ in Bob.OneTimePrekeyPublics do
FinalRemainingX25519Keys += 1
end
local FinalRemainingPqKeys = 0
for _ in Bob.PqOneTimePrekeyPublics do
FinalRemainingPqKeys += 1
end
print(`After 5 more key exchanges, Bob has {FinalRemainingX25519Keys} X25519 and {FinalRemainingPqKeys} ML-KEM one-time prekeys`)
print("done")
end
Demo()
Also you don’t really need the whole obfuscated key system, I’d just use them directly