# crypto

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

This is the Markdown version of https://osl-lang.org/packages/crypto. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
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:

```osl
import "std:crypto"

log crypto.sha256("abc")
log crypto.sha256("abd")
log crypto.hash("sha512", "abc").len
```

Output:

```text
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
128
```

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.

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

- [ ] SHA-256 is broken.
- [x] It is fast and has no salt, so attackers can try billions of guesses and reuse work between users.
- [ ] Hashes can be reversed with the right key.

Answer: Use `hashPassword`, which is slow on purpose and adds a random salt, as the next section shows.

## 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:

```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)
```

Output:

```text
true
false
false
false
```

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:

```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")
```

Output:

```text
64
true
false
```

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:

```osl
import "std:crypto"

log crypto.constantTimeCompare("token-1", "token-1")
log crypto.constantTimeCompare("token-1", "token-2")
```

Output:

```text
true
false
```

## 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:

```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
```

Output:

```text
true
meet at noon
true PermissionError
```

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:

```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()
```

Output:

```text
32
account 42
true
```

`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:

```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()
)
```

Output:

```text
private notes
true
true
```

## 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:

```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)
```

Output:

```text
true
false
```

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](/packages/random.md) for secrets, which is for games and simulations:

```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
```

Output:

```text
32
12
5
36
true true
```

`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:

```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
```

Output:

```text
true
false
64
```

### Exercise: 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`.

Starting code:

```osl
import "std:crypto"

string secret = "whsec_demo"

def isValid(string body, string signature) boolean (
  return false
)

string body = `{"event":"paid"}`
string good = crypto.hmac("sha256", secret, body)
log isValid(body, good)
log isValid(body, "0000")
log isValid(`{"event":"refund"}`, good)
```

It should print:

```text
true
false
false
```

Hint 1: Work out the expected signature with `crypto.hmac("sha256", secret, body)`.

Hint 2: `return crypto.constantTimeCompare(expected, signature)`.

A solution:

```osl
import "std:crypto"

string secret = "whsec_demo"

def isValid(string body, string signature) boolean (
  string expected = crypto.hmac("sha256", secret, body)
  return crypto.constantTimeCompare(expected, signature)
)

string body = `{"event":"paid"}`
string good = crypto.hmac("sha256", secret, body)
log isValid(body, good)
log isValid(body, "0000")
log isValid(`{"event":"refund"}`, good)
```

### Exercise: Seal and open

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

Starting code:

```osl
import "std:crypto"

def seal(string text, string password) string (
  return text
)

def open(string sealed, string password) string (
  return sealed
)

string box = seal("the cake is real", "lie")
log open(box, "lie")
log open(box, "truth")
```

It should print:

```text
the cake is real
locked
```

Hint 1: `crypto.encrypt(text, password)` seals, and `crypto.decrypt(...)` returns a result.

Hint 2: `.unwrapOr("locked")` covers the failure.

A solution:

```osl
import "std:crypto"

def seal(string text, string password) string (
  return crypto.encrypt(text, password)
)

def open(string sealed, string password) string (
  return crypto.decrypt(sealed, password).unwrapOr("locked")
)

string box = seal("the cake is real", "lie")
log open(box, "lie")
log open(box, "truth")
```

## API reference

## API reference

Every member of `std:crypto`, generated from the source of OSL.

### 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.
