# At-Rest Storage Encryption (V3.2)

**Status:** Implemented in `@shade/storage-encrypted` 0.4.0
**Adresses:** THREAT-MODEL §4 — Compromised device storage

Shade's default `SQLiteStorage` and `PostgresStorage` write private keys and
session state to disk *unencrypted* — the threat model assumes the DB lives
inside a trusted environment. For deployments that need defence in depth,
`@shade/storage-encrypted` adds opt-in at-rest encryption: a stolen DB file
alone yields no usable private key material.

## At a glance

```ts
import { KeyManager, EncryptedSQLiteStorage } from '@shade/storage-encrypted';

const km = await KeyManager.open({
  kind: 'passphrase',
  passphrase: process.env.SHADE_STORAGE_PASSPHRASE!,
  salt: loadSaltFromDisk(), // 16+ bytes, persisted alongside the DB
});

const storage = await EncryptedSQLiteStorage.open({
  dbPath: '/data/shade-client.db',
  keyManager: km,
});

// Use it exactly like SQLiteStorage — implements the same StorageProvider.
const manager = new ShadeSessionManager(crypto, storage);
```

## What is encrypted

Per-row AEAD over the sensitive payload of every row:

| Table                          | Encrypted |
|--------------------------------|-----------|
| `identity_enc`                 | the entire keypair (4× 32-byte keys) |
| `config_enc`                   | `registrationId` |
| `signed_prekeys_enc`           | full `SignedPreKey` (incl. private half) |
| `one_time_prekeys_enc`         | full `OneTimePreKey` |
| `sessions_enc`                 | the Double-Ratchet `SessionState` JSON |
| `trusted_identities_enc`       | the trusted peer identity key |
| `retired_identities_enc`       | full retired keypair |
| `stream_state_enc.ciphertext`  | partition / lane / IO descriptor / streamSecret |

Routing fields on `stream_state_enc` (`stream_id`, `direction`,
`peer_address`, `status`, timestamps) stay plaintext so `listActiveStreamStates()`
remains an indexed query.

## Cryptographic design

```
masterKey (passphrase / keychain / app-injected)
   │
   ├─ HKDF-SHA-256("shade-storage-v1") → storageKey  (32 bytes)
   │     └─ HKDF-SHA-256(storageKey, "shade-field-v1:{table}:{column}") → fieldKey (32 bytes)
   │
   └─ Used (transitively) for fingerprint checks
```

For each encrypted blob:

- `nonce = HKDF(fieldKey, "shade-row-nonce-v1:{table}:{pk}")[..12]` —
  deterministic per (key, row), safe because the per-(table, column)
  fieldKey is unique. AES-GCM nonce reuse is catastrophic only if the
  *same* key is reused with the *same* nonce on different plaintexts;
  here every (key, row) pair has a unique nonce.
- `aad = "shade-aad-v1|{table}|{column}|{pk}"` — binds the ciphertext
  to its row identity so a row swap or column move triggers decrypt
  failure.
- `wire = nonce(12) || ciphertext || tag(16)` — stored as a single
  `BLOB`/`BYTEA` column.

## Key sources

`KeyManager.open(...)` accepts three sources:

1. **Passphrase + KDF** — scrypt over `(passphrase, salt)`. Default
   parameters: `N=2^17, r=8, p=1, dkLen=32` (~250 ms on a modern laptop).
   The salt MUST be persisted alongside the DB (e.g. `<db>.salt`).
2. **OS keychain** — via `@shade/keychain`. Backends:
   - macOS: `security` CLI (Keychain).
   - Linux: `secret-tool` (libsecret).
   - Windows: PowerShell + `CredentialManager` module.
   No native deps; `createIfMissing: true` generates and stores a fresh
   32-byte key.
3. **App-injected** — caller supplies a 32-byte raw key. Most flexible;
   plug your own KMS / HSM / Vault path here.

Wrong-passphrase detection is built in: a fingerprint of the storageKey
is persisted in `shade_meta_enc` on first open and compared on every
subsequent open. A mismatch raises with a clear error — never silently
writing under the wrong key.

## Migration

CLI:

```bash
# Encrypt an existing unencrypted DB (atomic per row, .bak written first).
shade migrate-storage \
  --key-source passphrase \
  --passphrase "$SHADE_STORAGE_PASSPHRASE" \
  --salt-file /data/shade-client.db.salt

# Validate without writing.
shade migrate-storage ... --dry-run

# Keychain mode.
shade migrate-storage --key-source keychain \
  --keychain-service shade.storage --keychain-account default

# Inject a raw key (e.g. from your KMS).
shade migrate-storage --key-source injected \
  --key-hex "$(cat ~/.shade/storage.key.hex)"
```

The migration is *resumable*: re-running it on a partially-migrated DB
re-writes the same rows under the same key (idempotent). On clean
completion, the unencrypted tables are dropped (use `--keep-original`
to preserve them).

## Rotation

```bash
shade rotate-storage-key \
  --key-source passphrase     --passphrase "$OLD_PASS" \
  --new-key-source passphrase --new-passphrase "$NEW_PASS" \
  --new-salt-file /data/shade-client.db.salt.new
```

Reads each encrypted row under the old key, re-seals under the new key.
The DB stays online; brief read-after-write inconsistency for in-flight
readers is acceptable for the supported deployments (CLI tools,
single-process servers). On completion the fingerprint is updated and
the old key no longer opens the DB.

## What this does *not* protect

Even with at-rest enabled:

- A live process holds the storageKey and fieldKeys in memory. An attacker
  who can dump process memory (`/proc/<pid>/mem`, swap, hibernation,
  coredump) recovers the keys.
- Swap is not encrypted by Shade. Use an encrypted swap device.
- The `.bak` file produced during migration is plaintext during the
  migration window. Treat it like the original DB and store securely.
- Lost master key = lost DB. V3.10 (Social Recovery) is the long-term
  mitigation.

See `THREAT-MODEL.md` §4 for the full list, including the "with at-rest
enabled" boundary.

## Cross-implementation parity

`test-vectors/storage-encryption.json` pins KDF parameters, info strings,
nonce derivation, and AAD format. The Android implementation (V3.5) MUST
produce byte-identical outputs for the same inputs — covered by
`packages/shade-storage-encrypted/tests/test-vectors.test.ts`.
