basic-password-manager 0.2.4__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. basic_password_manager-0.4.0/PKG-INFO +281 -0
  2. basic_password_manager-0.4.0/README.md +265 -0
  3. basic_password_manager-0.4.0/basic_password_manager.egg-info/PKG-INFO +281 -0
  4. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/SOURCES.txt +4 -1
  5. basic_password_manager-0.4.0/basic_password_manager.egg-info/entry_points.txt +2 -0
  6. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/requires.txt +1 -0
  7. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/pw_manager/cli.py +12 -1
  8. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/pw_manager/logo.py +23 -0
  9. basic_password_manager-0.4.0/pw_manager/mobile.py +248 -0
  10. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/pw_manager/tui.py +10 -0
  11. basic_password_manager-0.4.0/pw_manager/web/argon2.umd.min.js +7 -0
  12. basic_password_manager-0.4.0/pw_manager/web/page.html +151 -0
  13. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/pyproject.toml +6 -2
  14. basic_password_manager-0.2.4/PKG-INFO +0 -214
  15. basic_password_manager-0.2.4/README.md +0 -199
  16. basic_password_manager-0.2.4/basic_password_manager.egg-info/PKG-INFO +0 -214
  17. basic_password_manager-0.2.4/basic_password_manager.egg-info/entry_points.txt +0 -2
  18. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/LICENSE +0 -0
  19. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/dependency_links.txt +0 -0
  20. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/top_level.txt +0 -0
  21. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/pw_manager/__init__.py +0 -0
  22. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/pw_manager/crypto.py +0 -0
  23. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/pw_manager/vault.py +0 -0
  24. {basic_password_manager-0.2.4 → basic_password_manager-0.4.0}/setup.cfg +0 -0
