crypto
Encryption and the building blocks around it: AES-GCM and AES-CBC (pure Python, so fine for secrets and small files, slow for gigabytes), random keys, PBKDF2 and scrypt key derivation, HMAC, TOTP one-time codes, JWTs (sign HS256/384/512, verify HS* and RS*), scrypt password hashing, RSA signature checks and X.509 certificate parsing. Keys and random bytes are hex text; encrypted data is base64 text.
Everything works in the playground; the first run of a program that imports crypto loads a little more of Python (about 2 MB) for scrypt and PBKDF2.
import crypto
Example
import crypto
let key = crypto.random_bytes(32)
let sealed = crypto.aes_gcm_encrypt(key, "meet at noon")
say(crypto.aes_gcm_decrypt(key, sealed))
let token = crypto.jwt_sign({"sub": "tanisha", "role": "analyst"}, "demo-secret")
say(crypto.jwt_verify(token, "demo-secret").sub)
let stored = crypto.password_hash("correct horse")
say(crypto.password_verify("correct horse", stored), crypto.password_verify("wrong", stored))
say(crypto.totp("JBSWY3DPEHPK3PXP", 59))
Functions
random_bytes(n)
returns textn cryptographically random bytes as hex text (2n characters): random_bytes(32) makes an AES-256 key, random_bytes(16) a salt or token.
Visualizehow crypto.random_bytes works
crypto.random_bytes(8) gives "bd9494449baea8f1"crypto.random_bytes(16) gives "62255d0c38b6025f20e05a97b941ea4b"crypto.random_bytes(32) gives "ca082a209552781f3fccdb4c0ef7f67f72f9649bf81c4445b6df9b92ed44c453"
Real result from a run while building the site; it is different every time.
random_token(bytes?)
returns textA random URL-safe token (base64 without padding) of bytes random bytes (default 32), for session ids, reset links and API keys.
Visualizehow crypto.random_token works
crypto.random_token(8) gives "WRjzIgMAFxM"crypto.random_token(16) gives "6H9Se-dD-nzDwbX3zwJepA"crypto.random_token(32) gives "kDUfsxB0vk3XC1ckHxNACU5oemjCkXXiV-HoRGMesxQ"
Real result from a run while building the site; it is different every time.
aes_gcm_encrypt(key, text, aad?)
returns textEncrypt text with AES-GCM. It is authenticated: any tampering is detected when decrypting.
Arguments
keyhex text of 32, 48 or 64 characters. Make one with crypto.random_bytes(32).textthe text to encrypt.aadoptional extra text, like a record id. It is not encrypted, but it must match when decrypting.
Returns
- base64 text holding the nonce, the ciphertext and the tag.
- A fresh random 12-byte nonce is used every time.
Pure Python, so it runs at about 0.5 MB/s.
Example
let key = crypto.random_bytes(32)
let secret = crypto.aes_gcm_encrypt(key, "meet at noon")
say(crypto.aes_gcm_decrypt(key, secret))
Visualizehow crypto.aes_gcm_encrypt works
crypto.aes_gcm_encrypt("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "card 4111 1111", "order-7") gives "AHRtBJl79QDEvQXfimTik12nQiY+0T0zCZkSHZPP+poKCx2KaOt7I3qE"
Real result from a run while building the site; a fresh random nonce makes it different every time.
aes_gcm_decrypt(key, data, aad?)
returns textDecrypt what aes_gcm_encrypt() made (base64 text). A wrong key, wrong aad or changed data gives a catchable error instead of garbage.
Visualizehow crypto.aes_gcm_decrypt works
crypto.aes_gcm_decrypt(k, box, "order-7") gives "card 4111 1111"crypto.aes_gcm_decrypt(k, box, "order-8") stops with an error: crypto.aes_gcm_decrypt(): wrong key, wrong extra data (aad), or the data was changed (the…
Real result: worked out by running this example with TB while the site was built.
aes_cbc_encrypt(key, text, iv?)
returns textEncrypt text with AES-CBC and PKCS#7 padding. Use it to talk to older systems. Prefer aes_gcm_encrypt(), which also detects tampering.
Arguments
keyhex text of 32, 48 or 64 characters.textthe text to encrypt.ivoptional hex text of 32 characters (default: random).
Returns base64 text holding the iv and then the ciphertext.
Visualizehow crypto.aes_cbc_encrypt works
crypto.aes_cbc_encrypt("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "hello", "00112233445566778899aabbccddeeff") gives "ABEiM0RVZneImaq7zN3u/7kCxdH99BBi9XWAjhO2KEk="
Real result: worked out by running this example with TB while the site was built.
aes_cbc_decrypt(key, data)
returns textDecrypt what aes_cbc_encrypt() made (base64 text of iv + ciphertext). Bad padding (wrong key or changed data) gives a catchable error.
Visualizehow crypto.aes_cbc_decrypt works
crypto.aes_cbc_decrypt(k, box) gives "hello"
Real result: worked out by running this example with TB while the site was built.
pbkdf2(password, salt, iterations, length?, hash?)
returns textTurn a password into a key with PBKDF2-HMAC (default sha256, length 32 bytes), as hex text. salt is text or hex from random_bytes(16); use 600000+ iterations for password storage.
Visualizehow crypto.pbkdf2 works
crypto.pbkdf2("hunter2", "a1b2c3d4", 1000, 8, "sha256") gives "c4e0e76476eb5d26"crypto.pbkdf2("hunter2", "a1b2c3d4", 1000, 16, "sha256") gives "c4e0e76476eb5d2601bd7e85994b0517"crypto.pbkdf2("hunter2", "a1b2c3d4", 1000, 32, "sha256") gives "c4e0e76476eb5d2601bd7e85994b05179fc019ec3e98866180f3c57127ced273"
Drawing…
Real result: worked out by running this example with TB while the site was built.
scrypt(password, salt, n?, r?, p?, length?)
returns textTurn a password into a key with scrypt (memory-hard, so GPUs can't guess fast), as hex text. Defaults: n = 16384, r = 8, p = 1, length = 32 bytes.
Visualizehow crypto.scrypt works
crypto.scrypt("hunter2", "a1b2c3d4", 1024, 8, 1, 16) gives "837f04585bf28ff60dbf57d149a08354"
Real result: worked out by running this example with TB while the site was built.
hmac(key, text, hash?)
returns textA keyed signature (HMAC) of text, as hex. Compare it with hash.compare(), so timing can't leak the answer.
Options
hashthe hash to use (default: sha256). One of:- sha256, sha1, sha384, sha512 or md5
Visualizehow crypto.hmac works
crypto.hmac("s3cret", "amount=10&to=bob", "sha256") gives "74b4738a88397a0cf0646c138e29ac139294c3a0404ee5ddb44d636dd3800bb3"crypto.hmac("s3cret", "amount=10&to=bob", "sha1") gives "e17b61c9db1f24646390e8ba7a3f598438b77ac6"crypto.hmac("s3cret", "amount=10&to=bob", "md5") gives "7b0667f8fd01ae7ae1faaa47a594238f"
Real result: worked out by running this example with TB while the site was built.
totp(secret, time?, options?)
returns textThe 6-digit one-time code an authenticator app shows right now. secret is the base32 text behind the QR code. time is an optional timestamp to use instead of now.
Options
digitshow long the code is (default: 6).periodseconds each code lasts (default: 30).hashthe hash to use (default: "sha1").
Visualizehow crypto.totp works
crypto.totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 59, {"digits": 6}) gives "287082"
Real result: worked out by running this example with TB while the site was built.
verify_totp(secret, code, time?, options?)
returns boolTrue if code is the current one-time code for the secret. The code just before or after also counts, because clocks drift. It is compared in constant time.
Options
digits, period, hashas for totp().windowhow many steps either side to accept (default: 1).
Visualizehow crypto.verify_totp works
crypto.verify_totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", "287082", 59, {"window": 1}) gives truecrypto.verify_totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", "123456", 59, {"window": 1}) gives false
Real result: worked out by running this example with TB while the site was built.
hotp(secret, counter, digits?)
returns textThe counter-based one-time code (HOTP, RFC 4226) for a base32 secret.
Visualizehow crypto.hotp works
crypto.hotp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 0, 6) gives "755224"crypto.hotp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 1, 6) gives "287082"crypto.hotp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 2, 6) gives "359152"crypto.hotp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 3, 6) gives "969429"
Real result: worked out by running this example with TB while the site was built.
totp_secret(bytes?)
returns textA new random base32 secret (default 20 bytes = 32 characters) to put in an authenticator app.
Visualizehow crypto.totp_secret works
crypto.totp_secret(20) gives "ZO2RMQYF4SGGE6AJOCZWN4DHHNCJI5HY"
Real result from a run while building the site; it is different every time.
jwt_sign(payload, key, alg?)
returns textMake a signed JSON Web Token from a payload map with a shared secret: alg is HS256 (default), HS384 or HS512. Add "exp" (a timestamp) to the payload yourself to make it expire. RS* signing needs a private key and isn't included; verifying RS* tokens is (jwt_verify).
Visualizehow crypto.jwt_sign works
crypto.jwt_sign({"sub": "ada", "role": "admin"}, "s3cret", "HS256") gives "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGEiLCJyb2xlIjoiYWRtaW4ifQ.p58RLbxPJXIL…
Real result: worked out by running this example with TB while the site was built.
jwt_verify(token, key, options?)
returns mapCheck a JWT's signature and times, and give back its payload.
The key
- For HS256, HS384 or HS512: the shared secret.
- For RS256, RS384 or RS512: a PEM public key or certificate (checked with pure-Python RSA).
Options
leewayseconds of clock difference to allow (default: 60).nowa timestamp to use as the current time.algsa list like ["HS256"] to allow only some algorithms.
Errors
Each one is catchable, and err.reason says which:
"signature"the signature is bad."alg"the token says "alg": "none" (or an algorithm you did not allow)."expired"the token has expired (its exp time)."not_yet"the token is not valid yet (its nbf time).
Visualizehow crypto.jwt_verify works
crypto.jwt_verify(token, "s3cret", {"algs": ["HS256"]}) gives {"sub": "ada", "role": "admin"}crypto.jwt_verify(token, "guess", {"algs": ["HS256"]}) stops with an error: crypto.jwt_verify(): the signature doesn't match, so the token was changed or signed with…
Real result: worked out by running this example with TB while the site was built.
jwt_decode(token)
returns mapRead a JWT WITHOUT checking its signature. Good for a look inside. Never trust the payload without jwt_verify().
Returns a map with
"header"the token's header map."payload"the token's payload map."alg"the algorithm the header names."expired"true if its exp time has passed."verified"always false.
Visualizehow crypto.jwt_decode works
crypto.jwt_decode(token) gives {"header": {"alg": "HS256", "typ": "JWT"}, "payload": {"sub": "ada", "role": "admin"}, "a…
Real result: worked out by running this example with TB while the site was built.
password_hash(password)
returns textHash a password for storing with scrypt and a random salt. The answer looks like $scrypt$n=16384,r=8,p=1$<salt>$<hash> and password_verify() checks it. Never store passwords any other way.
Visualizehow crypto.password_hash works
crypto.password_hash("correct horse") gives "$scrypt$n=16384,r=8,p=1$QQ8aonWXV30IMR-dv0g-wA$Ge8MzNEs9993qhh-1Y4VkWiTRUFwe1BR7OtrrBLD9…
Real result from a run while building the site; the random salt makes it different every time.
password_verify(password, hash)
returns boolTrue if password matches a hash from password_hash() (constant-time compare).
Visualizehow crypto.password_verify works
crypto.password_verify("correct horse", stored) gives truecrypto.password_verify("Correct horse", stored) gives false
Real result: worked out by running this example with TB while the site was built.
rsa_verify(text, signature, public_key, hash?)
returns boolTrue if signature is a valid RSA PKCS#1 v1.5 signature of text. Pure Python (pow()), no OpenSSL.
Arguments
signaturebase64 or hex text.public_keya PEM public key. One of:- a PUBLIC KEY block (-----BEGIN PUBLIC KEY-----)
- an RSA PUBLIC KEY block
- a CERTIFICATE
hashthe hash used (default: sha256). One of:- sha256, sha1, sha384 or sha512
Visualizehow crypto.rsa_verify works
crypto.rsa_verify("release 3.0.0", "WZKq9ZAcO7l4seqb1KdK81DbE6xTDCPWuQ/jIU4Yi/VlChtw1MFamdJ7dhIFhLIVRcIHqbmkyO+qaFi1RPUuwAfmE6HMgwgpRRSj1UAMG6UgLsHZpOjBhZv17JEVUH4KKiYpXmU/lM2oK7ocQxVJVx1MHAMdU/19+Rdm/ZOYvn4=", pub, "sha256") gives truecrypto.rsa_verify("release 3.0.1", "WZKq9ZAcO7l4seqb1KdK81DbE6xTDCPWuQ/jIU4Yi/VlChtw1MFamdJ7dhIFhLIVRcIHqbmkyO+qaFi1RPUuwAfmE6HMgwgpRRSj1UAMG6UgLsHZpOjBhZv17JEVUH4KKiYpXmU/lM2oK7ocQxVJVx1MHAMdU/19+Rdm/ZOYvn4=", pub, "sha256") gives false
Real result: worked out by running this example with TB while the site was built.
public_key(pem)
returns mapRead a PEM public key, or the key inside a certificate.
Returns a map with
"kind""rsa", "ec" or "ed25519"."bits"the key size.- "n" (hex) and "e": for RSA keys.
"curve"for EC keys.
Visualizehow crypto.public_key works
crypto.public_key(pub) gives {"kind": "rsa", "n": "9085ff8fb9e3882bd909320146e55f42942bd79654cfd69abf7706b33a343151a1e…
Real result: worked out by running this example with TB while the site was built.
x509_parse(pem_or_der, now?)
returns mapRead an X.509 certificate. Give it PEM text, or DER as base64 or hex text. now is an optional timestamp to judge validity against.
Returns a map with
"subject", "issuer"maps with CN, O, C... and "text"."common_name"the subject's CN."dns_names"the host names the certificate covers."sans"all alternative names (dns, ip, email, uri)."not_before", "not_after"when it starts and stops being valid."valid_now", "expired", "days_left"its validity now (or at now)."is_ca", "self_signed"what kind of certificate it is."key_usage", "ext_key_usage"what the key may be used for."signature_algorithm"how it was signed."public_key"a map with kind and bits."serial"the serial number."fingerprint_sha256", "fingerprint_sha1", "fingerprint_md5"colon hex."extensions"the names of its extensions, sorted."pem"the certificate as PEM text.
Visualizehow crypto.x509_parse works
crypto.x509_parse(cert, 1790000000) gives {"version": 3, "serial": "1a2b3c", "subject": {"CN": "intranet.example.com", "O": "Exampl…
Real result: worked out by running this example with TB while the site was built.
x509_parse_all(pem)
returns listEvery certificate in PEM text (a chain file), each parsed like x509_parse().
Visualizehow crypto.x509_parse_all works
crypto.x509_parse_all(cert + cert) gives [{"version": 3, "serial": "1a2b3c", "subject": {"CN": "intranet.example.com", "O": "Examp…
Real result: worked out by running this example with TB while the site was built.
hex_to_base64(hex)
returns textRe-encode hex text (a key or hash) as base64.
Visualizehow crypto.hex_to_base64 works
crypto.hex_to_base64("4d5a9000") gives "TVqQAA=="
Real result: worked out by running this example with TB while the site was built.
base64_to_hex(base64)
returns textRe-encode base64 text as hex.
Visualizehow crypto.base64_to_hex works
crypto.base64_to_hex("TVqQAA==") gives "4d5a9000"
Real result: worked out by running this example with TB while the site was built.