Skip to main content

Security Best Practices

A condensed guide to using this library safely.

Key Management

Generate keys properly

-- Correct
local Key = CSPRNG.RandomBytes(32)
local EdKey = CSPRNG.Ed25519Random()

-- Wrong
local Key = buffer.fromstring("mysecretpassword") -- predictable
local BadKey = buffer.create(32) -- all zeros

Never hardcode keys. Use Roblox Secrets for server side keys:

local SecretKey = game:GetService("HttpService"):GetSecret("ENCRYPTION_KEY")
-- Can't use secret data type with buffers though

Rotate keys periodically. Fresh keys limit damage from unknown compromises.

Use different keys for different purposes

local Context = buffer.fromstring("MyApp encryption key")
local KeyDeriver = Blake3.DeriveKey(Context)

local MasterSecret = CSPRNG.RandomBytes(32)
local EncryptionKey = KeyDeriver(MasterSecret, 32)

local MacContext = buffer.fromstring("MyApp mac key")
local MacDeriver = Blake3.DeriveKey(MacContext)
local MacKey = MacDeriver(MasterSecret, 32)

Never expose keys to clients. If the client needs to encrypt something, they should not have the key the server uses.

Nonce and IV Handling

The cardinal rule: never reuse a nonce with the same key.

With ChaCha20 Poly1305, reusing a nonce leaks the XOR of plaintexts, allows forgery of authentication tags, and completely breaks security.

-- Correct: random nonces
local Nonce = CSPRNG.RandomBytes(12)

-- Correct: counter based nonces
local Counter = 0
local function GetNonce()
Counter += 1
local Nonce = buffer.create(12)
buffer.writeu32(Nonce, 0, Counter)
return Nonce
end

-- Wrong: reusing nonces
local StaticNonce = buffer.fromstring("fixednonce12")

For high volume encryption, use XChaCha20

-- 24 byte nonces have negligible collision probability
local Nonce = CSPRNG.RandomBytes(24)
local Ciphertext, Tag = AEAD.Encrypt(Data, Key, Nonce, nil, nil, true)

AES GCM has a 2^32 message limit per key with random 96 bit IVs. Rotate keys before approaching this.

Algorithm Selection

Hashing

Use CaseAlgorithm
General purposeSHA256 or BLAKE3
Speed criticalBLAKE3
Regulatory complianceSHA256 or SHA3 256
Key derivationBLAKE3.DeriveKey

Symmetric Encryption

Use CaseAlgorithm
General purposeChaCha20 Poly1305 (AEAD)
AES compatibility requiredAES GCM
Many messages per keyXChaCha20 Poly1305

Signatures

Use CaseAlgorithm
General purposeEd25519
Post quantum securityML DSA
Both (paranoid)Ed25519 + ML DSA

Key Exchange

Use CaseAlgorithm
General purposeX25519
Post quantum securityML KEM
Both (paranoid)X25519 + ML KEM hybrid

Common Pitfalls

Comparing secrets incorrectly

-- Wrong: timing attack possible
if HexMac == ExpectedMac then

-- Correct: constant time comparison
local function ConstantTimeCompare(a, b)
if buffer.len(a) ~= buffer.len(b) then return false end
local diff = 0
for i = 0, buffer.len(a) - 1 do
diff = bit32.bor(diff, bit32.bxor(buffer.readu8(a, i), buffer.readu8(b, i)))
end
return diff == 0
end

The library's internal comparisons are constant time, but be careful when writing your own verification logic.

Using encryption without authentication

-- Wrong: raw ChaCha20 without Poly1305
local Encrypted = AEAD.ChaCha20(Data, Key, Nonce)
-- attacker can flip bits undetected

-- Correct: authenticated encryption
local Ciphertext, Tag = AEAD.Encrypt(Data, Key, Nonce)

Trusting client provided crypto

-- Wrong: client sends encrypted data with their own key
RemoteEvent.OnServerEvent:Connect(function(player, encryptedData, clientKey)
local decrypted = AEAD.Decrypt(encryptedData, clientKey, ...) -- pointless
end)

-- Correct: server controls the keys
RemoteEvent.OnServerEvent:Connect(function(player, encryptedData, nonce, tag)
local decrypted = AEAD.Decrypt(encryptedData, ServerKey, nonce, tag)
end)

Forgetting that encryption is not authentication

Encryption hides content. It does not prove who sent it. If you need to know the sender, use signatures or MACs.

-- Encrypt for confidentiality
local Ciphertext, Tag = AEAD.Encrypt(Message, SharedKey, Nonce)

-- Sign for authenticity
local Signature = EdDSA.Sign(Message, SenderPrivate, SenderPublic)

What the Library Cannot Protect Against

Side channels Luau execution timing is not constant. The library uses constant time operations where feasible, but the VM's behavior is not guaranteed.

Memory exposure Buffers persist until garbage collection. Zeroing them helps but does not guarantee the memory is overwritten before being reallocated.

Key compromise If someone gets your key, they get your data.

Protocol bugs The primitives are solid, but combining them incorrectly breaks security. AEAD without unique nonces, signatures without verifying the public key source, and so on.

Quick Checklist

Before releasing:

  • All keys generated with CSPRNG
  • No hardcoded keys in source
  • Nonces are unique per message
  • Using authenticated encryption (AEAD or AES GCM)
  • Signatures verified before trusting data
  • Keys not exposed to clients
  • Error handling does not leak information
  • Tested with invalid inputs