Skip to content
Engine

Data and Security

CryptographyService

Real cryptography in pure Luau. Secure random numbers, SHA-2, SHA-3 and BLAKE2 hashing, HMAC, authenticated encryption with rotating keys, signed tokens, Argon2, scrypt, PBKDF2 and one-time codes.

ServiceServer

Getting it

local CryptographyService = Engine:GetService("CryptographyService")

Overview

CryptographyService implements modern, well-reviewed algorithms in Luau, on the server only:

  • A CSPRNG for unpredictable ids, tokens and keys.
  • Hashing: SHA-256/224/384/512, SHA3-224/256/384/512, SHAKE128/256, BLAKE2s/b and SHA-1.
  • MACs: HMAC with any of those hashes, and keyed BLAKE2.
  • Encryption: XChaCha20-Poly1305 envelopes that carry the key version, so you can rotate keys without breaking old data; plus raw ChaCha20-Poly1305 and ChaCha20.
  • Key derivation: HKDF, PBKDF2, scrypt and Argon2 (i, d and id), with async versions that run off the main thread.
  • Signed tokens with expiry, and HOTP/TOTP one-time codes compatible with authenticator apps.

Every function accepts strings or buffers. Byte results are buffers; use :Encode() or the Hex/String variants for text.

local Crypto = Engine:GetService("CryptographyService")

-- Encrypt something before storing it
local key = Crypto:CreateKey("Messages", SECRET_FROM_CONFIGURATION)
local stored = Crypto:EncryptString("meet at the north gate", "Messages")
local text, reason = Crypto:DecryptString(stored, "Messages")

-- A signed, expiring invite code
local token = Crypto:CreateToken({ PartyId = partyId }, "Messages", { ExpiresIn = 600 })
local claims, problem = Crypto:VerifyToken(token, "Messages")

Important

Keep keys on the server. Put persistent key material in Configuration/CryptographyService (Keys = { Name = "<64 hex characters>" }); because this service is server-only, its configuration is never copied to clients. Never hard-code keys in client scripts.

For player passwords, use PasswordSystem, which builds on this service.

Random

MethodDescription
:RandomBytes(count): bufferSecure random bytes.
:RandomInt(min, max), :RandomFloat(min?, max?)Unbiased numbers.
:RandomHex(byteCount), :RandomBase64(byteCount)Random bytes as text.
:RandomString(length, alphabet?)Random characters from an alphabet.
:RandomUuid()A version 4 UUID.
:RandomToken(byteCount?)A URL-safe Base64 token from byteCount random bytes (default 32).
:AddEntropy(data)Mix extra unpredictable data into the generator.
:CreateGenerator(seed?)A separate generator. With a seed it is deterministic (repeatable), which is useful for tests but not for secrets.
CryptographyService.RandomThe shared generator.

The shared generator reseeds itself after ReseedAfterBytes bytes or ReseedAfterSeconds seconds. A Generator has Bytes, Fill, Int, Float, Bool, Pick, Shuffle, Hex, Base64, Base64Url and String.

Hashing

MethodDescription
:Hash(data, algorithm?): bufferDigest (default SHA256).
:HashHex(data, algorithm?): stringDigest as hex.
:CreateHasher(algorithm?)Incremental hashing for large or streamed data: :Update(data) (chainable), then :Digest() or :DigestHex(). Also :Clone(), :Reset(), DigestSize, BlockSize.
:Shake128(data, length), :Shake256(data, length)Extendable-output functions.

Algorithms (CryptographyService.HashAlgorithms): SHA256, SHA224, SHA384, SHA512, SHA3-224, SHA3-256, SHA3-384, SHA3-512, BLAKE2s, BLAKE2b, SHA1.

Message authentication

MethodDescription
:Mac(key, message, algorithm?): bufferA MAC (default HMAC-SHA256). key is a Key, a key name, or raw bytes.
:VerifyMac(key, message, mac, algorithm?): booleanChecks a MAC in constant time.

Algorithms (CryptographyService.MacAlgorithms): HMAC- plus any hash name above, and keyed BLAKE2s / BLAKE2b.

Keys

A Key holds 32 bytes of secret material and can have several versions. Rotating a key adds a new version for new encryptions while old versions keep decrypting older data.

