← Back to home

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.

PurposeAlgorithmSource
Symmetric encryptionAES-256-GCMApple CryptoKit
Key derivation (new vaults)Argon2id (v1.3)libsodium via swift-sodium
Key derivation (legacy vaults)PBKDF2-HMAC-SHA256CommonCrypto
Vault key when using Touch IDKeychain storage + LAContextSecurity + 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), opsLimit 3, memLimit 268 435 456 bytes (256 MiB), 32-byte output. Implemented in VaultCryptoService.
  • PBKDF2-HMAC-SHA256(legacy): 600 000 iterations, 16-byte salt, 32-byte output. Still used to read older metadata where kdfAlgorithm is absent or pbkdf2-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: KDF algorithm id, salt, iteration/ops limit, memory limit (Argon2id), nonce, ciphertext, and GCM tag — all base64 in JSON.
  • Vault envelope: format version, nonce, ciphertext, tag, and creation timestamp. Payload is a JSON snapshot of workspaces, items, and custom templates.
  • One file, replaced atomically. Both live in vault.package, written with a single atomic replace. They used to be two files — vault.meta and vault.enc — saved one after the other, so an interruption between the two writes (a crash or a flat battery, particularly while changing the master password) could leave the wrapped key and the data out of step and the vault unreadable. Both files are still kept up to date alongside the package so an older version of the app can still open the vault.
  • Sidebar ordering moved inside the vault. The order of your tags and environments used to sit in plain preferences, where the names of your tags were readable without unlocking anything. It is now part of the encrypted payload.

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).

Previous values

When you change a secret, PassStore keeps the value it replaced so you can look it up or put it back — up to 10 versions per field. Worth knowing what that means: a secret you rotated is still on disk until you clear it. Those versions live inside the same encrypted vault payload, under the same key, and are only decrypted for display when you explicitly ask to reveal one. You can clear them for a single item, clear them across the whole vault, or turn the feature off entirely in Settings → Data. Erasing the vault removes them with everything else.

Erasing the vault

A forgotten master password cannot be recovered — without it nothing, including PassStore, can decrypt your secrets. The only way forward is to erase and start again, which is offered from the lock screen and from Settings → Data. Where Touch ID is set up it is required first: anyone who can pass it could simply unlock the vault instead, so requiring it costs a legitimate owner nothing and stops someone walking past an unattended Mac from destroying the vault. Without biometrics the confirmation is typing ERASE, which is no weaker than deleting the vault file in Finder — already available to anyone sitting at your machine.

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.package and the vault.enc / vault.meta compatibility mirrors are written with atomic replace and mode 0600. Under the App Sandbox the same paths sit inside the app's container.

Memory and lock behavior

  • Argon2id and PBKDF2 password buffers and derived key material are zeroed in VaultCryptoService after 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.
  • System events lock too: the vault locks when the Mac sleeps, the screen locks, or the screensaver starts, rather than staying open until the idle timer happens to expire. Closing the lid is a lock.
  • Failed password attempts: after a wrong password, further attempts are delayed by 1s, then 2s, 5s, 10s, and 30s as the failure count increases. The delay is persisted, so quitting and reopening the app does not reset it (UI-oriented throttling, not a substitute for strong passwords).
  • Async work is cancelled on lock: locking, sleeping, or erasing invalidates any unlock, import, or export still in flight and clears decrypted previews and undo state, so a late result cannot repopulate a vault that is supposed to be closed.
  • Clipboard auto-clear:after copying from PassStore, a timer (default 10 seconds, configurable) clears the general pasteboard only if PassStore still owns it, checked through the pasteboard's changeCount — so anything you copied since is left alone. Earlier versions compared a SHA-256 fingerprint of the copied secret, which meant holding a hash of it in memory for the lifetime of the timer; the ownership counter needs no copy of the value at all.
  • 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.

System permissions

  • No Accessibility permission.The global command palette shortcut is a single chord registered with Carbon's RegisterEventHotKey. Earlier versions asked to be added to Privacy & Security → Accessibility, which grants an app the ability to observe every keystroke you type system-wide. That permission is no longer requested or used, and can be revoked.
  • App Sandbox. PassStore reaches files only through what you pick in an open or save panel, held as app-scoped security bookmarks. Linking an item to a .env grants access to that file and nothing around it.
  • Generated .env output is escaped. Values are quoted and shell expansion and command substitution are neutralised, and unsafe imported keys are repaired — so sourcing a file PassStore wrote cannot execute text smuggled into a field name, a value, or an item title.

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.

Updates are signed with an EdDSA key and Sparkle authenticates a downloaded archive before extracting anything from it. The app itself is signed with a Developer ID certificate, built with the hardened runtime, and notarized by Apple — you can check a downloaded copy yourself with spctl -a -vvv -t install PassStore.app, which should report source=Notarized Developer ID.

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 feedback@makio.app 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).