Packages

crypto

Hash data, store passwords safely, encrypt text and files, sign messages and make secure random values.

import "std:crypto"

std:crypto is the toolbox for the security basics: fingerprints of data, password storage, encryption, signatures and random values you can trust. Every function takes text or bytes, so you can pass a string anywhere a byte[] is wanted.

Random results change on every run, so the examples print lengths and yes/no answers instead of the random values. Change them and run again.

Hash something#

A hash turns any data into a short fixed-size fingerprint. The same input always gives the same fingerprint, and a different input gives a different one:

main.osl
import "std:crypto"

log crypto.sha256("abc")
log crypto.sha256("abd")
log crypto.hash("sha512", "abc").len
Click the code to edit it

Try changing the text by one letter. Every hex digit changes. Use sha256 for checksums and fingerprints. md5 and sha1 exist for old formats, but they are broken for anything security related.

crypto.hashFile(path) hashes a file without loading it all, which is how you check a download.

Quick check

You store sha256(password) in your database. Why is that a bad way to keep passwords?

Store passwords#

hashPassword returns text safe to keep. verifyPassword checks a login attempt against it. Hash the same password twice and you get two different results, because each has its own random salt:

main.osl
import "std:crypto"

string stored = crypto.hashPassword("hunter2").unwrap()

log crypto.verifyPassword("hunter2", stored)
log crypto.verifyPassword("Hunter2", stored)
log crypto.hashPassword("hunter2").unwrap() == stored
log crypto.needsRehash(stored)
Click the code to edit it

Never compare password hashes with == yourself. verifyPassword does it correctly. When needsRehash(stored) turns true (the recommended strength went up), hash the password again at the next successful login and save the new hash.

Message authentication#

An HMAC is a hash that needs a secret key. Only someone with the key can produce or check it, which makes it the tool for signing webhooks and cookies:

main.osl
import "std:crypto"

string key = "shared secret"
string tag = crypto.hmac("sha256", key, "amount=10")

log tag.len
log tag == crypto.hmac("sha256", key, "amount=10")
log tag == crypto.hmac("sha256", key, "amount=99")
Click the code to edit it

When you compare two secret values, use constantTimeCompare(a, b). It takes the same time whether they differ early or late, so an attacker can't learn the answer from timing:

main.osl
import "std:crypto"

log crypto.constantTimeCompare("token-1", "token-1")
log crypto.constantTimeCompare("token-1", "token-2")
Click the code to edit it

Encrypt with a password#

encrypt(data, password) returns text. decrypt turns it back, and fails when the password is wrong or the text was changed:

main.osl
import "std:crypto"

string sealed = crypto.encrypt("meet at noon", "correct horse")
log sealed != "meet at noon"
log crypto.decrypt(sealed, "correct horse").unwrap()

auto wrong = crypto.decrypt(sealed, "battery staple")
log wrong.isErr() wrong.unwrapErr().kind
Click the code to edit it

Encryption is authenticated: a wrong password and a tampered message both fail with a PermissionError, and neither gives you garbage text. Try deleting a character from sealed before decrypting.

Encrypt with a key#

When you have a random key of your own instead of a password, use aes256Encrypt and aes256Decrypt with a 32-byte key. Make one from random hex:

main.osl
import "std:crypto"

byte[] key = crypto.fromHex(crypto.randomHex(32)).unwrap()
log key.len

string box = crypto.aes256Encrypt("account 42", key).unwrap()
log crypto.aes256Decrypt(box, key).unwrap()

byte[] other = crypto.fromHex(crypto.randomHex(32)).unwrap()
log crypto.aes256Decrypt(box, other).isErr()
Click the code to edit it

toHex and fromHex convert between bytes and hex text. fromHex fails with a ParseError for text that isn't hex.

Encrypt files#

encryptFile(from, to, password) and decryptFile do the same to a file on disk:

main.osl
import "std:crypto"
import "std:fs"

fs.writeFile("notes.txt", "private notes").unwrap()
crypto.encryptFile("notes.txt", "notes.enc", "pw").unwrap()
crypto.decryptFile("notes.enc", "notes.out", "pw").unwrap()

log fs.readFile("notes.out").unwrap()
log fs.readFile("notes.enc").unwrap() != "private notes"
log crypto.decryptFile("notes.enc", "bad.out", "nope").isErr()

for name in ["notes.txt", "notes.enc", "notes.out", "bad.out"] (
  fs.remove(name, {force: true}).unwrap()
)
Click the code to edit it

Sign and verify#

A signature proves who wrote a message. ed25519GenerateKeyPair() returns a private key you keep and a public key you share. Anyone with the public key can check a signature, and nobody without the private key can make one:

main.osl
import "std:crypto"