MethodDescription
:CreateKey(name, material?)Registers a named key. Without material a random key is generated (it is lost when the server stops, so only use that for temporary data).
:GetKey(name), :FindKey(name), :GetKeys()Look up keys.
:GenerateKey()A random, unnamed key.
:ImportKey(material)An unnamed key from material.
:RotateKey(keyOrName, material?): numberAdds a version and returns its number.
:DestroyKey(name)Removes and wipes a key.
:DeriveKey(password, salt, { Algorithm?, Name? }?)A key from a passphrase using Argon2id (default), Argon2i/d, scrypt or PBKDF2.
:DeriveKeyAsync(...)The same without blocking the server.
KeyCreated, KeyRotated(key, version), KeyDestroyed(name)Signals.

Key material is 32 raw bytes, 64 hex characters, or Base64 of 32 bytes. A passphrase is not a key; derive one.

A Key object has Name, Version, VersionCount, Id, Size, IsDestroyed, :Rotate(material?), :Export(version?), :ExportString(encoding?, version?), :Fingerprint(version?), :HasVersion(version), :Derive(purpose, version?) (a sub-key for one purpose) and :Destroy().

Encryption

Encrypt / Decrypt

CryptographyService:Encrypt(plaintext, key, associatedData?): buffer
CryptographyService:Decrypt(envelope, key, associatedData?): (buffer?, DecryptReason?)

Encrypts with XChaCha20-Poly1305 using the key's current version. The envelope stores the format, the key version, a key fingerprint and a random nonce, so decryption picks the right key version automatically. associatedData is authenticated but not encrypted: decryption fails unless the same value is given, which binds ciphertext to its context (for example the player's UserId).

Decryption returns nil plus a reason instead of raising: "NotAnEnvelope", "UnknownFormat", "UnknownKeyVersion", "WrongKey" or "Tampered".

EncryptString / DecryptString

CryptographyService:EncryptString(text, key, associatedData?, encoding?): string
CryptographyService:DecryptString(encoded, key, associatedData?, encoding?): (string?, DecryptReason?)

The same, producing text (Base64 by default) that fits in a DataStore or attribute.

InspectEnvelope

CryptographyService:InspectEnvelope(envelope, encoding?): { Format, KeyVersion, KeyFingerprint, PlaintextSize }?

Reads an envelope's header without decrypting. Useful to find data that still uses an old key version.

CryptographyService.EnvelopeOverhead is the number of bytes an envelope adds to the plaintext.

Low-level

MethodDescription
:Seal(key, nonce, plaintext, associatedData?)ChaCha20-Poly1305 with your own 12-byte nonce. Never reuse a nonce with the same key.
:Open(key, nonce, sealed, associatedData?)Returns the plaintext or nil.
:ChaCha20(key, nonce, data, counter?)The raw stream cipher, without authentication.

Key derivation

MethodDescription
:Hkdf(inputKeyMaterial, length, { Salt?, Info?, Algorithm? }?)Expand a strong secret into one or more keys.
:Pbkdf2(password, salt, iterations?, length?, algorithm?)
:Scrypt(password, salt, { N?, R?, P?, Length? }?)Memory-hard.
:Argon2(password, salt, { Memory?, Iterations?, Parallelism?, Length?, Variant?, Secret?, AssociatedData? }?)Memory-hard, the current recommendation.
:Pbkdf2Async, :ScryptAsync, :Argon2AsyncReturn Promises and run on a ComputeService worker (or in small slices per frame), so a slow derivation never freezes the server.

Passwords longer than MaxKdfInputLength and parameters needing more than MaxKdfMemoryMb are rejected.

Tokens

CreateToken / VerifyToken

CryptographyService:CreateToken(data, key, { ExpiresIn?, NotBefore?, Subject?, Id?, Now? }?): string
CryptographyService:VerifyToken(token, key): (Claims?, Reason?)

A compact, signed string carrying data (any storable value). It is signed, not encrypted: anyone can read the data, but nobody without the key can change it. Use it for invite codes, signed links between places, or anything that must prove it came from your server.

