King Hippopotamus

Local-first password vault · terminal · v3.2.2

IronDome

Stores passwords in an encrypted file on your own machine, and fails the build if any module so much as imports a networking library.

pip install IronDome

3.2.2

on PyPI

157

tests, run before publish

0

network calls in the package

600,000

PBKDF2 iterations

GPL-3.0

source is the spec

What it is

IronDome is a terminal vault. Entries are encrypted locally with AES-128-CBC via Fernet, keyed by PBKDF2-HMAC-SHA256 at 600,000 iterations. There is no account, no sync and no telemetry — and a test suite walks every module and fails the build if one of them imports a networking library.

Specification

PropertyValue
Symmetric encryptionAES-128-CBC (Fernet, with HMAC-SHA256)
Key derivationPBKDF2-HMAC-SHA256 × 600,000 (OWASP 2023)
Password hashingPBKDF2-HMAC-SHA256 + unique 16-byte salt
RandomnessPython secrets — the OS CSPRNG
Initialisation vectorFresh 16 bytes per message
Session fileMode 0600, 30-minute timeout
Failed logins3 attempts, then a 15-minute device lockout
ClipboardAuto-clear after 30 seconds
InterfaceTextual TUI (15 screens) plus a CLI
PlatformsmacOS, Windows, Linux — Python 3.8–3.13
Dependenciescryptography, keyring, textual
Tests157, run before publish
LicenseGPL-3.0-only

Recorded session

Real recording — the published CLI driven in a throwaway home directory · 2026-08-23

Nothing below is written by hand. The file is the recording: every command was typed at a shell and every line under it is what the program printed back. It runs from an empty machine to a self-destructed vault.

$ irondome-cli status
Created data directory at: ~/.password_manager
Created secrets directory at: ~/.password_manager/secrets
=== IronDome Status ===
  Dome:     not configured
  Airspace: CLOSED

$ irondome-cli create bunker
=== IronDome — First Time Setup ===

No biometric hardware detected. Using password authentication.

=== Create Master Credentials ===
Create master username (min 4 characters): operator
Create master password (min 8 characters): 
Confirm master password: 
✅ Master account created successfully!

Configure your defaults:

=== IronDome Settings ===
  1. Password length:    20
  2. Special characters: yes
  3. Uppercase letters:  yes
  4. Include digits:     yes
  5. Session timeout:    30 min
  6. Show strength:      yes
  7. Reset to defaults
  8. Back

Select (1-8): 8

IronDome configured. Use 'irondome open airspace' to start.

$ irondome-cli status
=== IronDome Status ===
  Dome:     configured
  Airspace: CLOSED
  Biometric: None
  Timeout:  30 min

$ irondome-cli open airspace
Opening airspace...

=== IronDome Login ===
Enter username: operator
Enter password: 
✅ Login successful. Welcome back, operator!
Airspace open. You have 30 minutes of free access.
   Run 'bunker' commands freely.
   Run 'irondome close airspace' to lock down.

$ bunker create

Master account exists. Please login.

=== IronDome Login ===
Enter username: operator
Enter password: 
✅ Login successful. Welcome back, operator!

Generated: wi-^F240a3vgEKkT)CEO
Strength: Excellent

Username: ops@example.com
Service/site: example.com
Notes (optional): primary demo account
✅ Bunker stored for ops@example.com at example.com

$ bunker create

Master account exists. Please login.

=== IronDome Login ===
Enter username: operator
Enter password: 
✅ Login successful. Welcome back, operator!

Generated: V^B3xQqJ4JN4bm%P%@wp
Strength: Excellent

Username: k.hippo
Service/site: git.example.org
Notes (optional): signing passphrase
✅ Bunker stored for k.hippo at git.example.org

$ bunker create

Master account exists. Please login.

=== IronDome Login ===
Enter username: operator
Enter password: 
✅ Login successful. Welcome back, operator!

Generated: g^LhC(_acQNsk1VMe-tJ
Strength: Excellent

Username: archive-bot
Service/site: backup.example.net
Notes (optional): rotates quarterly
✅ Bunker stored for archive-bot at backup.example.net

$ bunker create

Master account exists. Please login.

=== IronDome Login ===
Enter username: operator
Enter password: 
✅ Login successful. Welcome back, operator!

Generated: A)I4=9D%UzE%+jRm3Ee*
Strength: Excellent

Username: root
Service/site: db.internal.example
Notes (optional): 
✅ Bunker stored for root at db.internal.example

$ bunker open

Master account exists. Please login.

=== IronDome Login ===
Enter username: operator
Enter password: 
✅ Login successful. Welcome back, operator!

