# How AFFiNE Handles Data Privacy and Security in Its Local-First Architecture

> Discover how AFFiNE's local-first architecture protects your data privacy and security with advanced encryption and secure hashing methods. Keep your information safe.

- Repository: [Toeverything/AFFiNE](https://github.com/toeverything/AFFiNE)
- Tags: deep-dive
- Published: 2026-03-05

---

**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`](https://github.com/toeverything/AFFiNE/blob/main/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`](https://github.com/toeverything/AFFiNE/blob/main/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:

1. The backend receives the plaintext token through the UI layer.
2. The `CryptoHelper.encrypt()` method processes the token, returning a base64 ciphertext string.
3. The encrypted value is persisted to the database—visible in implementations like [`packages/backend/server/src/models/calendar-account.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/backend/server/src/models/calendar-account.ts) (which uses `encryptToken`) and [`packages/backend/server/src/models/verification-token.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/backend/server/src/models/verification-token.ts).
4. 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

```typescript
// 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

```typescript
// 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

```typescript
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`](https://github.com/toeverything/AFFiNE/blob/main/packages/backend/server/src/base/helpers/crypto.ts)** – Core cryptographic utilities including `CryptoHelper` class, encryption/decryption, signing, and password hashing.
- **[`packages/backend/server/src/models/calendar-account.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/backend/server/src/models/calendar-account.ts)** – Demonstrates encrypted storage patterns for OAuth tokens via `encryptToken`.
- **[`packages/backend/server/src/models/verification-token.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/backend/server/src/models/verification-token.ts)** – Shows encryption implementation for temporary verification codes.
- **[`packages/backend/server/src/base/helpers/config.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/backend/server/src/base/helpers/config.ts)** – Defines `AFFINE_PRIVATE_KEY` environment variable handling for custom key injection.
- **[`SECURITY.md`](https://github.com/toeverything/AFFiNE/blob/main/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`](https://github.com/toeverything/AFFiNE/blob/main/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.