Skip to main content

Data Security

toda provides integrity and ownership guarantees for AI-agent memory. It provides confidentiality at rest under an operator-held key. It does not provide availability, and it is not an end-to-end encrypted system.

This page states the guarantees, the assumptions they rest on, and the attacks they do not stop. Read it before you rely on any of them.

What toda guarantees

Integrity

Given a memory, its inclusion proof, and read access to Solana, any party can determine whether that memory was part of the owner's committed log - with no cooperation from toda.

Concretely, an adversary who fully controls toda's database and API cannot:

  • alter a memory's content or metadata without every proof over it failing;
  • insert a memory into the past - the root over a different leaf set is a different root, and the on-chain nonce orders every commit;
  • delete a memory silently - memory_count is monotonic on-chain, so a shorter log cannot be anchored, and a root over a shortened leaf set contradicts the tip;
  • backdate a memory earlier than the block time of the transaction that anchored it.

Ownership

A user's commitment account is a PDA derived from their wallet public key. Ownership is address-derived, not assigned. toda does not maintain a mapping from users to accounts that it could tamper with; the mapping is a hash function.

Read authorization at the API is gated on proving control of that wallet, via Sign-In With Solana.

Confidentiality at rest

Memory bodies are encrypted with AES-256-GCM under a per-user data key before being written. A database dump, a stolen backup, or a compromised replica yields ciphertext. See encryption and key custody.

Trust assumptions

Every guarantee above holds only if all of these hold. State them out loud:

AssumptionIf violated
SHA-256 is collision- and second-preimage-resistantProofs can be forged. The entire construction fails.
Solana finalizes and does not reorg the anchoring transactionAn anchor can be rolled back; the tip regresses. Wait for finalized rather than confirmed for high-value claims.
The reader fetches the on-chain root from an RPC they trustAn RPC that lies about the tip can validate any root. This is why verify() re-reading toda's supplied onchainRoot is not sufficient.
The master encryption key is not disclosedAll memory plaintext at rest is recoverable by the holder.
The anchoring authority key is not disclosedAn attacker can stall anchoring or anchor an incorrect root. Both are detectable by the owner; neither exposes plaintext.
The API's process memory is not read by an adversaryPlaintext is transiently present during encrypt/decrypt.
JWT_SECRET is not disclosedSession tokens can be forged for any user, granting read access to their decrypted memories.
The owner's wallet private key is not disclosedFull account takeover at the API, and full control of any stake vault.

Threat model

Defended

AdversaryCapabilityOutcome
Database attackerFull read/write on PostgresReads ciphertext, not plaintext. Any write is detectable via proofs.
Malicious toda operatorControls API and database, not the master keyCannot rewrite history undetectably. Cannot regress the count.
Malicious toda operatorControls the anchoring authorityCan stall or misanchor. Cannot decrypt. Detected by root comparison.
Network attackerMITM on API trafficTLS. Additionally, SIWS nonces are single-use and DB-persisted, so a captured challenge signature is not replayable.
Replay attackerCaptures a signed SIWS messageThe nonce is atomically consumed on verify. A replay finds no nonce and is rejected.
Proof forgerWants a proof for a memory never writtenMust find a SHA-256 second preimage, or produce a root matching the on-chain tip over a leaf set they do not control.
Capability-link thiefSteals an agent case URLBounded by expiry (≤ 24h, 15 min default) and read count (≤ 10, 1 by default). Minting a new case revokes prior ones.

Not defended

State these plainly. They are consequences of the design, not oversights, and each has a documented reason.

The platform can read your memories. The master encryption key lives in the API's environment, and the API unwraps every user's data key on every request. This is a product requirement - the API must decrypt to serve recall(), and search over ciphertext is not implemented. toda is encrypted at rest, not end-to-end encrypted. If your threat model includes the operator, encrypt content yourself before calling remember(); the leaf commits to whatever bytes you pass, and the proof works identically over a ciphertext you control.

Availability is not guaranteed. toda commits to bytes; it does not host them redundantly. If your Postgres is lost, the commitment survives on-chain and proves nothing you can still produce. Back up the database. If you need availability guarantees, store the ciphertext on a storage network and put the blob identifier in metadata - the commitment then covers the pointer.

Metadata is public to anyone with database access. It is stored as plaintext JSONB and hashed into the leaf. There is no path where metadata is confidential.

Timing and volume leak. Anchoring transactions are public. An observer of the chain learns when a given wallet's log grew and by how many memories - the memory_count delta. They learn nothing about content. If write-pattern metadata is sensitive, this is a real leak.

The authority can withhold anchoring. A user cannot force their own commit today: commit_root accepts the owner as signer, but the anchor service is the party that computes the root. An owner who wants to anchor unilaterally must run the tree computation themselves.

No rate limiting or quota enforcement. The API does not throttle writes. Deploy behind a gateway that does.

Denial of service is out of scope. An attacker who can exhaust the API or the database can stop you writing memories. They cannot alter or forge the ones already anchored.

Data lifecycle

StageLocationProtection
In flight, client → APINetworkTLS (your deployment's responsibility)
In useAPI process memoryTransient plaintext during encrypt/decrypt
At rest, bodyPostgres memories.ciphertextAES-256-GCM, per-user data key
At rest, metadataPostgres memories.metadataNone. Plain JSONB
At rest, key materialPostgres users.wrapped_data_keyAES-256-GCM under a master-derived, user-bound KEK
CommittedSolanaPublic. A 32-byte hash.

Reporting

Security issues should be reported privately to the maintainers before public disclosure. Do not open a public issue for a vulnerability that affects deployed keys or user data.

Next: Encryption and key custody.