Skip to content
Engine

Systems

PasswordSystem

Hash and verify passwords with Argon2id, scrypt or PBKDF2, check password rules, generate passwords and PINs, and protect doors, rooms or admin panels with locks that limit guessing.

SystemServer

Getting it

local PasswordSystem = Engine:GetSystem("PasswordSystem")

Overview

Some games let players set passwords: private rooms, club houses, safes, admin panels. PasswordSystem makes sure those passwords are never stored in plain text and cannot be guessed quickly.

  • Hashing with Argon2id by default (scrypt and PBKDF2 also available). A hash is a single string in the standard PHC format ($argon2id$v=19$m=...) that contains the algorithm, the settings and a random salt, so you can store it anywhere, such as a DataService profile.
  • Policies that check length, character classes and a blocklist.
  • Locks: a password-protected thing with built-in attempt limits and lockouts per player.

It runs on the server only and uses CryptographyService underneath. Hashing is deliberately slow, so prefer the Async methods, which do the work without freezing the server.

A password-protected room
local PasswordSystem = Engine:GetSystem("PasswordSystem")

-- When the owner sets a password
PasswordSystem:HashAsync(newPassword):andThen(function(hash)
	roomProfile:Set("PasswordHash", hash)
end)

-- When someone tries to enter
local lock = PasswordSystem:CreateLock("Room_" .. roomId, roomProfile:Get("PasswordHash"))

enterRemote.Received:Connect(function(player, args)
	lock:VerifyAsync(args[1], player):andThen(function(ok, reason)
		if ok then
			teleportIntoRoom(player)
		elseif reason == "LockedOut" then
			notify(player, "Too many tries, wait a minute.")
		else
			notify(player, "Wrong password.")
		end
	end)
end)

Hashing

MethodDescription
:HashAsync(password, options?): Promise<string>Hashes a password with a new random salt.
:Hash(password, options?): stringThe same, blocking.
:VerifyAsync(password, hash): Promise<boolean>Checks a password against a stored hash in constant time.
:Verify(password, hash): booleanThe same, blocking.
:NeedsRehash(hash, options?): booleanWhether a hash uses older settings than the current ones. After a successful login, hash the password again and store the new hash.
:Parse(hash): Parsed?{ Algorithm, Parameters, Salt, Hash }, or nil if it is not a valid hash.
:IsHash(value): boolean

Hash options override the configuration for one call: Algorithm ("Argon2id", "Argon2i", "Argon2d", "Scrypt", "PBKDF2"), SaltSize, HashSize, and the cost settings Argon2Memory, Argon2Iterations, Argon2Parallelism, ScryptN, ScryptR, ScryptP, Pbkdf2Iterations.

Passwords longer than MaxLength are rejected everywhere, to stop very long inputs being used to slow the server down.

Password rules

MethodDescription
:Validate(password, policy?): (boolean, reason?)Checks a new password against the rules.
:GetPolicy(policy?)The effective rules, with your overrides applied.

Reasons (PasswordSystem.PolicyReasons): "TooShort", "TooLong", "NoLetter", "NoDigit", "NoSymbol", "NoMixedCase", "Blocked". Policy overrides: MinLength, MaxLength, RequireLetter, RequireDigit, RequireSymbol, RequireMixedCase, Blocklist (compared case-insensitively).

local ok, reason = PasswordSystem:Validate(input, { MinLength = 6, Blocklist = { "password", "123456" } })

Generating

MethodDescription
:Generate(length?, alphabet?)A random password from a cryptographically secure source. The default alphabet leaves out look-alike characters.
:GeneratePin(digits?)A numeric PIN, such as a safe code.

Locks

A lock combines a stored hash with attempt limiting. Attempts are counted per key: usually the player, but any string or number works (for example a team).

MethodDescription
:CreateLock(name, hash, options?): LockA lock for an existing hash.
:CreateLockAsync(name, password, options?): Promise<Lock>Hashes the password and creates the lock.
:GetLock(name), :FindLock(name), :GetLocks(), :DestroyLock(name)
LockCreated: Signal<Lock>, LockDestroyed: Signal<string>

Options: MaxAttempts (wrong tries before lockout; 0 never locks out), AttemptWindow (seconds after which wrong tries are forgotten), LockoutSeconds.

Lock memberDescription
:VerifyAsync(password, key?): Promise<(boolean, reason?)>Tries a password. Reasons: "Wrong", "LockedOut", or "Busy" while a previous try for the same key is still being checked.
:Verify(password, key?): (boolean, reason?)The same, blocking.
:SetPasswordAsync(password)Changes the password.
HashThe stored hash. Can be replaced directly.
:GetAttempts(key), :IsLockedOut(key): (boolean, secondsLeft)
:Reset(key?)Clears attempts for one key, or all.
:Destroy()
Unlocked: Signal<key>, Failed: Signal<(key, attempts)>, LockedOut: Signal<(key, seconds)>
MaxAttempts, AttemptWindow, LockoutSecondsSettable.

Configuration

Defaults from DefaultConfiguration/Systems/PasswordSystem23 settings
SettingDefaultWhat it does
Algorithm"Argon2id"Password hash algorithm used when options pass none.
SaltSize16Random salt bytes per password hash.
HashSize32Output bytes per password hash.
Argon2Memory19456KiB of memory per Argon2 password hash.
Argon2Iterations2Passes over that memory per Argon2 password hash.
Argon2Parallelism1Parallel lanes per Argon2 password hash.
ScryptN16384scrypt CPU/memory cost parameter (a power of two).
ScryptR8scrypt block size factor.
ScryptP1scrypt parallelism factor.
Pbkdf2Iterations100000PBKDF2-HMAC-SHA256 iterations.
MinLength8Shortest password :Validate accepts, in characters.
MaxLength128Longest password accepted anywhere.
RequireLetterfalse:Validate rejects a password without a letter.
RequireDigitfalse:Validate rejects a password without a digit.
RequireSymbolfalse:Validate rejects a password without a symbol.
RequireMixedCasefalse:Validate rejects a password without both an uppercase and a lowercase letter.
Blocklist{}Passwords :Validate always rejects, compared case-insensitively.
GenerateLength16Characters in a :Generate password when no length is passed.
GenerateAlphabet"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"Characters :Generate draws from when no alphabet is passed.
PinDigits6Digits in a :GeneratePin code when no count is passed.
MaxAttempts5Wrong tries per key before a Lock locks it out; 0 never locks out.
AttemptWindow300Seconds after which a key's wrong tries on a Lock are forgotten.
LockoutSeconds60Seconds a locked-out key waits before a Lock takes its tries again.

Types

export type Bytes = buffer | string
export type Algorithm = "Argon2id" | "Argon2i" | "Argon2d" | "Scrypt" | "PBKDF2"
export type HashOptions
export type Parsed
export type Policy
export type PolicyOptions
export type PolicyReason
export type Lock
export type LockOptions
export type LockKey = Player | string | number
export type VerifyReason = "Wrong" | "LockedOut" | "Busy"