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 text

n 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(16) gives "62255d0c38b6025f20e05a97b941ea4b"

crypto.random_bytes: inputs flowing into the function and the resultn8the system's secure random sourceunpredictable bytescrypto.random_bytesresult · hex text, 2 characters per bytebd9494449baea8f1n16the system's secure random sourceunpredictable bytescrypto.random_bytesresult · hex text, 2 characters per byte62255d0c38b6025f20e05a97b941ea4bn32the system's secure random sourceunpredictable bytescrypto.random_bytesresult · hex text, 2 characters per byteca082a209552781f3fccdb4c0ef7f67f72f9649bf81c4445b6df9b92ed44c453

Real result from a run while building the site; it is different every time.

random_token(bytes?)

returns text

A 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(32) gives "kDUfsxB0vk3XC1ckHxNACU5oemjCkXXiV-HoRGMesxQ"

crypto.random_token: inputs flowing into the function and the resultbytes?8secure random byteswritten as URL-safe base64crypto.random_tokenresult · a token for links and session idsWRjzIgMAFxMbytes?16secure random byteswritten as URL-safe base64crypto.random_tokenresult · a token for links and session ids6H9Se-dD-nzDwbX3zwJepAbytes?32secure random byteswritten as URL-safe base64crypto.random_tokenresult · a token for links and session idskDUfsxB0vk3XC1ckHxNACU5oemjCkXXiV-HoRGMesxQ

Real result from a run while building the site; it is different every time.

aes_gcm_encrypt(key, text, aad?)

returns text

Encrypt text with AES-GCM. It is authenticated: any tampering is detected when decrypting.

Arguments

  • key hex text of 32, 48 or 64 characters. Make one with crypto.random_bytes(32).
  • text the text to encrypt.
  • aad optional 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"

crypto.aes_gcm_encrypt: inputs flowing into the function and the resultkey000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1ftextcard 4111 1111aad?order-7crypto.aes_gcm_encryptresult · base64 of nonce + ciphertext + tagAHRtBJl79QDEvQXfimTik12nQiY+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 text

Decrypt 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: inputs flowing into the function and the resultkey000102… 1d1e1f(the same key)database64 fromaes_gcm_encrypt()aad?order-7crypto.aes_gcm_decryptresult · textcard 4111 1111key000102… 1d1e1f(the same key)database64 fromaes_gcm_encrypt()aad?order-8crypto.aes_gcm_decryptstops with an errorcrypto.aes_gcm_decrypt(): wrongkey, wrong extra data (aad), orthe data was changed (the tagdoesn't match).

Real result: worked out by running this example with TB while the site was built.

aes_cbc_encrypt(key, text, iv?)

returns text

Encrypt text with AES-CBC and PKCS#7 padding. Use it to talk to older systems. Prefer aes_gcm_encrypt(), which also detects tampering.

Arguments

  • key hex text of 32, 48 or 64 characters.
  • text the text to encrypt.
  • iv optional 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="

crypto.aes_cbc_encrypt: inputs flowing into the function and the resultkey000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1ftexthelloiv?00112233445566778899aabbccddeeffcrypto.aes_cbc_encryptresult · base64 of iv + ciphertextABEiM0RVZneImaq7zN3u/7kCxdH99BBi9XWAjhO2KEk=

Real result: worked out by running this example with TB while the site was built.

aes_cbc_decrypt(key, data)

returns text

Decrypt 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"

crypto.aes_cbc_decrypt: inputs flowing into the function and the resultkey000102… 1d1e1fdatabase64 fromaes_cbc_encrypt()crypto.aes_cbc_decryptresult · texthello

Real result: worked out by running this example with TB while the site was built.

pbkdf2(password, salt, iterations, length?, hash?)

returns text

Turn 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, 16, "sha256") gives "c4e0e76476eb5d2601bd7e85994b0517"

Drawing…

Real result: worked out by running this example with TB while the site was built.

scrypt(password, salt, n?, r?, p?, length?)

returns text

Turn 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"

crypto.scrypt: inputs flowing into the function and the resultpasswordhunter2salta1b2c3d4n?1024r?8p?1length?16crypto.scryptresult · the derived key (hex)837f04585bf28ff60dbf57d149a08354

