End-to-End Encryption
Enable P2P encryption and zero-knowledge data protection in Persist stores.
End-to-End Encryption
Enable encryption
import { createStore } from "@cuitty/persist";
const store = await createStore({
name: "my-app",
adapter: "sqlite",
path: "./data/my-app.db",
encrypt: true,
});
When encrypt: true is set, all data is encrypted on the device before it is written to disk or sent over the network. The sync server, storage backend, and any intermediary never see plaintext.
How it works
Persist generates a 256-bit device key on first run and stores it in the OS keychain (or a local keyfile as fallback). Every record is encrypted with AES-256-GCM using a per-record nonce derived from the key and the record’s path.
plaintext --> AES-256-GCM encrypt --> ciphertext --> store / sync
Reads reverse the process transparently. Application code works with plain objects — encryption and decryption are invisible to the caller.
Key management
Device keys
Each device generates its own key on first use. The key never leaves the device unless explicitly exported.
// Export the device key (for backup or migration)
const exportedKey = await store.crypto.exportKey();
// Import a key on a new device
const store = await createStore({
name: "my-app",
adapter: "sqlite",
path: "./data/my-app.db",
encrypt: true,
key: exportedKey,
});
Key rotation
Rotate the encryption key without downtime. Persist re-encrypts existing records in the background.
await store.crypto.rotateKey();
After rotation the old key is kept in a sealed keyring so previously-synced peers can still decrypt historical data until they receive the new key.
Encrypted sync
When encryption and sync are both enabled, peers exchange ciphertext. Decryption happens only on the receiving device using a shared key.
const store = await createStore({
name: "my-app",
adapter: "sqlite",
path: "./data/my-app.db",
encrypt: true,
sync: {
remote: "postgres",
strategy: "last-write-wins",
},
});
Sharing keys between peers
Peers use a Diffie-Hellman key exchange to establish a shared secret. No key material is transmitted in the clear.
// On device A: generate an invite
const invite = await store.crypto.createInvite();
// --> send `invite.code` to device B out-of-band
// On device B: accept the invite
await store.crypto.acceptInvite(invite.code);
Once paired, both devices derive the same data key and can decrypt each other’s records.
Zero-knowledge architecture
The sync server stores only ciphertext and opaque metadata (record size, sync timestamps). It cannot:
- Read record contents
- Identify record types or schemas
- Correlate records across stores (keys are encrypted too)
Even if the server is compromised, an attacker gains no usable data without the device keys.