auto keys = crypto.ed25519GenerateKeyPair()
string signature = crypto.ed25519Sign(keys.private, "release v1.2")

log crypto.ed25519Verify(keys.public, "release v1.2", signature)
log crypto.ed25519Verify(keys.public, "release v1.3", signature)
Click the code to edit it

Keys and signatures are plain text (base64url), so they fit in a file or a header.

Random values#

Use these for tokens, ids and anything an attacker mustn't guess. Don't use random for secrets, which is for games and simulations:

main.osl
import "std:crypto"

log crypto.randomHex(16).len
log crypto.randomString(12).len
log crypto.randomString(5, "01").len
log crypto.randomUUID().len

int roll = crypto.randomBetween(1, 6)
log roll >= 1 roll <= 6
Click the code to edit it

randomHex(n) gives n bytes as 2n characters. Pass an alphabet as the second argument of randomString to limit the characters.

Stretching a password into a key#

deriveKey(password, salt) turns a password into key text with PBKDF2. It is deterministic: the same password and salt always give the same key, so store the salt next to the data:

main.osl
import "std:crypto"

string a = crypto.deriveKey("pw", "salt-1", {iterations: 1000})
string b = crypto.deriveKey("pw", "salt-1", {iterations: 1000})
string c = crypto.deriveKey("pw", "salt-2", {iterations: 1000})

log a == b
log a == c
log a.len
Click the code to edit it
Your turn

Sign a webhook

A webhook is accepted when its signature header equals the HMAC-SHA256 of the body under the shared secret. Write isValid(body, signature) that returns true for a matching signature. Use constantTimeCompare.

main.osl
Ctrl Enter runs
Expected output
true
false
false
Your turn

Seal and open

Write seal(text, password) and open(sealed, password). open should return the text, or "locked" when the password is wrong.

main.osl
Ctrl Enter runs
Expected output
the cake is real
locked

API reference#

50 members, generated from the std:crypto source of OSL 9.3.0.

Functions

#crypto.aes256Decrypt(string data, byte[] key) result<string, error>

aes256Decrypt decrypts text from crypto.aes256Encrypt with the same 32-byte key. Fails with ValueError for a key that isn't 32 bytes, ParseError when data isn't aes256Encrypt's output and PermissionError for the wrong key or modified data.

#crypto.aes256Encrypt(byte[] data, byte[] key) result<string, error>

aes256Encrypt encrypts data with AES-256-GCM under key and returns hex text that crypto.aes256Decrypt reads. key is exactly 32 random bytes, such as crypto.fromHex(crypto.randomHex(32)).unwrap(); to encrypt with a password, use crypto.encrypt(data, password), which stretches it. Every call uses a fresh random nonce, and the result is authenticated, so tampering is detected. Fails with ValueError for a key that isn't 32 bytes.

#crypto.base64Decode(any data) string

Deprecated. Deprecated: use atob(text) instead.

base64Decode decodes standard base64, returning "" for invalid input.

#crypto.base64Encode(any data) string

Deprecated. Deprecated: use btoa(data) instead.

base64Encode encodes data as standard base64.

#crypto.bcryptHash(any password, any ...costs) string

Deprecated. Deprecated: use crypto.hashPassword(password, {algorithm: "bcrypt", cost: n}) instead, or crypto.hashPassword(password) for Argon2id.

bcryptHash hashes a password with bcrypt at the given cost (default 10), returning "" for a cost outside 4 to 31 or a password it can't hash.

#crypto.bcryptVerify(any password, any storedHash) boolean

Deprecated. Deprecated: use crypto.verifyPassword(password, hash) instead, which checks bcrypt hashes too.

bcryptVerify reports whether password matches a bcrypt hash.

#crypto.binDecode(any data) string

Deprecated. Deprecated: use text.decodeBin() instead, which fails with ParseError for invalid input.

binDecode reads space-separated binary bytes, returning "" for invalid input.

#crypto.binEncode(any data) string

Deprecated. Deprecated: use text.encodeBin() instead.

binEncode writes each byte of data in binary, separated by spaces.

#crypto.constantTimeCompare(byte[] a, byte[] b) boolean

constantTimeCompare reports whether a and b are equal, taking the same time wherever they differ, so comparing a secret such as an HMAC doesn't leak how much of it matched.

#crypto.decrypt(string data, string password) result<string, error>

decrypt decrypts text from crypto.encrypt with the same password, including text from earlier versions of encrypt. Fails with ParseError when data isn't encrypt's output and with PermissionError for the wrong password or modified data.

#crypto.decryptFile(string inputPath, string outputPath, string password) result<unit, error>

decryptFile decrypts a file written by crypto.encryptFile with the same password and writes the original bytes to outputPath, readable only by its owner when new. Fails like crypto.decrypt for the wrong password or a file that isn't encrypted, and with NotFoundError, PermissionError or IOError when a file can't be read or written.

