Encryption
AES-256-GCM authenticated encryption with scrypt key derivation. Your data is encrypted before it leaves your machine. We never see your data.
How It Works
Every snapshot is encrypted end-to-end using a key derived from your passphrase. The encryption pipeline looks like this:
User passphrase
→ scrypt (N=2^17, r=8, p=1) # Memory-hard KDF, ~128MB
→ 256-bit encryption key # Derived fresh each time
→ AES-256-GCM encryption # Authenticated encryption
→ .saf.enc file # Self-contained encrypted archive
Key Derivation (scrypt)
Your passphrase is converted to a 256-bit encryption key using scrypt, a memory-hard key derivation function designed to resist brute-force attacks.
| Parameter | Value | Meaning |
|---|---|---|
N | 131072 (217) | CPU/memory cost factor — requires ~128MB of memory |
r | 8 | Block size — sequential memory read size |
p | 1 | Parallelization factor |
| Key length | 256 bits (32 bytes) | Output encryption key size |
| Salt | 256 bits (32 bytes) | Random, unique per encryption |
These parameters are comparable to Argon2id defaults, making brute-force attacks extremely expensive. Each derivation uses a fresh random salt, so identical passphrases produce different keys.
crypto.scryptSync() — no external dependencies. scrypt is memory-hard, audited, and widely deployed. It's used by cryptocurrency wallets, password managers, and enterprise security tools.
AES-256-GCM Encryption
Data is encrypted using AES-256-GCM (Galois/Counter Mode), which provides both confidentiality and integrity in a single operation.
| Component | Size | Purpose |
|---|---|---|
| Algorithm | — | AES-256-GCM (authenticated encryption) |
| IV (Initialization Vector) | 128 bits (16 bytes) | Random nonce, unique per encryption |
| Auth Tag | 128 bits (16 bytes) | GCM authentication tag — detects tampering |
| Key | 256 bits (32 bytes) | Derived from passphrase via scrypt |
GCM mode means decryption will fail if even a single bit has been tampered with. You get confidentiality + integrity + authenticity in one pass.
Encrypted File Format
Each .saf.enc file is self-contained — everything needed to decrypt (except your passphrase) is embedded in the header:
┌─────────────────────────────────────────────────┐
│ Byte 0 │ Version (0x01) │
│ Bytes 1–32 │ Salt (32 bytes, random) │
│ Bytes 33–48 │ IV (16 bytes, random) │
│ Bytes 49–64 │ Auth Tag (16 bytes) │
│ Bytes 65+ │ Ciphertext (variable) │
└─────────────────────────────────────────────────┘
Header overhead: 65 bytes
Decryption process
- Read the version byte (currently
0x01) - Extract the salt (32 bytes)
- Derive the key from passphrase + salt using scrypt
- Extract the IV (16 bytes)
- Extract the auth tag (16 bytes)
- Decrypt the ciphertext using AES-256-GCM with the derived key, IV, and auth tag
- GCM automatically verifies integrity — if data was tampered with, decryption fails
Passphrase Management
Key principles
- Never stored: Your passphrase is never written to disk. It's used to derive the key in memory, then discarded.
- Per-encryption salt: Each encryption generates a fresh random salt, so the same passphrase produces different ciphertext.
- No recovery: There is no "forgot password" mechanism. If you lose your passphrase, your data is gone. This is by design.
Best practices
- Use a strong passphrase — a random string or multiple random words
- Store your passphrase in a password manager (1Password, Bitwarden, etc.)
- Don't use the same passphrase as your email or other critical accounts
- Consider writing it down and storing it in a safe (for true disaster recovery)
Verification
You can verify that a snapshot is decryptable without actually extracting it:
// The verify function attempts decryption and returns true/false
async function verify(data: Buffer, passphrase: string): Promise<boolean>
This is used internally to validate passphrases during restore.
Security Model
🔐 Client-side only
All encryption/decryption happens on your machine. Data is encrypted before it touches any storage backend.
🎲 Random salt + IV
Fresh 32-byte salt and 16-byte IV per encryption. Same data + same passphrase = different ciphertext.
✅ Authenticated
GCM mode provides integrity verification. Tampered data is detected and rejected during decryption.
🧱 Memory-hard KDF
scrypt requires ~128MB to derive one key. GPU/ASIC brute-force attacks are extremely expensive.