@@ -0,0 +1,281 @@
1
+ Metadata-Version: 2.4
2
+ Name: basic-password-manager
3
+ Version: 0.4.0
4
+ Summary: The Paladin — a local encrypted password manager that lives in your terminal
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/Daisentaur/password-manager
7
+ Project-URL: Changelog, https://github.com/Daisentaur/password-manager/blob/main/CHANGELOG.md
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: argon2-cffi
12
+ Requires-Dist: cryptography
13
+ Requires-Dist: textual
14
+ Requires-Dist: qrcode
15
+ Dynamic: license-file
16
+
17
+ <p align="center">
18
+ <img src="assets/logo.png" width="160" alt="The Paladin — a pixel-art knight" />
19
+ </p>
20
+
21
+ <h1 align="center">The Paladin</h1>
22
+
23
+ <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, just protection.</em></p>
24
+
25
+ A password manager that is one encrypted file on my own disk and a small
26
+ terminal app in front of it. No browser, no cloud, no daemon, no ai running
27
+ in the background, no company holding my secrets. One file, one master password,
28
+ that's the whole thing.
29
+
30
+ (It installs as `basic-password-manager` because the name "The Paladin" came to me
31
+ quite a while after already starting the project. It answers to `paladin` as is its name)
32
+
33
+ ![unlocking the vault](assets/unlock.gif)
34
+
35
+ ## Install
36
+
37
+ From PyPI ([pipx](https://pipx.pypa.io) keeps CLI tools out of each other's business):
38
+
39
+ ```bash
40
+ pipx install basic-password-manager # or: pip install basic-password-manager
41
+ paladin init
42
+ ```
43
+
44
+ From source:
45
+
46
+ ```bash
47
+ git clone https://github.com/Daisentaur/password-manager && cd password-manager
48
+ pipx install .
49
+ ```
50
+
51
+ Hacking on it: `python3 -m venv venv && ./venv/bin/pip install -e .`
52
+
53
+ ## The TUI
54
+
55
+ Run `paladin` with nothing after it and you get the full app: unlock, arrow
56
+ around, Enter copies the selected password to your clipboard. `n` adds,
57
+ `e` edits, `d` deletes (it asks first), `q` quits. Every key is listed in
58
+ the bottom bar so there's nothing to memorize.
59
+
60
+ Press `/` and type whatever fragments you remember of whatever it is you're
61
+ looking for it just has to appear *somewhere* in the entry, any field, any
62
+ order. It even searches inside the passwords themselves, for the day all
63
+ you remember is what you typed on that site, so if it's in there the search
64
+ will for sure find it. Matches get highlighted in the theme's accent, except
65
+ password matches, which quietly show the row and highlight nothing — passwords
66
+ never get displayed, that's the deal).
67
+
68
+ ![searching the vault](assets/search.gif)
69
+
70
+ The rarer stuff lives in the command palette — `super+p`, commands you will need
71
+ sometimes but deffintely shouldn't crowd the main password list for example,
72
+ importing your browser's passwords, opening your vault on your phone, and
73
+ changing the master password all live there, along with others.
74
+
75
+ ![the command palette](assets/palette.gif)
76
+
77
+ And themes. `t` opens the theme picker; the bundled ones — muted-slate (default),
78
+ dawn, matrix — are lifted with love from
79
+ [tuxedo](https://github.com/webstonehq/tuxedo)'s palettes, and all of
80
+ Textual's built-ins are in there too. Your pick sticks across runs. Even the
81
+ knight on the unlock screen dresses to match your theme, try it ;)
82
+
83
+ ![switching themes](assets/themes.gif)
84
+
85
+ ## The CLI
86
+
87
+ Same vault, no interface, for when you just want the thing:
88
+
89
+ | Command | What it does |
90
+ |---|---|
91
+ | `paladin init` | create a new empty vault |
92
+ | `paladin add example` | store an entry (prompts for username/password/notes) |
93
+ | `paladin add example --gen` | same, but it invents a strong random password for you |
94
+ | `paladin get example` | copy the password to the clipboard, show the username |
95
+ | `paladin edit example` | update an entry field by field (Enter keeps what's there) |
96
+ | `paladin passwd` | change the master password |
97
+ | `paladin ls` | list entry names |
98
+ | `paladin rm example` | delete an entry |
99
+ | `paladin find "dt bank"` | search names, usernames, notes — every word must match, any order |
100
+ | `paladin gen -l 32` | just print a random password |
101
+ | `paladin import passwords.csv` | import a browser CSV export (see below) |
102
+ | `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
103
+ | `paladin about` | meet the knight |
104
+
105
+ The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
106
+ environment variable somewhere else if you disagree.
107
+
108
+ The `notes` field takes anything you want kept secret next to the password —
109
+ recovery codes, PINs, the answer to "what was your first pet".
110
+
111
+ ### Leaving your browser's password manager
112
+
113
+ This is why the project exists, so here's the exit route:
114
+
115
+ 1. Export:
116
+ Chrome → `chrome://password-manager/settings` → "Export passwords".
117
+ Firefox → `about:logins` → ⋯ menu → "Export passwords".
118
+ Either way you get a CSV.
119
+ 3. `paladin import passwords.csv`
120
+ 4. **Delete that CSV immediately** — it's every password you own, in
121
+ plaintext: `shred -u passwords.csv`
122
+ 5. Turn off password saving in the browser and delete what it stored.
123
+
124
+ ## Your phone
125
+
126
+ `paladin mobile` (or "Open on phone" in the palette) puts your vault on your
127
+ phone with one QR scan. It spins up a throwaway HTTPS tunnel
128
+ ([cloudflared](https://github.com/cloudflare/cloudflared), fetched
129
+ automatically the first time), serves a single page with the *encrypted*
130
+ vault baked in, and prints the URL as a QR in your terminal. Scan it, type
131
+ your master password **on the phone**, and the decryption happens in the
132
+ phone's browser — the laptop, the tunnel, and the network only ever carry
133
+ ciphertext. Nothing is stored on the phone; the page auto-locks after 5
134
+ minutes idle, and the whole session dies when you press Ctrl+C.
135
+
136
+ It works on any modern phone browser (Safari, Chrome, Firefox…) — no app to
137
+ install. The page opens in whatever TUI theme you had set when you generated
138
+ the QR, knight and all (it won't follow later theme changes — scan again for
139
+ that).
140
+
141
+ Have your own domain? `paladin mobile --url https://vault.you.dev` serves at
142
+ your subdomain instead of a random tunnel, so a bookmark keeps working (the
143
+ path rotates every 30 days on its own). It serves on `127.0.0.1:8787` by
144
+ default — point your reverse proxy or named tunnel there. Set
145
+ `PALADIN_MOBILE_URL` (and optionally `PALADIN_MOBILE_PORT`) and plain
146
+ `paladin mobile` uses it.
147
+
148
+ The one thing crossing the internet is the encrypted blob, which is useless
149
+ without the master password that never leaves your phone. The tunnel provider
150
+ sees the same noise anyone else would.
151
+
152
+ ## How it works
153
+
154
+ Three layers, pretty small. This project exists because I wanted to
155
+ understand this stuff, I'd be happy if someone learns from this, from me:
156
+
157
+ ### 1. `crypto.py` — password → key, key → ciphertext
158
+
159
+ An AES key must be 32 unpredictable bytes; your master password is neither.
160
+ **Argon2id** bridges the gap: `derive_key(password, salt)` always returns the
161
+ same 32 bytes for the same inputs — that's what makes unlocking possible at
162
+ all. It's deliberately slow and memory-hungry (64 MiB *per guess*), so
163
+ someone who steals the vault file can't brute-force short passwords on a GPU
164
+ the way they could against a plain hash. The pause when you unlock? That's
165
+ the lock being hard to pick. I've learned to love it.
166
+
167
+ The **salt** is 16 random bytes stored *unencrypted* in the vault file, and
168
+ that's fine — it's not a secret. Its whole job is making your derived key
169
+ unique so precomputed attack tables are useless. (This took me a while to
170
+ truly believe. It's fine. Smarter people than me checked.)
171
+
172
+ Encryption is **AES-256-GCM**, which is *authenticated*: decrypt with the
173
+ wrong key, or decrypt a file where even one bit was flipped, and it fails
174
+ loudly instead of handing back plausible garbage. Every encryption uses a
175
+ fresh random 12-byte **nonce** (also stored unencrypted, also not a secret).
176
+ The one iron rule of GCM: a (key, nonce) pair must never repeat — which is
177
+ why it's random every single time.
178
+
179
+ ### 2. `vault.py` — the file format
180
+
181
+ ```
182
+ [4 bytes "PWV1"][16-byte salt][12-byte nonce][ciphertext...]
183
+ ```
184
+
185
+ The ciphertext is just your entries as JSON, encrypted. Load = read → derive
186
+ key → decrypt → `json.loads`. Save = the same backwards, with a fresh salt
187
+ and nonce, written to a temp file and atomically renamed so a crash
188
+ mid-write can't destroy the vault.
189
+
190
+ Your master password is stored nowhere. Not hashed, not hidden — nowhere.
191
+ "Wrong password" is just the decryption screaming, translated.
192
+
193
+ ### 3. `cli.py` and `tui.py` — the interfaces
194
+
195
+ Both are thin skins over the two modules above. The TUI (built with
196
+ [Textual](https://textual.textualize.io)) holds the decrypted entries in
197
+ memory for the session; the CLI re-derives the key per command.
198
+
199
+ Passwords go to the clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists)
200
+ rather than the screen, so they don't sit in your terminal scrollback. One
201
+ deliberate asymmetry: CLI `find` does *not* search passwords, because a CLI
202
+ argument lands in your shell history forever — the TUI search box does,
203
+ because nothing you type there is logged anywhere.
204
+
205
+ ## Backups
206
+
207
+ Every save keeps the previous version as `vault.bak` next to the vault —
208
+ one-step undo for when a save goes wrong or you delete the wrong entry.
209
+
210
+ That covers mistakes, not the disk dying. For that: the vault is one file,
211
+ so copy it anywhere. Another disk, a USB stick, even somewhere you don't
212
+ trust — it's gibberish without the master password.
213
+
214
+ ```bash
215
+ cp ~/.local/share/pw-manager/vault /some/backup/location/
216
+ ```
217
+
218
+ ### Moving to a new machine (or another OS)
219
+
220
+ The vault file plus the master password in your head is the *complete*
221
+ system — the salt lives inside the file, nothing belongs to the machine.
222
+ Install the package on the new system, drop your vault at
223
+ `~/.local/share/pw-manager/vault` (don't run `init` — that's for brand-new
224
+ vaults only), and everything works. Dual-booting? Park the vault on a
225
+ partition both systems mount and point both at it:
226
+ `export PW_VAULT=/mnt/shared/vault`.
227
+
228
+ ## Troubleshooting
229
+
230
+ **"I forgot the master password."** The data is gone. Mathematically gone.
231
+ That's by design: every "forgot password?" flow is a back door, and back doors
232
+ don't check IDs. Pick a long phrase you can't forget (a sentence beats
233
+ `Tr0ub4dor&3`), and know that backups protect you from losing the *file*,
234
+ never from forgetting the *word*.
235
+
236
+ **"wrong master password (or the vault file is corrupted)"** — 99% of the
237
+ time you typo'd it. If you're *certain* it's right, the file got damaged;
238
+ restore the automatic backup
239
+ (`cp ~/.local/share/pw-manager/vault.bak ~/.local/share/pw-manager/vault`)
240
+ or an off-machine copy.
241
+
242
+ **Deleted or overwrote an entry by mistake** — the state before your last
243
+ save is sitting in `vault.bak`; restore it as above. One generation only,
244
+ so do it before the next save.
245
+
246
+ **"not a vault file (bad magic bytes)"** — whatever's at the vault path
247
+ isn't ours (truncated, overwritten, or wrong path). Check `echo $PW_VAULT`
248
+ and restore from a backup.
249
+
250
+ **"no vault at ..."** — run `paladin init`, or `PW_VAULT` points somewhere
251
+ unexpected.
252
+
253
+ **Password prints instead of copying** — no clipboard tool found. Install
254
+ one: `sudo apt install wl-clipboard` (Wayland) or `xclip` (X11).
255
+
256
+ **Unlock feels slow (~1s)** — that's Argon2 doing its job. The delay *is*
257
+ the brute-force resistance. Per command, not per keystroke.
258
+
259
+ **`MemoryError` from Argon2** — the key derivation wants 64 MiB free;
260
+ something is eating your RAM.
261
+
262
+ **Import brought in junk** — browser CSVs export everything, including
263
+ accounts from 2013. `paladin rm <name>` the corpses, or prune the CSV first.
264
+
265
+ ## Honest limitations
266
+
267
+ I'd rather you know these than find them:
268
+
269
+ - No clipboard auto-clear yet: a copied password stays on the clipboard
270
+ until you copy over it. Copy something else when you're done.
271
+ - While a command runs, decrypted data briefly exists in process memory.
272
+ Malware already on your machine could read it — true of every password
273
+ manager ever made; the vault protects the file at rest, not a compromised
274
+ machine.
275
+ - Single file, no sync. Syncing is your problem for now.
276
+
277
+ ## Running the checks
278
+
279
+ ```bash
280
+ ./venv/bin/python test_vault.py
281
+ ```
@@ -0,0 +1,265 @@
1
+ <p align="center">
2
+ <img src="assets/logo.png" width="160" alt="The Paladin — a pixel-art knight" />
3
+ </p>
4
+
5
+ <h1 align="center">The Paladin</h1>
6
+
7
+ <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, just protection.</em></p>
8
+
9
+ A password manager that is one encrypted file on my own disk and a small
10
+ terminal app in front of it. No browser, no cloud, no daemon, no ai running
11
+ in the background, no company holding my secrets. One file, one master password,
12
+ that's the whole thing.
13
+
14
+ (It installs as `basic-password-manager` because the name "The Paladin" came to me
15
+ quite a while after already starting the project. It answers to `paladin` as is its name)
16
+
17
+ ![unlocking the vault](assets/unlock.gif)
18
+
19
+ ## Install
20
+
21
+ From PyPI ([pipx](https://pipx.pypa.io) keeps CLI tools out of each other's business):
22
+
23
+ ```bash
24
+ pipx install basic-password-manager # or: pip install basic-password-manager
25
+ paladin init
26
+ ```
27
+
28
+ From source:
29
+
30
+ ```bash
31
+ git clone https://github.com/Daisentaur/password-manager && cd password-manager
32
+ pipx install .
33
+ ```
34
+
35
+ Hacking on it: `python3 -m venv venv && ./venv/bin/pip install -e .`
36
+
37
+ ## The TUI
38
+
39
+ Run `paladin` with nothing after it and you get the full app: unlock, arrow
40
+ around, Enter copies the selected password to your clipboard. `n` adds,
41
+ `e` edits, `d` deletes (it asks first), `q` quits. Every key is listed in
42
+ the bottom bar so there's nothing to memorize.
43
+
44
+ Press `/` and type whatever fragments you remember of whatever it is you're
45
+ looking for it just has to appear *somewhere* in the entry, any field, any
46
+ order. It even searches inside the passwords themselves, for the day all
47
+ you remember is what you typed on that site, so if it's in there the search
48
+ will for sure find it. Matches get highlighted in the theme's accent, except
49
+ password matches, which quietly show the row and highlight nothing — passwords
50
+ never get displayed, that's the deal).
51
+
52
+ ![searching the vault](assets/search.gif)
53
+
54
+ The rarer stuff lives in the command palette — `super+p`, commands you will need
55
+ sometimes but deffintely shouldn't crowd the main password list for example,
56
+ importing your browser's passwords, opening your vault on your phone, and
57
+ changing the master password all live there, along with others.
58
+
59
+ ![the command palette](assets/palette.gif)
60
+
61
+ And themes. `t` opens the theme picker; the bundled ones — muted-slate (default),
62
+ dawn, matrix — are lifted with love from
63
+ [tuxedo](https://github.com/webstonehq/tuxedo)'s palettes, and all of
64
+ Textual's built-ins are in there too. Your pick sticks across runs. Even the
65
+ knight on the unlock screen dresses to match your theme, try it ;)
66
+
67
+ ![switching themes](assets/themes.gif)
68
+
69
+ ## The CLI
70
+
71
+ Same vault, no interface, for when you just want the thing:
72
+
73
+ | Command | What it does |
74
+ |---|---|
75
+ | `paladin init` | create a new empty vault |
76
+ | `paladin add example` | store an entry (prompts for username/password/notes) |
77
+ | `paladin add example --gen` | same, but it invents a strong random password for you |
78
+ | `paladin get example` | copy the password to the clipboard, show the username |
79
+ | `paladin edit example` | update an entry field by field (Enter keeps what's there) |
80
+ | `paladin passwd` | change the master password |
81
+ | `paladin ls` | list entry names |
82
+ | `paladin rm example` | delete an entry |
83
+ | `paladin find "dt bank"` | search names, usernames, notes — every word must match, any order |
84
+ | `paladin gen -l 32` | just print a random password |
85
+ | `paladin import passwords.csv` | import a browser CSV export (see below) |
86
+ | `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
87
+ | `paladin about` | meet the knight |
88
+
89
+ The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
90
+ environment variable somewhere else if you disagree.
91
+
92
+ The `notes` field takes anything you want kept secret next to the password —
93
+ recovery codes, PINs, the answer to "what was your first pet".
94
+
95
+ ### Leaving your browser's password manager
96
+
97
+ This is why the project exists, so here's the exit route:
98
+
99
+ 1. Export:
100
+ Chrome → `chrome://password-manager/settings` → "Export passwords".
101
+ Firefox → `about:logins` → ⋯ menu → "Export passwords".
102
+ Either way you get a CSV.
103
+ 3. `paladin import passwords.csv`
104
+ 4. **Delete that CSV immediately** — it's every password you own, in
105
+ plaintext: `shred -u passwords.csv`
106
+ 5. Turn off password saving in the browser and delete what it stored.
107
+
108
+ ## Your phone
109
+
110
+ `paladin mobile` (or "Open on phone" in the palette) puts your vault on your
111
+ phone with one QR scan. It spins up a throwaway HTTPS tunnel
112
+ ([cloudflared](https://github.com/cloudflare/cloudflared), fetched
113
+ automatically the first time), serves a single page with the *encrypted*
114
+ vault baked in, and prints the URL as a QR in your terminal. Scan it, type
115
+ your master password **on the phone**, and the decryption happens in the
116
+ phone's browser — the laptop, the tunnel, and the network only ever carry
117
+ ciphertext. Nothing is stored on the phone; the page auto-locks after 5
118
+ minutes idle, and the whole session dies when you press Ctrl+C.
119
+
120
+ It works on any modern phone browser (Safari, Chrome, Firefox…) — no app to
121
+ install. The page opens in whatever TUI theme you had set when you generated
122
+ the QR, knight and all (it won't follow later theme changes — scan again for
123
+ that).
124
+
125
+ Have your own domain? `paladin mobile --url https://vault.you.dev` serves at
126
+ your subdomain instead of a random tunnel, so a bookmark keeps working (the
127
+ path rotates every 30 days on its own). It serves on `127.0.0.1:8787` by
128
+ default — point your reverse proxy or named tunnel there. Set
129
+ `PALADIN_MOBILE_URL` (and optionally `PALADIN_MOBILE_PORT`) and plain
130
+ `paladin mobile` uses it.
131
+
132
+ The one thing crossing the internet is the encrypted blob, which is useless
133
+ without the master password that never leaves your phone. The tunnel provider
134
+ sees the same noise anyone else would.
135
+
136
+ ## How it works
137
+
138
+ Three layers, pretty small. This project exists because I wanted to
139
+ understand this stuff, I'd be happy if someone learns from this, from me:
140
+
141
+ ### 1. `crypto.py` — password → key, key → ciphertext
142
+
143
+ An AES key must be 32 unpredictable bytes; your master password is neither.
144
+ **Argon2id** bridges the gap: `derive_key(password, salt)` always returns the
145
+ same 32 bytes for the same inputs — that's what makes unlocking possible at
146
+ all. It's deliberately slow and memory-hungry (64 MiB *per guess*), so
147
+ someone who steals the vault file can't brute-force short passwords on a GPU
148
+ the way they could against a plain hash. The pause when you unlock? That's
149
+ the lock being hard to pick. I've learned to love it.
150
+
151
+ The **salt** is 16 random bytes stored *unencrypted* in the vault file, and
152
+ that's fine — it's not a secret. Its whole job is making your derived key
153
+ unique so precomputed attack tables are useless. (This took me a while to
154
+ truly believe. It's fine. Smarter people than me checked.)
155
+
156
+ Encryption is **AES-256-GCM**, which is *authenticated*: decrypt with the
157
+ wrong key, or decrypt a file where even one bit was flipped, and it fails
158
+ loudly instead of handing back plausible garbage. Every encryption uses a
159
+ fresh random 12-byte **nonce** (also stored unencrypted, also not a secret).
160
+ The one iron rule of GCM: a (key, nonce) pair must never repeat — which is
161
+ why it's random every single time.
162
+
163
+ ### 2. `vault.py` — the file format
164
+
165
+ ```
166
+ [4 bytes "PWV1"][16-byte salt][12-byte nonce][ciphertext...]
167
+ ```
168
+
169
+ The ciphertext is just your entries as JSON, encrypted. Load = read → derive
170
+ key → decrypt → `json.loads`. Save = the same backwards, with a fresh salt
171
+ and nonce, written to a temp file and atomically renamed so a crash
172
+ mid-write can't destroy the vault.
173
+
174
+ Your master password is stored nowhere. Not hashed, not hidden — nowhere.
175
+ "Wrong password" is just the decryption screaming, translated.
176
+
177
+ ### 3. `cli.py` and `tui.py` — the interfaces
178
+
179
+ Both are thin skins over the two modules above. The TUI (built with
180
+ [Textual](https://textual.textualize.io)) holds the decrypted entries in
181
+ memory for the session; the CLI re-derives the key per command.
182
+
183
+ Passwords go to the clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists)
184
+ rather than the screen, so they don't sit in your terminal scrollback. One
185
+ deliberate asymmetry: CLI `find` does *not* search passwords, because a CLI
186
+ argument lands in your shell history forever — the TUI search box does,
187
+ because nothing you type there is logged anywhere.
188
+
189
+ ## Backups
190
+
191
+ Every save keeps the previous version as `vault.bak` next to the vault —
192
+ one-step undo for when a save goes wrong or you delete the wrong entry.
193
+
194
+ That covers mistakes, not the disk dying. For that: the vault is one file,
195
+ so copy it anywhere. Another disk, a USB stick, even somewhere you don't
196
+ trust — it's gibberish without the master password.
197
+
198
+ ```bash
199
+ cp ~/.local/share/pw-manager/vault /some/backup/location/
200
+ ```
201
+
202
+ ### Moving to a new machine (or another OS)
203
+
204
+ The vault file plus the master password in your head is the *complete*
205
+ system — the salt lives inside the file, nothing belongs to the machine.
206
+ Install the package on the new system, drop your vault at
207
+ `~/.local/share/pw-manager/vault` (don't run `init` — that's for brand-new
208
+ vaults only), and everything works. Dual-booting? Park the vault on a
209
+ partition both systems mount and point both at it:
210
+ `export PW_VAULT=/mnt/shared/vault`.
211
+
212
+ ## Troubleshooting
213
+
214
+ **"I forgot the master password."** The data is gone. Mathematically gone.
215
+ That's by design: every "forgot password?" flow is a back door, and back doors
216
+ don't check IDs. Pick a long phrase you can't forget (a sentence beats
217
+ `Tr0ub4dor&3`), and know that backups protect you from losing the *file*,
218
+ never from forgetting the *word*.
219
+
220
+ **"wrong master password (or the vault file is corrupted)"** — 99% of the
221
+ time you typo'd it. If you're *certain* it's right, the file got damaged;
222
+ restore the automatic backup
223
+ (`cp ~/.local/share/pw-manager/vault.bak ~/.local/share/pw-manager/vault`)
224
+ or an off-machine copy.
225
+
226
+ **Deleted or overwrote an entry by mistake** — the state before your last
227
+ save is sitting in `vault.bak`; restore it as above. One generation only,
228
+ so do it before the next save.
229
+
230
+ **"not a vault file (bad magic bytes)"** — whatever's at the vault path
231
+ isn't ours (truncated, overwritten, or wrong path). Check `echo $PW_VAULT`
232
+ and restore from a backup.
233
+
234
+ **"no vault at ..."** — run `paladin init`, or `PW_VAULT` points somewhere
235
+ unexpected.
236
+
237
+ **Password prints instead of copying** — no clipboard tool found. Install
238
+ one: `sudo apt install wl-clipboard` (Wayland) or `xclip` (X11).
239
+
240
+ **Unlock feels slow (~1s)** — that's Argon2 doing its job. The delay *is*
241
+ the brute-force resistance. Per command, not per keystroke.
242
+
243
+ **`MemoryError` from Argon2** — the key derivation wants 64 MiB free;
244
+ something is eating your RAM.
245
+
246
+ **Import brought in junk** — browser CSVs export everything, including
247
+ accounts from 2013. `paladin rm <name>` the corpses, or prune the CSV first.
248
+
249
+ ## Honest limitations
250
+
251
+ I'd rather you know these than find them:
252
+
253
+ - No clipboard auto-clear yet: a copied password stays on the clipboard
254
+ until you copy over it. Copy something else when you're done.
255
+ - While a command runs, decrypted data briefly exists in process memory.
256
+ Malware already on your machine could read it — true of every password
257
+ manager ever made; the vault protects the file at rest, not a compromised
258
+ machine.
259
+ - Single file, no sync. Syncing is your problem for now.
260
+
261
+ ## Running the checks
262
+
263
+ ```bash
264
+ ./venv/bin/python test_vault.py
265
+ ```