#crypto.deriveKey(string password, byte[] salt, object? options) string

deriveKey derives a key from a password and salt with PBKDF2 and returns it as hex. Use a random salt of at least 16 bytes and store it with the result. Options: iterations (default 600000), length in bytes (default 32) and hash (default "sha256", any name crypto.hash accepts). Throws ValueError for iterations or a length below 1 or an unknown hash.

#crypto.ed25519GenerateKeyPair() {private: string, public: string}

ed25519GenerateKeyPair returns a new Ed25519 key pair as {private, public}: the 32-byte private seed and the public key, each as unpadded base64url text. Keep private secret; share public so others can check your signatures with crypto.ed25519Verify.

#crypto.ed25519Sign(string privateKey, byte[] data) string

ed25519Sign signs data with an Ed25519 private key, given as base64url or hex (the 32-byte seed or the 64-byte private key), and returns the signature as unpadded base64url text. Throws ValueError for a key that isn't one of those.

#crypto.ed25519Verify(string publicKey, byte[] data, string signature) boolean

ed25519Verify reports whether signature, as base64url, is a valid Ed25519 signature of data by the holder of publicKey, given as base64url or hex. A malformed key or signature is false.

#crypto.encrypt(byte[] data, string password) string

encrypt encrypts data with a password and returns text that crypto.decrypt turns back into data with the same password. The password is stretched with Argon2id and a random salt, and the data is sealed with AES-256-GCM, so the result reveals nothing but the data's length and any change to it is detected.

#crypto.encryptFile(string inputPath, string outputPath, string password) result<unit, error>

encryptFile encrypts the file at inputPath with a password, like crypto.encrypt, and writes the text to outputPath. Fails with NotFoundError, PermissionError or IOError when a file can't be read or written.

#crypto.fromHex(string text) result<byte[], error>

fromHex decodes hexadecimal text, in either case, to bytes. Fails with ParseError for an odd length or a character that isn't a hex digit.

#crypto.generateKeyPair() object

Deprecated. Deprecated: use crypto.ed25519GenerateKeyPair() instead, which encodes the keys as base64url like the signatures; both encodings sign and verify.

generateKeyPair returns a new Ed25519 key pair as hex text.

#crypto.hash(string algorithm, byte[] data) string

hash returns the hex digest of data with the named algorithm: md5, sha1, sha224, sha256, sha384, sha512, sha3-224, sha3-256, sha3-384 or sha3-512, in any case. A string is hashed as its UTF-8 bytes. Throws ValueError for an unknown algorithm.

#crypto.hashDirectory(string path, object? options) result<string, error>

hashDirectory returns one hex digest for every file below a directory, covering each file's path relative to it and its contents, so it changes when a file is added, removed, renamed or edited. Option: algorithm (default "sha256"). Fails with UnsupportedError when the tree contains a symbolic link, and with NotFoundError, PermissionError or IOError when it can't be read.

#crypto.hashFile(string path, object? options) result<string, error>

hashFile returns the hex digest of a file's contents, reading it in pieces so large files don't have to fit in memory. Option: algorithm (default "sha256", any name crypto.hash accepts). Fails with NotFoundError, PermissionError or IOError when the file can't be read.

#crypto.hashPassword(string password, object? options) result<string, error>

