3.2.2
on PyPI
Local-first password vault · terminal · v3.2.2
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
| Property | Value |
|---|---|
| Symmetric encryption | AES-128-CBC (Fernet, with HMAC-SHA256) |
| Key derivation | PBKDF2-HMAC-SHA256 × 600,000 (OWASP 2023) |
| Password hashing | PBKDF2-HMAC-SHA256 + unique 16-byte salt |
| Randomness | Python secrets — the OS CSPRNG |
| Initialisation vector | Fresh 16 bytes per message |
| Session file | Mode 0600, 30-minute timeout |
| Failed logins | 3 attempts, then a 15-minute device lockout |
| Clipboard | Auto-clear after 30 seconds |
| Interface | Textual TUI (15 screens) plus a CLI |
| Platforms | macOS, Windows, Linux — Python 3.8–3.13 |
| Dependencies | cryptography, keyring, textual |
| Tests | 157, run before publish |
| License | GPL-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.
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 themEnforced 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()
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.
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
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
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.
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: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 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.
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
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
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