How AFFiNE Handles Data Privacy and Security in Its Local-First Architecture
AFFiNE ensures data privacy and security through AES-256-GCM encryption-at-rest, Argon2 password hashing, and ECDSA token signing, keeping sensitive data encrypted on disk and only decrypting it in memory with a non-persisted private key.
AFFiNE (toeverything/AFFiNE) is designed as a local-first application, meaning all workspace data resides on the user's device by default rather than remote servers. This architecture demands robust cryptographic protections to safeguard persisted secrets against filesystem compromises. The platform implements a comprehensive cryptographic layer through the CryptoHelper class in packages/backend/server/src/base/helpers/crypto.ts to encrypt OAuth tokens, verification codes, and user credentials before they touch the disk.
Core Security Mechanisms
Encryption-at-Rest Using AES-256-GCM
AFFiNE protects any stored plain-text data—such as OAuth tokens and verification codes—using AES-256-GCM symmetric encryption. The encrypt() method in CryptoHelper generates a random initialization vector (IV) and authentication tag for each operation, concatenating them with the ciphertext in the format <iv><authTag><encrypted>. The result is base64-encoded for database storage. When retrieval is needed, the decrypt() method parses this structure and verifies the authentication tag before returning the plaintext only in memory.
Cryptographic Key Management
The system generates a unique EC (Elliptic Curve) key pair per instance using generateKeyPairSync('ec', {namedCurve: 'prime256v1'}) if the AFFINE_PRIVATE_KEY environment variable is not configured. The private key remains exclusively in memory and never persists to disk, while the public key is exposed for signature verification. Advanced users can override this behavior by setting AFFINE_PRIVATE_KEY in packages/backend/server/src/base/helpers/config.ts to supply their own key material, further isolating trust boundaries.
Internal API Token Signing
To guarantee that internal APIs are only accessible to trusted components, AFFiNE employs ECDSA signing for inter-service communication. The signInternalAccessToken method creates JSON payloads containing the HTTP method, path, timestamp, and nonce, then base64-url encodes and signs them with the private key. The verify() method validates these tokens using the public key, preventing unauthorized internal requests even if the network layer is compromised.
Password Hashing with Argon2
User passwords are never stored in plain text. Instead, AFFiNE utilizes the Argon2 algorithm via the @node-rs/argon2 library through the hashPassword and verifyPassword methods. This modern memory-hard hashing function resists GPU and ASIC cracking attempts. Additionally, the compare() method uses Node.js's timingSafeEqual to perform constant-time comparisons of secrets, eliminating timing side-channel vulnerabilities.
Pro License Key Protection
AFFiNE Pro features are gated by cryptographically signed licenses encrypted with a static AES key. The loadAFFiNEProLicenseAESKey() function manages the AFFINE_PRO_LICENSE_AES_KEY environment variable, ensuring license validation occurs through secure decryption rather than simple string comparison.
Data Flow in Local-First Operation
When a user adds a credential such as an OAuth token, the data flow follows this secure pipeline:
- The backend receives the plaintext token through the UI layer.
- The
CryptoHelper.encrypt()method processes the token, returning a base64 ciphertext string. - The encrypted value is persisted to the database—visible in implementations like
packages/backend/server/src/models/calendar-account.ts(which usesencryptToken) andpackages/backend/server/src/models/verification-token.ts. - When the application needs the token, it calls
crypto.decrypt(ciphertext), recovering the plain value only in active memory for the duration of the operation.
Because the private key required for decryption is ephemeral (existing only in memory or provided via environment variable), a compromised filesystem, backup, or database dump cannot reveal the original sensitive data without the corresponding key.
Implementation Examples
Encrypting and Decrypting Sensitive Values
// Assume `crypto` is an injected CryptoHelper instance
const secret = 'my-super-secret-token';
// Encrypt before persisting to database
const encrypted = crypto.encrypt(secret);
// → "b3Vja... (base64 string)"
// Later, decrypt when needed for API calls
const decrypted = crypto.decrypt(encrypted);
console.log(decrypted); // "my-super-secret-token"
Signing Internal Access Tokens
// Create a signed token for internal HTTP requests
const signed = crypto.signInternalAccessToken({
method: 'GET',
path: '/api/v1/notes',
});
// Result format: "eyJ2IjoxLCJ0cyI6MTY5... ,AbcDefGhi..."
// Verify before processing the request
if (crypto.verify(signed)) {
// Token valid - proceed with internal API call
}
Hashing and Verifying Passwords
const plain = 'user-password';
// Hash using Argon2
const hash = await crypto.encryptPassword(plain);
// Verify against stored hash
const isValid = await crypto.verifyPassword(plain, hash); // returns boolean
Critical Source Files for Security Review
packages/backend/server/src/base/helpers/crypto.ts– Core cryptographic utilities includingCryptoHelperclass, encryption/decryption, signing, and password hashing.packages/backend/server/src/models/calendar-account.ts– Demonstrates encrypted storage patterns for OAuth tokens viaencryptToken.packages/backend/server/src/models/verification-token.ts– Shows encryption implementation for temporary verification codes.packages/backend/server/src/base/helpers/config.ts– DefinesAFFINE_PRIVATE_KEYenvironment variable handling for custom key injection.SECURITY.md– Project-wide security policy and vulnerability reporting instructions.
Summary
- Local-first by design: Data remains on-device unless the user explicitly enables cloud synchronization, minimizing exposure vectors.
- AES-256-GCM encryption-at-rest: All persisted secrets are cryptographically sealed using authenticated encryption with associated data.
- Argon2 password hashing: Modern memory-hard algorithm protects user credentials against brute-force attacks.
- Ephemeral key management: The private key exists only in memory (or via user-supplied
AFFINE_PRIVATE_KEY), ensuring encrypted data remains unreadable offline. - Internal request signing: ECDSA signatures prevent unauthorized access to internal API endpoints even within the local network.
Frequently Asked Questions
How does AFFiNE protect my data if my device is stolen?
Since AFFiNE uses local-first storage with AES-256-GCM encryption-at-rest, all sensitive credentials stored in the database are encrypted blobs. The private key required for decryption resides only in application memory or is provided via the AFFINE_PRIVATE_KEY environment variable; it is never written to disk by default. An attacker with physical access to the storage medium cannot decrypt the data without the in-memory key.
What encryption standard does AFFiNE use for stored passwords?
AFFiNE utilizes the Argon2 algorithm via the @node-rs/argon2 library, implemented through hashPassword and verifyPassword methods in the CryptoHelper class. This memory-hard hashing function provides superior resistance to GPU cracking compared to older algorithms like bcrypt or PBKDF2.
Can I supply my own encryption keys to AFFiNE?
Yes. Advanced users can configure the AFFINE_PRIVATE_KEY environment variable defined in packages/backend/server/src/base/helpers/config.ts to inject their own cryptographic key material. This bypasses the automatic key generation and ensures complete control over the encryption lifecycle.
How does AFFiNE prevent unauthorized access between its internal components?
Internal API requests are secured through ECDSA-signed tokens generated by signInternalAccessToken and validated by verify(). These tokens contain method, path, timestamp, and nonce fields, preventing replay attacks and ensuring only cryptographically verified components can access sensitive internal endpoints.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →