Security
This document describes how PassStore protects secrets on your Mac. It reflects the current implementation in the macOS app source code (encryption, Keychain, session behavior). It is an engineering overview, not a formal security audit or cryptographic proof.
PassStore is open source on GitHub (MIT). The repository contains the full Xcode project, unit tests, and files such as SECURITY.md for vulnerability reporting and an in-repo crypto summary.
Threat model (high level)
PassStore is aimed at keeping API keys, database credentials, and environment material off shared clipboards, chat logs, and remote backends you do not control. It does not protect you if your Mac is fully compromised by malware with sufficient privileges, or if someone has your unlocked session and vault password. It does follow Apple platform conventions: data at rest is encrypted, optional biometric unlock can gate a copy of the vault key in Keychain, and the design avoids sending vault contents over the network as part of normal use.
Local-first architecture
The vault lives on disk under your user account. There is no PassStore account or mandatory cloud sync for the product to function. Exporting or importing .pstore backups is an explicit action you take.
Cryptographic primitives
No custom ciphers are implemented. The app relies on well-reviewed platform and library APIs.
| Purpose | Algorithm | Source |
|---|---|---|
| Symmetric encryption | AES-256-GCM | Apple CryptoKit |
| Key derivation (new vaults) | Argon2id (v1.3) | libsodium via swift-sodium |
| Key derivation (legacy vaults) | PBKDF2-HMAC-SHA256 | CommonCrypto |
| Vault key when using Touch ID | Keychain storage + LAContext | Security + LocalAuthentication |
Key hierarchy
Your master password does not encrypt vault items directly. A random 256-bit vault key encrypts all vault data; the password only unwraps that key.
User password
│
▼
┌─────────────────────────────┐
│ Argon2id (new) or PBKDF2 │
│ (legacy), salt 16 bytes │
└──────────────┬──────────────┘
▼
Derived key (256-bit)
▼
┌──────────────┐
│ AES-256-GCM │── unwraps ──▶ Vault key (256-bit random)
└──────────────┘ │
▼
┌──────────────┐
│ AES-256-GCM │── encrypts ──▶ Vault JSON snapshot
└──────────────┘Key derivation parameters
- Argon2id (default for new vaults and after migration): salt 16 bytes (per vault),
opsLimit3,memLimit268 435 456 bytes (256 MiB), 32-byte output. Implemented inVaultCryptoService. - PBKDF2-HMAC-SHA256(legacy): 600 000 iterations, 16-byte salt, 32-byte output. Still used to read older metadata where
kdfAlgorithmis absent orpbkdf2-sha256. - Automatic migration: on the first successful password unlock, legacy KDF metadata is re-wrapped with Argon2id and saved; no separate user step.
Wrapped key and vault envelope (on disk)
- Wrapped vault key (in
vault.meta): KDF algorithm id, salt, iteration/ops limit, memory limit (Argon2id), nonce, ciphertext, and GCM tag — all base64 in JSON. - Vault envelope (in
vault.enc): format version, nonce, ciphertext, tag, and creation timestamp. Payload is a JSON snapshot of workspaces, items, and custom templates.
macOS Keychain and Touch ID
When biometric unlock is enabled and the device supports it, a copy of the vault key is stored as a generic password item with kSecAttrAccessibleWhenUnlockedThisDeviceOnly and .biometryCurrentSet. Reading that item triggers LocalAuthentication (e.g. Touch ID). If you turn biometrics off in settings, the Keychain copy is removed; password-only unlock still uses the wrapped key in metadata. The app does not store the vault key in Keychain without this biometric access control when the Keychain-backed store is used.
Encrypted backup (.pstore)
Full backups use the same pattern as the vault: a random 256-bit key encrypts the backup payload with AES-256-GCM; that key is wrapped with Argon2id + AES-GCM using your export password. Current export format version is 3 (see ExportService).
File locations and permissions
Default directory: ~/Library/Application Support/<bundle-id>/ (bundle id defaults to app.makio.PassStore). The directory is created with mode 0700;vault.enc and vault.meta are written with atomic replace and mode 0600.
Memory and lock behavior
- Argon2id and PBKDF2 password buffers and derived key material are zeroed in
VaultCryptoServiceafter use. - On lock, the in-memory vault key is cleared with
memset; sensitive field values in the memory store are overwritten before release. - The master password is a Swift
String; you cannot reliably zero its backing storage — a limitation shared with other Swift apps handling passwords.
Session, clipboard, and UI throttling
- Auto-lock: default inactivity timeout is 300 seconds (5 minutes), configurable. While unlocked, local keyboard, mouse button, and scroll events refresh the timer; a repeating timer checks roughly every 5 seconds whether the timeout elapsed.
- Failed password attempts: after a wrong password, further attempts are delayed by 1s, then 2s, 5s, 10s, and 30s as the failure count increases (UI-oriented throttling, not a substitute for strong passwords).
- Clipboard auto-clear: after copying from PassStore, a timer (default 10 seconds, configurable) clears the general pasteboard only if the current string still matches a SHA-256 fingerprint of what PassStore placed there — so content you replaced manually is not wiped.
- Universal Clipboard / Handoff hint: copied secrets also set an empty payload for type
org.nspasteboard.ConcealedType, a convention many macOS apps use so Handoff and some clipboard tools skip the item. Effectiveness depends on OS and third-party behavior.
Network and updates
Unlocking, editing, and saving the vault do not send your data over the network. Direct-download builds include Sparkle and may fetch an update appcast (configured in the app's Info.plist) to check for new versions; that is separate from vault operations and does not upload vault contents. App Store builds omit Sparkle at compile time.
What we intentionally avoid
- No mandatory cloud sync of vault data for core functionality.
- No shipping vault secrets to a vendor in the default workflow.
- No proprietary or home-grown ciphers for vault encryption.
Limitations
A stolen vault file still requires your master password (or biometric-gated keychain read on a machine where that applies). Weak passwords remain weak. Physical access to an unlocked session can read secrets the same as any local app. New vault passwords must be at least 8 characters — length alone is not enough; prefer a strong, unique passphrase.
Reporting vulnerabilities
Do not open a public issue for undisclosed security bugs. Email [email protected] with reproduction steps and impact. See the project SECURITY.md for supported versions and policy.
Implementation (verify in tree PassStore/): VaultPersistence.swift (crypto + files), VaultSecurity.swift (session, settings, clipboard), SecretStores.swift (Keychain), VaultTransfer.swift (export/import).