Security
End-to-end encryption model — per-record content keys, key wrapping schemes, and access control.
End-to-end encryption
All user content is encrypted client-side before sending to the server. The server stores encrypted fields as opaque enc_* blobs and never sees plaintext.
Per-record content key
Each record has its own random AES-256-GCM key (content_key). This key encrypts all enc_* fields on the record and its nested data (subtasks, attachments).
Key wrapping
The content_key is wrapped and stored in two ways depending on context:
Direct ownership — every record carries owner_id and owner_wrapped_key:
| Prefix | Scheme | Used for |
|---|---|---|
0x01 |
AES-GCM(account_sym_key, content_key) |
owner_wrapped_key on the record — symmetric wrap with owner’s account key |
0x02 |
ECDH(recipient_pub_key, content_key) |
wrapped_content_key in *_user_role — asymmetric wrap for shared recipients |
Container sharing — folders, groups, albums, and drive folders use *_user_role inlines (e.g. folder_user_role, group_user_role) to grant access to other users. Individual records (notes, tasks, contacts, photos) do not have their own *_user_role — ownership is tracked directly via owner_id + owner_wrapped_key. When a record belongs to a shared container, it also carries a folder_wrapped_key (or equivalent) that chains the record’s content key to the container’s key.
Wire format
Encrypted field names on the wire use the enc_ prefix: enc_title, enc_notes, enc_name, etc.
Each value is a base64-encoded blob:
nonce (12 bytes) || ciphertext || auth_tag (16 bytes)
The client sends encrypted values in the same format via create and updateOneById. The server stores and returns them as-is.
Access control
Records are scoped by ownership and container sharing:
- Every record has an
owner_idfield — the server sets it to the authenticated user on create - The owner always has access via
owner_wrapped_key(symmetric, prefix0x01) - Container models (folders, groups, albums) use
*_user_roleinlines to share access with other users - Shared recipients decrypt via the container’s
wrapped_content_key(ECDH, prefix0x02), then chain throughfolder_wrapped_key/group_wrapped_key/album_wrapped_keyto reach the record’s content key - Users cannot enumerate records they don’t own or that aren’t in a container shared with them