Claims: { Data, IssuedAt, ExpiresAt?, NotBefore?, Subject?, Id?, KeyVersion }. Failure reasons: "Malformed", "UnknownVersion", "UnknownKeyVersion", "BadSignature", "Expired", "NotYetValid", "BadPayload". Expiry checks allow TokenLeewaySeconds of clock skew.

One-time codes

Codes compatible with Google Authenticator, Authy and similar apps.

MethodDescription
:GenerateOtpSecret(): bufferA new secret.
:Totp(secret, options?)The current time-based code.
:VerifyTotp(secret, code, options?): (boolean, number?)Checks a code, allowing Window steps of drift; returns the step offset that matched.
:Hotp(secret, counter, options?)A counter-based code.
:OtpUri(secret, account, options?)An otpauth:// URI to show as a QR code.

Options: { Digits?, Step?, Algorithm?, Window?, Time?, Issuer? }.

Utilities

MethodDescription
:Encode(data, encoding?), :Decode(text, encoding?)"Hex", "Base64", "Base64Url", "Base32" or "Binary".
:ConstantTimeEqual(a, b)Compare secrets without leaking timing.
:Wipe(buffer)Overwrite a buffer with zeros once you no longer need its secret.

Configuration

Defaults from DefaultConfiguration/Services/CryptographyService29 settings
SettingDefaultWhat it does
Keys{}Named encryption keys created at load; server-only.
DefaultHashAlgorithm"SHA256"Algorithm :Hash/:HashHex/:CreateHasher use when none is passed.
DefaultMacAlgorithm"HMAC-SHA256"Algorithm :Mac/:VerifyMac use when none is passed.
DefaultEncoding"Hex"Encoding :Encode/:Decode use when none is passed.
DefaultStringEncoding"Base64"Text encoding :EncryptString produces and :DecryptString expects when none is passed.
DefaultKeyDerivation"Argon2id"KDF :DeriveKey/:DeriveKeyAsync use when none is passed.
KdfLength32Output bytes :Pbkdf2/:Scrypt/:Argon2 derive when no length is passed.
MaxKdfInputLength1024Longer passwords/passphrases are rejected before a KDF runs.
Argon2Memory19456KiB of memory per Argon2 derivation.
Argon2Iterations2Passes over that memory per Argon2 derivation.
Argon2Parallelism1Parallel lanes per Argon2 derivation.
ScryptN16384scrypt CPU/memory cost parameter.
ScryptR8scrypt block size factor.
ScryptP1scrypt parallelism factor.
Pbkdf2Iterations100000PBKDF2 iterations.
MaxKdfMemoryMb256Memory ceiling a :Scrypt/:Argon2 call may request.
UseComputeWorkerstrueRun the Async KDF methods on a ComputeService worker Actor instead of the main thread.
ComputeWorkers2Worker Actors the service's pool may spin up.
AsyncSliceMs6Milliseconds of main-thread work per frame when sliced instead of run on a worker.
ReseedAfterBytes1048576Output bytes after which the shared CSPRNG reseeds.
ReseedAfterSeconds300Seconds after which the shared CSPRNG reseeds.
TokenExpiresIn3600Seconds a :CreateToken token stays valid when ExpiresIn is not passed.
TokenLeewaySeconds5Clock skew tolerated when checking a token's expiry and not-before.
OtpSecretSize20Bytes in a :GenerateOtpSecret secret.
OtpDigits6Digits per one-time code.
OtpStepSeconds30Seconds per TOTP time step.
OtpAlgorithm"SHA1"HMAC hash used for HOTP/TOTP.
OtpWindow1Time steps either side of now :VerifyTotp still accepts.
OtpIssuer"Engine"Issuer name in :OtpUri when none is passed.

Types

export type Bytes = buffer | string
export type Key
export type KeyRef = Key | string
export type Generator
export type Hasher
export type HashAlgorithm
export type MacAlgorithm
export type Encoding = "Hex" | "Base64" | "Base64Url" | "Base32" | "Binary"
export type KeyDerivation = "Argon2id" | "Argon2i" | "Argon2d" | "Scrypt" | "PBKDF2"
export type DecryptReason
export type TokenClaims
export type TokenOptions
export type TokenReason
export type OtpOptions
export type EnvelopeInfo