=== Bunker Registry ===
  1. backup.example.net
  2. db.internal.example
  3. example.com
  4. git.example.org

  Total: 4 bunkers

$ bunker open example

Master account exists. Please login.

=== IronDome Login ===
Enter username: operator
Enter password: 
✅ Login successful. Welcome back, operator!

  Service:  example.com
  Username: ops@example.com
  Password: wi-^F240a3vgEKkT)CEO
  Notes:    primary demo account
  Created:  2026-08-23 15:56:21

$ bunker open git

Master account exists. Please login.

=== IronDome Login ===
Enter username: operator
Enter password: 
✅ Login successful. Welcome back, operator!

  Service:  git.example.org
  Username: k.hippo
  Password: V^B3xQqJ4JN4bm%P%@wp
  Notes:    signing passphrase
  Created:  2026-08-23 15:56:22

$ bunker open paypal

Master account exists. Please login.

=== IronDome Login ===
Enter username: operator
Enter password: 
✅ Login successful. Welcome back, operator!
No bunker found matching 'paypal'

$ bunker fortify

Master account exists. Please login.

=== IronDome Login ===
Enter username: operator
Enter password: 
✅ Login successful. Welcome back, operator!
✅ Dome fortified at: ~/.password_manager/backups/.passwords_backup_20260823-155627.enc
Dome fortified. Backup created.

$ irondome-cli status
=== IronDome Status ===
  Dome:     configured
  Airspace: OPEN - 29 min remaining
  Biometric: None
  Timeout:  30 min
  Vault:    932 bytes

$ irondome-cli close airspace
Airspace closed. All access locked.

$ irondome-cli open airspace
Opening airspace...

=== IronDome Login ===
Enter username: operator
Enter password: 
⚠️ Invalid password. 2 attempts remaining.
Enter username: operator
Enter password: 
⚠️ Invalid password. 1 attempt remaining.
Enter username: operator
Enter password: 
⚠️ Invalid password. No attempts remaining.
Your device has been locked out for 15 minutes.
Authentication failed. Airspace remains closed.

$ irondome-cli open airspace
Opening airspace...

⛔ Login blocked. Try again in 15.0 minutes.
Authentication failed. Airspace remains closed.

$ irondome-cli open airspace
Opening airspace...

⛔ Login blocked. Try again in 15.0 minutes.
Authentication failed. Airspace remains closed.

$ irondome-cli open airspace
Opening airspace...

⛔ Login blocked. Try again in 15.0 minutes.
Authentication failed. Airspace remains closed.

$ irondome nuke

  ██╗███╗   ██╗██╗   ██╗██╗  ██╗███████╗
  ██║████╗  ██║██║   ██║██║ ██╔╝██╔════╝
  ██║██╔██╗ ██║██║   ██║█████╔╝ █████╗  
  ██║██║╚██╗██║██║   ██║██╔═██╗ ██╔══╝  
  ██║██║ ╚████║╚██████╔╝██║  ██╗███████╗
  ╚═╝╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝╚══════╝

  WARNING: SELF-DESTRUCT PROTOCOL
  This will overwrite with random bytes, then delete:

    - ~/.password_manager/secrets/.login_attempts.dat
    - ~/.password_manager/secrets/.master_user.enc
    - ~/.password_manager/secrets/.passwords.enc
    - ~/.password_manager/secrets/salt.bin
    - ~/.password_manager/secrets/.master_hash.enc
    - ~/.password_manager/secrets
    - ~/.password_manager/backups/.passwords_backup_20260823-155627.enc
    - ~/.password_manager/backups
    - ~/.password_manager/password_manager.log
    - ~/.password_manager

  The OS keychain entries (master_key, auth_mode, recovery_hash)
  will also be purged.

  Erasure limit: each file is overwritten once in place, fsynced,
  then unlinked. On SSDs with wear-levelling, on copy-on-write
  filesystems (btrfs/ZFS) or on journaled ext4, the original blocks
  may survive. For guaranteed destruction, destroy the disk key.

  This action CANNOT be undone. All passwords will be lost forever.

  Type "DESTROY" to confirm, or anything else to abort: DESTROY

  Executing nuke sequence...
IronDome nuke initiated on data_dir=~/.password_manager
IronDome nuke complete. All user data destroyed.

  [OK] OS keychain entries purged.
  [OK] 7 file(s) securely deleted.

  IronDome has been destroyed. Run 'irondome create bunker' to start fresh.

$ find ~/.password_manager -type f
find: '~/.password_manager': No such file or directory

How it works

Every capability sits at one of four distances from a secret. Reading them from the outside in is also the order an attacker meets them.

At the perimeter

