Cryptographic Design

Ghost Auth v1.2 — March 2026

This document describes Ghost Auth's cryptographic architecture. All primitives use well-established, auditable libraries: the aes-gcm and argon2 crates in Rust, and Web Crypto API with hash-wasm in the browser extension.

1. Encryption at Rest

Ghost Auth uses a two-layer key architecture to protect stored accounts. A randomly generated Data Encryption Key (DEK) encrypts the vault. A Key Encryption Key (KEK), derived from the user's master password, wraps the DEK. This separation means changing the password only re-wraps the DEK — it does not re-encrypt every account.

Key hierarchy

┌────────────────────────────────────────────────────────┐ Master Password Argon2id(password, salt) ──▶ KEK (256-bit) AES-256-GCM(KEK, DEK) ──▶ Wrapped DEK (stored) DEK (256-bit, random) AES-256-GCM(DEK, vault) ──▶ Encrypted Vault (stored) └────────────────────────────────────────────────────────┘

Vault format

The encrypted vault is stored as:

nonce(12 bytes) + ciphertext(variable, AES-256-GCM)

A fresh 12-byte nonce is generated via crypto.getRandomValues() (JS) or OsRng (Rust) for every write operation. Nonces are never reused with the same key.

Wrapped key format

salt(16 bytes) + nonce(12 bytes) + ciphertext(variable, AES-256-GCM)

Platform keystores

On the desktop and mobile app, the DEK is stored in the platform's secure keystore rather than derived from a password:

PlatformKeystore
WindowsCredential Manager (via keyring crate)
macOSKeychain Services
iOSKeychain (via security-framework crate)
AndroidAndroid KeyStore — hardware-backed AES-256/GCM master key in TEE/StrongBox
LinuxSecret Service / libsecret (via keyring crate)

On Android, the raw encryption key never leaves the secure hardware. A hardware-backed master key in the TEE wraps the app's DEK; the wrapped form is stored in SharedPreferences.

2. Argon2id Parameters

All password and PIN hashing uses Argon2id (v1.3), the winner of the Password Hashing Competition, chosen for its resistance to both GPU and side-channel attacks.

ContextMemoryIterationsParallelismOutput
Vault KEK / Backup64 MB31256-bit
PIN KEK16 MB31256-bit
PIN hash64 MB31PHC string
Recovery code hash64 MB31PHC string

A fresh random 16-byte salt is generated for every key derivation. Salts are stored alongside the wrapped key (they are not secret).

3. PIN & Recovery Codes

PIN protection

When a user sets a PIN, two things happen:

1. The PIN is hashed with Argon2id and stored in PHC string format for verification.

2. The in-memory DEK is wrapped with a PIN-derived KEK (Argon2id with PIN-specific parameters) and stored separately.

On unlock, the PIN-derived KEK unwraps the DEK. If decryption fails, the PIN was incorrect — the encrypted DEK acts as implicit verification.

Rate limiting

Failed PIN attempts trigger escalating lockouts, persisted to disk so they survive app restarts:

AttemptsLockout
1–4None
5–730 seconds
8–95 minutes
10+15 minutes

Recovery codes

When a PIN is set, 8 single-use recovery codes are generated. Each code is 8 characters (format: XXXX-XXXX) from a 30-character alphabet that excludes ambiguous characters (0/O, 1/I/L). Each code is individually hashed with Argon2id and marked as used after redemption. Using a recovery code disables the PIN entirely.

4. Encrypted Backup Format

Backups use a versioned binary format with a magic header for integrity checking:

GHST(4 bytes, magic) + 0x01(1 byte, version) + salt(16 bytes) + nonce(12 bytes) + ciphertext(variable)

The encryption key is derived from the user's chosen backup password via Argon2id (64 MB, 3 iterations) with a fresh random salt. The payload (JSON array of accounts with metadata) is encrypted with AES-256-GCM. All key material is zeroed after use.

Password requirements

Backup passwords must be at least 8 characters and contain at least one digit and one special character.

5. Device Sync Protocol

Ghost Auth supports syncing accounts between devices over the local network. The protocol provides mutual authentication, forward secrecy per session, and per-account encryption.

Pairing

Devices pair via a sync code (24 characters from a 30-character alphabet, displayed as XXXX-XXXX-XXXX-XXXX-XXXX-XXXX) or by scanning a QR code containing a 256-bit random key. Both methods establish a shared secret.

