Docs
Home GitHub npm

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.

ParameterValueMeaning
N131072 (217)CPU/memory cost factor — requires ~128MB of memory
r8Block size — sequential memory read size
p1Parallelization factor
Key length256 bits (32 bytes)Output encryption key size
Salt256 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.

ℹ️ Why scrypt? SaveState uses Node.js native 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.

ComponentSizePurpose
AlgorithmAES-256-GCM (authenticated encryption)
IV (Initialization Vector)128 bits (16 bytes)Random nonce, unique per encryption
Auth Tag128 bits (16 bytes)GCM authentication tag — detects tampering
Key256 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

  1. Read the version byte (currently 0x01)
  2. Extract the salt (32 bytes)
  3. Derive the key from passphrase + salt using scrypt
  4. Extract the IV (16 bytes)
  5. Extract the auth tag (16 bytes)
  6. Decrypt the ciphertext using AES-256-GCM with the derived key, IV, and auth tag
  7. GCM automatically verifies integrity — if data was tampered with, decryption fails

Passphrase Management

Key principles

Best practices

⚠ Warning: SaveState provides no key escrow, no master key, no admin backdoor. If you lose your passphrase, your snapshots are permanently unrecoverable. Treat it like a cryptocurrency seed phrase.

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.