hashPassword hashes a password for storage, with a random salt, so it can later be checked with crypto.verifyPassword. It uses Argon2id by default, giving a "$argon2id$..." string. Options: algorithm ("argon2id" or "bcrypt") and cost (bcrypt's work factor from 4 to 31, default 10). Fails with ValueError when bcrypt is asked to hash a password longer than its limit of 72 bytes; throws ValueError for a bad option.

#crypto.hexDecode(any data) string

Deprecated. Deprecated: use crypto.fromHex(text) or text.decodeHex(), which fail with ParseError for invalid hex.

hexDecode decodes hex text, returning "" when it isn't valid hex.

#crypto.hexEncode(any data) string

Deprecated. Deprecated: use crypto.toHex(data) or text.encodeHex() instead.

hexEncode encodes data as hex.

#crypto.hmac(string algorithm, byte[] key, byte[] data) string

hmac returns the hex HMAC of data under key with the named hash algorithm, as listed for crypto.hash. Compare HMACs with crypto.constantTimeCompare. Throws ValueError for an unknown algorithm.

#crypto.hmacSha256(any key, any data) string

Deprecated. Deprecated: use crypto.hmac("sha256", key, data) instead.

hmacSha256 returns the hex HMAC-SHA256 of data under key.

#crypto.hmacSha512(any key, any data) string

Deprecated. Deprecated: use crypto.hmac("sha512", key, data) instead.

hmacSha512 returns the hex HMAC-SHA512 of data under key.

#crypto.md5(byte[] data) string

md5 returns the hex MD5 digest of data. MD5 is broken for security; use it only for checksums and compatibility.

#crypto.md5Hash(any data) string

Deprecated. Deprecated: use crypto.md5(data) instead.

md5Hash returns the hex MD5 digest of data.

#crypto.needsRehash(string hash) boolean

needsRehash reports whether a stored password hash is weaker than what crypto.hashPassword produces now: a bcrypt or old salted SHA-256 hash, or Argon2id with lighter settings. After a successful crypto.verifyPassword, store crypto.hashPassword(password) in its place.

#crypto.pbkdf2(any password, any salt, any iterations, any keyLen, any hashFunc) string

Deprecated. Deprecated: use crypto.deriveKey(password, salt, {iterations, length, hash}) instead.

pbkdf2 derives a PBKDF2 key as hex, returning "" for bad arguments.

#crypto.random(any min, any max) any

Deprecated. Deprecated: use crypto.randomBetween(min, max) instead, whose bounds are both included.

random returns a random whole number from min up to but not including max.

#crypto.randomBetween(int min, int max) int

randomBetween returns a cryptographically secure random whole number from min to max, including both ends, like random.between. The bounds may come in either order.

#crypto.randomBytes(any size) string

Deprecated. Deprecated: use crypto.randomHex(size) instead, which returns the same text and throws ValueError for a bad size.

randomBytes returns size random bytes as hex, or "" for a size of 0 or less or over 16777216.

#crypto.randomFloat(any ...bounds) number

randomFloat returns a cryptographically secure random number. randomFloat() is from 0 up to but not including 1, randomFloat(max) from 0 up to max, and randomFloat(min, max) from min up to max, or min when max isn't above it. Throws TypeError for more than two arguments or one that isn't a number.

#crypto.randomHex(int size) string

randomHex returns size cryptographically secure random bytes as hex, so the text is twice size characters long. Throws ValueError for a size outside 0 to 16777216.

#crypto.randomInt(any ...args) int

Deprecated. Deprecated: use crypto.randomBetween(min, max) instead, whose bounds are both included.

randomInt returns a random whole number from 0 up to but not including max, or from min up to but not including max. Throws TypeError without arguments.

#crypto.randomString(int length, any ...alphabet) string

randomString returns length cryptographically secure random letters and digits, or characters of alphabet when given, as in crypto.randomString(6, "0123456789"). Throws ValueError for a length outside 0 to 16777216 or an empty alphabet.

#crypto.randomUUID() string

randomUUID returns a random version 4 UUID, such as "3b241101-e2bb-4255-8caf-4136c566a962".

#crypto.secureErase(string path) result<unit, error>

secureErase overwrites a file's contents with random bytes three times, flushing each pass to the device, and then deletes it. This is best effort: on SSDs, copy-on-write or journaling filesystems, and wherever backups or snapshots exist, copies of the old data can survive, so use full-disk encryption when that matters. Fails with UnsupportedError for anything but a regular file, and with NotFoundError, PermissionError or IOError when it can't be written or removed.

#crypto.sha1(byte[] data) string

sha1 returns the hex SHA-1 digest of data. SHA-1 is broken for security; use it only for checksums and compatibility.

#crypto.sha256(byte[] data) string

sha256 returns the hex SHA-256 digest of data.

#crypto.sha3_256(any data) string

Deprecated. Deprecated: use crypto.hash("sha3-256", data) instead.

sha3_256 returns the hex SHA3-256 digest of data.

#crypto.sha512(byte[] data) string

sha512 returns the hex SHA-512 digest of data.

#crypto.sign(any key, any data) string

Deprecated. Deprecated: this is not a secure signature, because SHA-256 of a key followed by data can be extended by anyone who has seen one signature. Use crypto.hmac("sha256", key, data) and check it with crypto.constantTimeCompare, or crypto.ed25519Sign for public-key signatures.

sign returns sha256(key + data) as hex.

#crypto.toHex(byte[] data) string

toHex encodes data as lowercase hexadecimal text.

#crypto.uuidv4() string

Deprecated. Deprecated: use crypto.randomUUID() instead.

uuidv4 returns a random version 4 UUID.

#crypto.verify(any key, any data, any signature) boolean

Deprecated. Deprecated: crypto.sign is not a secure signature (see crypto.sign). Use crypto.hmac("sha256", key, data) with crypto.constantTimeCompare, or crypto.ed25519Verify.

verify reports whether signature is sha256(key + data) as hex.

#crypto.verifyPassword(string password, string hash) boolean

verifyPassword reports whether password matches a hash from crypto.hashPassword: an Argon2id or bcrypt hash, or the salted SHA-256 format hashPassword produced in earlier versions. A hash it can't read is false.