Handshake

Initiator Joiner 1. Generate 32-byte nonce ────────▶ 2. ◀──────── HMAC-SHA256(nonce, key) 3. HMAC-SHA256(joiner_hmac, key) ─▶ 4. Both sides verify (constant-time comparison) 5. Derive session key: HKDF-SHA256( ikm = sync_key, salt = nonce, info = "ghost-auth-session-v1" ) ──▶ 256-bit session key

The mutual HMAC exchange proves both sides possess the sync code without transmitting it. The nonce prevents replay attacks. All comparisons use constant-time equality to prevent timing side-channels.

Data transfer

Each account is individually encrypted with AES-256-GCM using the session key and a fresh random nonce. The encrypted payload includes device IDs, timestamps, and tombstones for conflict resolution (last-writer-wins with 90-day tombstone retention).

Per-account wire format: length(4 bytes) + nonce(12 bytes) + ciphertext(AES-256-GCM)

6. Browser Extension

The browser extension cannot use platform keystores, so it provides two modes for protecting the DEK:

Password mode (default)

Identical to the desktop key hierarchy: the user's master password derives a KEK via Argon2id, which wraps the DEK. The wrapped DEK is stored in browser.storage.local. The plaintext DEK is cached in browser.storage.session (cleared on browser close) during an active session.

Passwordless mode

A non-extractable AES-256-GCM CryptoKey is generated via Web Crypto API and stored in IndexedDB. This key wraps the DEK. Because the key is non-extractable, the raw key material is never exposed to JavaScript — even code running in the extension context cannot read it. Unwrapping requires the browser to perform the decryption internally.

Auto-lock

The extension uses browser alarms to automatically clear the cached DEK after a configurable timeout (default: 5 minutes of inactivity), requiring re-authentication.

7. Key Zeroing

Sensitive key material is zeroed from memory as soon as it is no longer needed:

Rust — The zeroize crate provides Zeroizing<[u8; 32]> wrappers that overwrite memory on drop, even in the presence of compiler optimizations.

JavaScriptUint8Array.fill(0) is called on KEKs and DEKs after every encrypt/decrypt operation, in both success and error paths.

Tauri commands — PINs passed from the frontend are zeroed after processing.

8. File Permissions

All sensitive files written to disk (encrypted vault, PIN hash, rate limit state, crash queue) are protected with restrictive permissions:

PlatformPermission
Unix (macOS, Linux)0o600 — owner read/write only
Windowsicacls — removes inherited ACLs, grants only current user + SYSTEM

Files are written atomically (write to temporary file, then rename) to prevent corruption from interrupted writes.

9. Crash Report Encryption

When crash reporting is enabled (opt-in), reports are queued to an encrypted on-disk file before transmission. The queue encryption key is derived from the main keystore key via HMAC-SHA256 with a domain-specific label (ghost-auth-crash-queue), ensuring it is independent of the vault DEK.

All report data passes through a multi-layer sanitizer before touching disk or network: otpauth:// URIs are redacted, base32 sequences (32+ characters) are stripped, home directory paths are reduced to basenames, and 15 denylist keywords (including secret, pin, password, token, sync_code) trigger value redaction in any key=value pair.

Summary of Primitives

PurposePrimitive
Data encryptionAES-256-GCM (12-byte random nonce)
Key derivationArgon2id v1.3 (64 MB / 16 MB memory)
Sync session keyHKDF-SHA256
Sync authenticationHMAC-SHA256 (constant-time comparison)
Key storagePlatform keychain / Android KeyStore TEE
Nonce generationcrypto.getRandomValues() / OsRng (CSPRNG)
Key cleanupzeroize crate (Rust) / fill(0) (JS)
TOTP generationHMAC-SHA1/SHA256/SHA512 (RFC 6238)

Source Code

All cryptographic implementations are open source. Key files:

src-tauri/src/storage.rs — vault encryption
src-tauri/src/keystore.rs — platform keychain integration
src-tauri/src/pin.rs — PIN hashing, rate limiting, recovery codes
src-tauri/src/backup.rs — backup encryption format
src-tauri/src/crash_reporter.rs — crash queue encryption & sanitization
extension/src/core/crypto.ts — AES-GCM, HMAC, constant-time comparison
extension/src/core/storage.ts — extension vault, passwordless mode

Review the full source on GitHub.