What can leave

The one claim on this page that is mechanically enforced.

Network

No network calls anywhere in the package

Not a promise not to sync — there is no code path that could. No sockets are opened and no ports are bound by anything you install.

$ grep -rnE "^\s*(import|from)\s+(socket|http|urllib|requests|ssl)" \
      password_manager/
(no matches)   # two comments mention the words; no module imports them
CI

Enforced by a test, not by a sentence

A suite walks the AST of every module and fails the build on a networking import, a resolver call, or an http:// literal. The allowlist is empty, and a separate test asserts it stays empty — so restoring network access means editing this claim in the same commit.

ALLOWLIST: frozenset = frozenset()

assert ALLOWLIST == frozenset()
Erase

Self-destruct that reports its own failure

nuke clears the keyring entries, overwrites each file once with random bytes, fsyncs, and unlinks. Then it re-walks the directory: if anything survived, it returns success: False and names every leftover.

fh.write(os.urandom(size))
fh.flush()
os.fsync(fh.fileno())

  [OK] 7 file(s) securely deleted.

At the door

Getting in

Three ways to authenticate, and one way to be locked out.

Modes

Three security modes

Biometric only, biometric plus password, or password only. The mode is recorded in the OS keyring and drives the login flow.

1. Touch ID Only
2. Touch ID + Master Password
3. Master Password Only
Gate

Biometric is an OS gate, not key material

Touch ID, Windows Hello or fprintd runs in front of a vault key that lives in the OS keyring. Your fingerprint authorises the unlock; a process already running as you can read that key without presenting one. Use mode 2 if that matters to you.

# biometric.py — the check is a boolean, not a KDF input
return result.returncode == 0
Lockout

Adaptive lockout, per device

Three wrong passwords and the device is locked out for fifteen minutes. The counter is stored per machine and survives the process exiting.

MAX_LOGIN_ATTEMPTS = 3
LOCKOUT_DURATION   = 15 * 60

In session

While the vault is open

The window where a secret is in the clear is the window that matters.

Clipboard

The clipboard clears itself

Copying a password schedules its own erasure thirty seconds later, on every platform. A new copy cancels and replaces the pending timer.

def copy_with_auto_clear(secret: str,
                         timeout: int = 30) -> bool:
TTY

Alternate screen buffer

The interface runs in the alternate buffer, so nothing it drew is in your scrollback after you quit. Nothing to page back through, nothing in a terminal log.

SMCUP on start / RMCUP on exit
Signals

Signals restore the terminal

SIGTERM, SIGINT and SIGHUP all run the same handler: put the terminal back, clear the clipboard, exit. A killed session does not leave a mess behind.

SIGTERM / SIGINT / SIGHUP
  -> restore tty, clear clipboard, exit

At rest

The vault on disk

What survives a reboot, and what it costs to read it.

Cipher

AES-128-CBC via Fernet

Every entry is encrypted with Fernet — AES-128 in CBC mode with an HMAC-SHA256 tag over the ciphertext. Tampering fails loudly instead of decrypting to garbage.

token = Fernet(key).encrypt(plaintext)
# 0x80 | 8B ts | 16B IV | ct | 32B HMAC
Entropy

Fresh salt, fresh IV

A 16-byte salt from the OS CSPRNG at setup, and a new 16-byte IV for every single message. Encrypting the same password twice never produces the same bytes.

salt = secrets.token_bytes(16)   # once, at setup
Storage

The master password is never written

Only a derived digest of it reaches the disk. There is no server to send it to and no file that holds it.

~/.password_manager/secrets/
  .passwords.enc    .master_hash.enc
  salt.bin          .master_user.enc

Limits

What this does not do, and where it should not be trusted. These stay on the page whatever else changes.

  • Key protection is software-only. The vault key is protected by your password and by the OS keyring. There is no TPM and no Secure Enclave, and machine-id scoping is obfuscation rather than a hardware boundary.

  • The biometric check is not cryptographic. It is an OS authentication gate in front of a keyring-stored key. A process already running as you can read that key without presenting a fingerprint.

  • nuke cannot defeat an SSD controller. One overwrite pass, fsync, unlink. Wear-levelling, copy-on-write filesystems and journaling can all leave the original blocks. For guaranteed destruction, use full-disk encryption and destroy the key.

  • The key is not zeroed in memory. It is held as immutable Python bytes inside a Fernet object and cannot be overwritten in place.

  • No independent third-party audit. 157 self-authored tests are not an audit. Nobody outside the project has reviewed the cryptography. Treat this as beta software.

Links

Defects in this package are issues in its own repository.

Author approval — 2026-08-23, hash f03bce7c