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
Vault format
The encrypted vault is stored as:
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
Platform keystores
On the desktop and mobile app, the DEK is stored in the platform's secure keystore rather than derived from a password:
| Platform | Keystore |
|---|---|
| Windows | Credential Manager (via keyring crate) |
| macOS | Keychain Services |
| iOS | Keychain (via security-framework crate) |
| Android | Android KeyStore — hardware-backed AES-256/GCM master key in TEE/StrongBox |
| Linux | Secret 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.
| Context | Memory | Iterations | Parallelism | Output |
|---|---|---|---|---|
| Vault KEK / Backup | 64 MB | 3 | 1 | 256-bit |
| PIN KEK | 16 MB | 3 | 1 | 256-bit |
| PIN hash | 64 MB | 3 | 1 | PHC string |
| Recovery code hash | 64 MB | 3 | 1 | PHC 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:
| Attempts | Lockout |
|---|---|
| 1–4 | None |
| 5–7 | 30 seconds |
| 8–9 | 5 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:
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
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).
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.
JavaScript — Uint8Array.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:
| Platform | Permission |
|---|---|
| Unix (macOS, Linux) | 0o600 — owner read/write only |
| Windows | icacls — 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
| Purpose | Primitive |
|---|---|
| Data encryption | AES-256-GCM (12-byte random nonce) |
| Key derivation | Argon2id v1.3 (64 MB / 16 MB memory) |
| Sync session key | HKDF-SHA256 |
| Sync authentication | HMAC-SHA256 (constant-time comparison) |
| Key storage | Platform keychain / Android KeyStore TEE |
| Nonce generation | crypto.getRandomValues() / OsRng (CSPRNG) |
| Key cleanup | zeroize crate (Rust) / fill(0) (JS) |
| TOTP generation | HMAC-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.