Real result: worked out by running this example with TB while the site was built.

hmac(key, text, hash?)

returns text

A keyed signature (HMAC) of text, as hex. Compare it with hash.compare(), so timing can't leak the answer.

Options

  • hash the 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: inputs flowing into the function and the resultkeys3crettextamount=10&to=bobhash?sha256crypto.hmacresult · the signature (hex)74b4738a88397a0cf0646c138e29ac139294c3a0404ee5ddb44d636dd3800bb3keys3crettextamount=10&to=bobhash?sha1crypto.hmacresult · the signature (hex)e17b61c9db1f24646390e8ba7a3f598438b77ac6keys3crettextamount=10&to=bobhash?md5crypto.hmacresult · the signature (hex)7b0667f8fd01ae7ae1faaa47a594238f

Real result: worked out by running this example with TB while the site was built.

totp(secret, time?, options?)

returns text

The 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

  • digits how long the code is (default: 6).
  • period seconds each code lasts (default: 30).
  • hash the hash to use (default: "sha1").
Visualizehow crypto.totp works

crypto.totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 59, {"digits": 6}) gives "287082"

crypto.totp: moments on a time linetime →time = 59in the 30-60 s stepone code per 30 ssecretGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQtime?59options?digits6crypto.totpresult · the code an authenticator app shows287082

Real result: worked out by running this example with TB while the site was built.

verify_totp(secret, code, time?, options?)

returns bool

True 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, hash as for totp().
  • window how many steps either side to accept (default: 1).
Visualizehow crypto.verify_totp works

crypto.verify_totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", "287082", 59, {"window": 1}) gives true

crypto.verify_totp: inputs flowing into the function and the resultsecretGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQcode287082time?59options?window1crypto.verify_totpresult · booltruesecretGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQcode123456time?59options?window1crypto.verify_totpresult · boolfalse

Real result: worked out by running this example with TB while the site was built.

hotp(secret, counter, digits?)

returns text

The 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: inputs flowing into the function and the resultsecretGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQcounter0digits?6crypto.hotpresult · text755224secretGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQcounter1digits?6crypto.hotpresult · text287082secretGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQcounter2digits?6crypto.hotpresult · text359152secretGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQcounter3digits?6crypto.hotpresult · text969429

Real result: worked out by running this example with TB while the site was built.

totp_secret(bytes?)

returns text

A 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"

crypto.totp_secret: inputs flowing into the function and the resultbytes?20crypto.totp_secretresult · base32: put it in a QR codeZO2RMQYF4SGGE6AJOCZWN4DHHNCJI5HY

Real result from a run while building the site; it is different every time.

jwt_sign(payload, key, alg?)

returns text

Make 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…

crypto.jwt_sign: inputs flowing into the function and the resultpayloadsub"ada"role"admin"keys3cretalg?HS256crypto.jwt_signresult · header.payload.signatureeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGEiLCJyb2xlIjoiYWRtaW4ifQ.p58RLbxPJXILLd8TsQMszoNIYiZk2InlKT2BoUQh3bc

Real result: worked out by running this example with TB while the site was built.

jwt_verify(token, key, options?)

returns map

Check 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

  • leeway seconds of clock difference to allow (default: 60).
  • now a timestamp to use as the current time.
  • algs a 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: inputs flowing into the function and the resulttokeneyJhbGciOiJIUzI1NiIs…(from jwt_sign)keys3cretoptions?algs["HS256"]crypto.jwt_verifyresult · the payload, once it checks outsub"ada"role"admin"tokeneyJhbGciOiJIUzI1NiIs…(from jwt_sign)keyguessoptions?algs["HS256"]crypto.jwt_verifystops with an errorcrypto.jwt_verify(): the signature doesn't match, so the token waschanged or signed with anotherkey.

Real result: worked out by running this example with TB while the site was built.

jwt_decode(token)

returns map

Read 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…

crypto.jwt_decode: inputs flowing into the function and the resulttokeneyJhbGciOiJIUzI1NiIs…(any JWT)crypto.jwt_decoderesult · a look inside, NOT verifiedheader{map}alg"HS256"typ"JWT"payload{map}sub"ada"role"admin"alg"HS256"expiredfalseverifiedfalse

