Skip to main content

Module crypto

Module crypto 

Source
Expand description

Application-layer at-rest encryption for the OAuth secrets.

Everything security-sensitive that is persisted — the atproto OAuth access/refresh tokens, the per-DID DPoP key material, the short-lived per-auth-request state, and the confidential-client signing JWK — is AEAD-encrypted before it touches the SQLite volume (or, for the JWK, the disk file). The key comes only from the process environment, never from the volume, so a raw volume/snapshot read is useless without the running process’s environment.

Construction throughout: AES-256-GCM, random 96-bit nonce per record, 128-bit auth tag, serialized as a self-describing string. There are two formats, differing only in what is authenticated alongside the ciphertext:

enc.v1.gcm.<b64url(nonce)>.<b64url(tag)>.<b64url(ct)>    AAD = empty
enc.v2.gcm.<b64url(nonce)>.<b64url(tag)>.<b64url(ct)>    AAD = binding context

v1 is byte-identical to the Node sidecar’s crypto.ts, and must stay that way: it is what lets this implementation read what the sidecar wrote, which is what makes the cutover reversible. The cross-implementation test at the bottom of this file pins that against ciphertext from the real sidecar. It is used for the signing-key file, the one artefact a rollback must read.

v2 binds a record to where it lives. The AAD names the row and column, so a ciphertext lifted into a different row fails to authenticate rather than decrypting into the wrong place. Without it, anything able to write the database could graft one login flow’s DPoP key or issuer onto another flow’s state row. The OAuth state and session tables are new, so the bound form can be required there from the start — and it is: Aead::decrypt_bound rejects a v1 token, because accepting one would make the binding opt-out.

The prefix also lets Aead::maybe_decrypt do migrate-on-read: a stored value with NEITHER prefix is treated as legacy plaintext and returned as-is, so a file written before encryption was enabled keeps working and is transparently re-encrypted on the next write.

Structs§

Aead
A bound encryptor/decryptor holding the derived key.

Enums§

Codec
The codec actually installed: real AEAD, or a pass-through used only on a localhost dev stack where no key is configured. Production refuses to boot without a real key, so Codec::Null never runs there.

Functions§

derive_key
Derive the 32-byte AES key from the raw configured value.