A local, offline, terminal-based password manager built around deterministic password generation — your passwords are never stored anywhere. They are derived on demand from your master password, the domain name, and a counter using a cryptographic key derivation function. The vault stores only metadata.
Most password managers encrypt and store your passwords. This one doesn't. Instead, it stores the parameters needed to re-derive each password and generates the actual value at the moment you ask for it.
master password + domain + counter + policy → HKDF / Argon2 → password
This means:
- A compromised vault file reveals no passwords — only usernames, policy names, and counters
- There is nothing to sync, backup, or leak in the traditional sense
- Rotating a password means incrementing a counter, not generating and storing a new random string
- Deterministic generation — same inputs always produce the same output; no random secrets stored
- AES-256-GCM encrypted vault — metadata is encrypted at rest with a key derived via Argon2id
- Scrollback clearing — terminal scrollback buffer is purged before and after revealing a secret so it cannot be read by scrolling up
- Master password verification — the master is verified against the vault ciphertext before any secret is derived; a wrong or blank entry is rejected (deterministic functions return output for any input, so naive implementations silently accept wrong passwords)
- Named policies — reusable password rules (length, character sets, forbidden characters, sequence limits) stored in
config.yaml - PIN support — separate secret type for numeric PINs with its own validation rules
- Per-entry overrides — tweak one policy field for a specific entry without changing the shared policy; the full merged policy is validated before saving
- Password rotation — increment the counter to move to a new password; previous counters are tracked up to a configurable history depth
- Search — filter entries by domain or username substring
- Clipboard support — copy username or password directly; falls back to plaintext display if
pyperclipis not installed - ESC to cancel — every prompt respects ESC as a cancellation signal; no dead ends
- Autosave — vault is re-encrypted and written to disk after every mutation
| Threat | Mitigation |
|---|---|
| Vault file stolen | AES-256-GCM encryption; file contains no passwords |
| Wrong master accepted | Master is verified via vault decrypt before use |
| Secret visible in terminal history | Scrollback buffer purged via \033[3J before and after reveal |
| Master password leaked from memory | Master string held in process memory only, never written to disk or derived key cache |
| Weak KDF | Argon2id with time_cost=3, memory_cost=64MB |
| Idle terminal exposes secrets | Secret shown on a separate screen; cleared on Enter |
Limitations to be aware of:
tmuxandscreenmaintain their own scrollback that ANSI sequences cannot clear — close and reopen the pane when using a multiplexer- Process memory is not locked (
mlock); a memory dump or swap file could expose the master string in theory - This is a local tool — there is no sync, no cloud, no mobile app
.
├── cli.py # Terminal UI, all user-facing flows
├── vault_models.py # Pydantic models: Vault, VaultEntry
├── vault_manager.py # Vault operations: CRUD, search, counter management
├── vault_crypto.py # Encryption layer: Argon2id KDF, AES-256-GCM, serialisation
├── policy.py # PolicySchema, config.yaml read/write
├── generator.py # Deterministic password derivation
├── config.yaml # Named policies (created on first policy save)
└── vault.vault # Encrypted vault (created on first run)
Requirements: Python 3.10+
git clone https://github.com/SiddhantGupta3112/deterministic-password-manager
cd deterministic-password-manager
python -m venv env
source env/bin/activate # Windows: env\Scripts\activate
pip install -r requirements.txtOptional clipboard support:
pip install pyperclip
# Linux may also need: sudo apt install xclippython cli.py uiOn first run the vault does not exist — you will be prompted to create a master password. This password cannot be recovered. The vault is created immediately and encrypted with a key derived from it.
1. Run the app
2. Create a master password (min 8 characters recommended)
3. Go to Manage Policies → Create new policy
4. Give the policy a name, choose Password or PIN, set your rules
5. Go to Add new entry, pick your policy, enter the domain
6. Select the entry → Reveal password to see your generated password
| Key | Action |
|---|---|
↑ / ↓ |
Move between options |
Enter |
Select |
ESC |
Cancel current flow and go back |
Policies are named rule sets stored in config.yaml. They define what a generated password looks like.
| Field | Description |
|---|---|
secret_type |
password or pin |
length |
Character count (4–128) |
uppercase |
Include A–Z |
lowercase |
Include a–z |
digits |
Include 0–9 |
special |
Include special characters |
special_chars |
Which special characters are allowed |
max_alpha_sequence |
Max consecutive letters before rejection |
max_digit_sequence |
Max consecutive digits before rejection |
forbidden_chars |
Characters never included |
history_depth |
How many previous counters to remember |
An override adjusts one policy field for a single entry without changing the shared policy. Useful when a site imposes constraints your default policy doesn't match — for example, a site that forbids % or caps length at 12.
Overrides are validated against the full merged policy before saving, so an invalid combination (e.g. all character sets disabled) is rejected at the UI level.
default_policy:
secret_type: password
length: 16
uppercase: true
lowercase: true
digits: true
special: true
special_chars: '!@#$%^&*'
max_alpha_sequence: null
max_digit_sequence: null
forbidden_chars: []
history_depth: 5
banking_policy:
secret_type: password
length: 12
uppercase: true
lowercase: true
digits: true
special: true
special_chars: '@#$*'
max_alpha_sequence: 3
max_digit_sequence: 3
forbidden_chars: [" ", "/", "\\", "'", '"', '`']
history_depth: 10
pin_policy:
secret_type: pin
length: 6
digits: true
uppercase: false
lowercase: false
special: false
special_chars: ''
max_digit_sequence: 2
forbidden_chars: []
history_depth: 5| Package | Purpose |
|---|---|
click |
CLI framework and terminal output |
questionary |
Interactive arrow-key menus and prompts |
pydantic |
Schema validation for vault models and policies |
cryptography |
AES-256-GCM encryption |
argon2-cffi |
Argon2id key derivation |
pyyaml |
Policy config serialisation |
pyperclip |
Clipboard support (optional) |
Argon2id(master_password, salt, time=3, memory=64MB) → 32-byte key
A fresh 16-byte salt is generated when the vault is created and stored in the vault file. The key is derived fresh on every unlock and never cached to disk.
{
"version": 1,
"salt": "<base64>",
"nonce": "<base64>",
"ciphertext": "<base64>"
}The ciphertext is the AES-256-GCM encryption of a JSON-serialised Vault object. The GCM authentication tag means any tampering with the ciphertext causes decryption to fail — the same mechanism used to reject a wrong master password.
generator.py takes the master, domain, counter, and resolved policy and produces a password deterministically. The same inputs on any machine always produce the same output.
-
mlockto prevent master password from being swapped to disk - Auto-lock after configurable idle timeout
- TOTP / 2FA code generation
- Export to encrypted portable format
- Delete / rename policies
- View password history (previous counters)