Real result: worked out by running this example with TB while the site was built.

password_hash(password)

returns text

Hash 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…

crypto.password_hash: inputs flowing into the function and the resultpasswordcorrect horsescrypt + a random saltn=16384, r=8, p=1crypto.password_hashresult · store this, never the password$scrypt$n=16384,r=8,p=1$QQ8aonWXV30IMR-dv0g-wA$Ge8MzNEs9993qhh-1Y4VkWiTRUFwe1BR7OtrrBLD9r4

Real result from a run while building the site; the random salt makes it different every time.

password_verify(password, hash)

returns bool

True if password matches a hash from password_hash() (constant-time compare).

Visualizehow crypto.password_verify works

crypto.password_verify("correct horse", stored) gives true

crypto.password_verify: inputs flowing into the function and the resultpasswordcorrect horsehash$scrypt$n=16384,r=8,p=1$…(from password_hash)crypto.password_verifyresult · booltruepasswordCorrect horsehash$scrypt$n=16384,r=8,p=1$…(from password_hash)crypto.password_verifyresult · boolfalse

Real result: worked out by running this example with TB while the site was built.

rsa_verify(text, signature, public_key, hash?)

returns bool

True if signature is a valid RSA PKCS#1 v1.5 signature of text. Pure Python (pow()), no OpenSSL.

Arguments

  • signature base64 or hex text.
  • public_key a PEM public key. One of:
    • a PUBLIC KEY block (-----BEGIN PUBLIC KEY-----)
    • an RSA PUBLIC KEY block
    • a CERTIFICATE
  • hash the 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 true

crypto.rsa_verify: inputs flowing into the function and the resulttextrelease 3.0.0signatureWZKq9ZAcO7l4seqb…(base64)public_keyBEGIN PUBLICKEY(1024-bit demo)hash?sha256crypto.rsa_verifyresult · booltruetextrelease 3.0.1signatureWZKq9ZAcO7l4seqb…(base64)public_keyBEGIN PUBLICKEY(1024-bit demo)hash?sha256crypto.rsa_verifyresult · boolfalse

Real result: worked out by running this example with TB while the site was built.

public_key(pem)

returns map

Read 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…

crypto.public_key: inputs flowing into the function and the resultpem-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEB…crypto.public_keyresult · mapkind"rsa"bits1024e65537

Real result: worked out by running this example with TB while the site was built.

x509_parse(pem_or_der, now?)

returns map

Read 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…

crypto.x509_parse: inputs flowing into the function and the resultpem_or_der-----BEGIN CERTIFICATE-----MIICYjCCAcugAwIB…now?1790000000crypto.x509_parseresult · mapcommon_name"intranet.example.com"dns_names["intranet.example.com"]not_after"2030-01-01 00:00:00"days_left1197self_signedtrue

Real result: worked out by running this example with TB while the site was built.

x509_parse_all(pem)

returns list

Every 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…

crypto.x509_parse_all: inputs flowing into the function and the resultpemtwo CERTIFICATE blocks(a chain file)crypto.x509_parse_allresult · one map per certificatecommon_namedays_leftself_signedintranet.exa…1157trueintranet.exa…1157true

Real result: worked out by running this example with TB while the site was built.

hex_to_base64(hex)

returns text

Re-encode hex text (a key or hash) as base64.

Visualizehow crypto.hex_to_base64 works

crypto.hex_to_base64("4d5a9000") gives "TVqQAA=="

crypto.hex_to_base64: inputs flowing into the function and the resulthex4d5a9000crypto.hex_to_base64result · textTVqQAA==

Real result: worked out by running this example with TB while the site was built.

base64_to_hex(base64)

returns text

Re-encode base64 text as hex.

Visualizehow crypto.base64_to_hex works

crypto.base64_to_hex("TVqQAA==") gives "4d5a9000"

crypto.base64_to_hex: inputs flowing into the function and the resultbase64TVqQAA==crypto.base64_to_hexresult · text4d5a9000

Real result: worked out by running this example with TB while the site was built.