Encryption and Key Custody
toda uses envelope encryption. Each user has a random data key. The data key is wrapped by a key derived from the platform master key and the user ID.
MASTER_ENCRYPTION_KEY
-> HKDF-SHA256(userId, "toda:kek:v1")
-> per-user KEK
-> wrapped data key
-> encrypted memory bodies
Envelope model
The data key is random and independent for each user. The key-encryption key (KEK) is bound to userId through the HKDF salt.
Moving a wrapped key to another user record causes AES-GCM authentication to fail.
Legacy rows with blobs of 32 bytes or less use the deprecated derived-key path.
Parameters
| Parameter | Value |
|---|---|
| Body cipher | AES-256-GCM |
| Key size | 256 bits |
| IV | 96 random bits per encryption |
| Authentication tag | 128 bits |
| KEK derivation | HKDF-SHA256 |
| HKDF info | toda:kek:v1 |
| Key wrap | AES-256-GCM |
IVs must not be reused with the same key. The crypto package generates IVs internally and does not accept caller-provided values.
Enrollment
The first successful sign-in creates a user data key:
upsertUser(db, publicKey, authorityPubkey, (userId) =>
wrapDataKey(masterKey, userId, generateDataKey()),
);
The unwrapped key is not written to the database or logs.
Who can decrypt
The API can decrypt memory content. It holds the master key and unwraps the user data key for authenticated reads and recall.
toda is encrypted at rest, not end-to-end encrypted.
| Compromise | Result |
|---|---|
| Database only | Encrypted content is exposed |
| Database and master key | All content is exposed |
| User session | That user's content is exposed through the API |
Client-side encryption
Clients that do not trust the operator can encrypt content before writing it.
const sealed = await myEncrypt(plaintext, myKey);
await toda.remember(sealed, { alg: "xchacha20-poly1305" });
Commitments and proofs continue to work over the ciphertext.
Tradeoffs:
- server-side recall cannot search the content
- ingestion providers cannot extract memory
- the client must manage recovery and rotation
Non-sensitive search tags can be stored in metadata. Metadata remains visible to database readers.
Master key
| Property | Requirement |
|---|---|
| Variable | MASTER_ENCRYPTION_KEY |
| Format | Base64 |
| Recommended size | 32 bytes |
| Storage | Secrets manager |
Generate a key with:
openssl rand -base64 32
Master-key rotation is not implemented. Replacing the key without rewrapping every user data key makes existing memory unreadable.
Key material at a glance
| Key | Location | Access |
|---|---|---|
| Master encryption key | API environment | All user data keys |
| User data key | API memory | One user's content |
| JWT secret | API environment | Session issuance |
| Anchor authority | Anchor service | Commitment writes only |
| Wallet key | User wallet | Sign-in and stake control |
Next: Authentication.