basic-password-manager 0.3.0__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 (20) hide show
  1. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/PKG-INFO +63 -33
  2. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/README.md +61 -32
  3. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/PKG-INFO +63 -33
  4. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/SOURCES.txt +4 -1
  5. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/requires.txt +1 -0
  6. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/cli.py +11 -0
  7. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/logo.py +23 -0
  8. basic_password_manager-0.4.0/pw_manager/mobile.py +248 -0
  9. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/tui.py +10 -0
  10. basic_password_manager-0.4.0/pw_manager/web/argon2.umd.min.js +7 -0
  11. basic_password_manager-0.4.0/pw_manager/web/page.html +151 -0
  12. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pyproject.toml +5 -1
  13. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/LICENSE +0 -0
  14. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/dependency_links.txt +0 -0
  15. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/entry_points.txt +0 -0
  16. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/top_level.txt +0 -0
  17. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/__init__.py +0 -0
  18. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/crypto.py +0 -0
  19. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/vault.py +0 -0
  20. {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-password-manager
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: The Paladin — a local encrypted password manager that lives in your terminal
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/Daisentaur/password-manager
@@ -11,6 +11,7 @@ License-File: LICENSE
11
11
  Requires-Dist: argon2-cffi
12
12
  Requires-Dist: cryptography
13
13
  Requires-Dist: textual
14
+ Requires-Dist: qrcode
14
15
  Dynamic: license-file
15
16
 
16
17
  <p align="center">
@@ -19,15 +20,15 @@ Dynamic: license-file
19
20
 
20
21
  <h1 align="center">The Paladin</h1>
21
22
 
22
- <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, no mercy.</em></p>
23
+ <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, just protection.</em></p>
23
24
 
24
25
  A password manager that is one encrypted file on my own disk and a small
25
26
  terminal app in front of it. No browser, no cloud, no daemon, no ai running
26
27
  in the background, no company holding my secrets. One file, one master password,
27
28
  that's the whole thing.
28
29
 
29
- (It installs as `basic-password-manager` because the name came after the project.
30
- It answers to `paladin` as is its name)
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)
31
32
 
32
33
  ![unlocking the vault](assets/unlock.gif)
33
34
 
@@ -56,26 +57,28 @@ around, Enter copies the selected password to your clipboard. `n` adds,
56
57
  `e` edits, `d` deletes (it asks first), `q` quits. Every key is listed in
57
58
  the bottom bar so there's nothing to memorize.
58
59
 
59
- The search forgives you. Press `/` and type whatever fragments you remember
60
- — every word just has to appear *somewhere* in the entry, any field, any
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
61
62
  order. It even searches inside the passwords themselves, for the day all
62
- you remember is what you typed on that site (matches get highlighted in the
63
- theme's accent, except password matches, which quietly show the row and
64
- highlight nothing — passwords never get displayed, that's the deal).
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).
65
67
 
66
68
  ![searching the vault](assets/search.gif)
67
69
 
68
- The rarer stuff lives in the command palette — `ctrl+p`, type a few letters,
69
- done. Importing your browser's passwords and changing the master password
70
- both live there.
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.
71
74
 
72
75
  ![the command palette](assets/palette.gif)
73
76
 
74
- And themes. `t` opens the picker; the bundled ones — muted-slate (default),
77
+ And themes. `t` opens the theme picker; the bundled ones — muted-slate (default),
75
78
  dawn, matrix — are lifted with love from
76
79
  [tuxedo](https://github.com/webstonehq/tuxedo)'s palettes, and all of
77
80
  Textual's built-ins are in there too. Your pick sticks across runs. Even the
78
- knight on the unlock screen dresses to match.
81
+ knight on the unlock screen dresses to match your theme, try it ;)
79
82
 
80
83
  ![switching themes](assets/themes.gif)
81
84
 
@@ -86,16 +89,17 @@ Same vault, no interface, for when you just want the thing:
86
89
  | Command | What it does |
87
90
  |---|---|
88
91
  | `paladin init` | create a new empty vault |
89
- | `paladin add github` | store an entry (prompts for username/password/notes) |
90
- | `paladin add github --gen` | same, but it invents a strong random password for you |
91
- | `paladin get github` | copy the password to the clipboard, show the username |
92
- | `paladin edit github` | update an entry field by field (Enter keeps what's there) |
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) |
93
96
  | `paladin passwd` | change the master password |
94
97
  | `paladin ls` | list entry names |
95
- | `paladin rm github` | delete an entry |
98
+ | `paladin rm example` | delete an entry |
96
99
  | `paladin find "dt bank"` | search names, usernames, notes — every word must match, any order |
97
100
  | `paladin gen -l 32` | just print a random password |
98
101
  | `paladin import passwords.csv` | import a browser CSV export (see below) |
102
+ | `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
99
103
  | `paladin about` | meet the knight |
100
104
 
101
105
  The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
@@ -108,19 +112,47 @@ recovery codes, PINs, the answer to "what was your first pet".
108
112
 
109
113
  This is why the project exists, so here's the exit route:
110
114
 
111
- 1. Export: Chrome → `chrome://password-manager/settings` → "Export passwords".
112
- Firefox → `about:logins` → ⋯ menu → "Export passwords". Either way you get
113
- a CSV.
114
- 2. `paladin import passwords.csv`
115
- 3. **Delete that CSV immediately** — it's every password you own, in
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
116
121
  plaintext: `shred -u passwords.csv`
117
- 4. Turn off password saving in the browser and delete what it stored.
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.
118
151
 
119
152
  ## How it works
120
153
 
121
- Three layers, ~120 lines total, and I mean it about the reading order — this
122
- project exists because I wanted to understand this stuff, and the code is
123
- meant to be read:
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:
124
156
 
125
157
  ### 1. `crypto.py` — password → key, key → ciphertext
126
158
 
@@ -195,9 +227,8 @@ partition both systems mount and point both at it:
195
227
 
196
228
  ## Troubleshooting
197
229
 
198
- **"I forgot the master password."** The data is gone. Not "gone until
199
- support resets it" — mathematically gone. That's not a bug, that's the
200
- entire design: every "forgot password?" flow is a back door, and back doors
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
201
232
  don't check IDs. Pick a long phrase you can't forget (a sentence beats
202
233
  `Tr0ub4dor&3`), and know that backups protect you from losing the *file*,
203
234
  never from forgetting the *word*.
@@ -241,8 +272,7 @@ I'd rather you know these than find them:
241
272
  Malware already on your machine could read it — true of every password
242
273
  manager ever made; the vault protects the file at rest, not a compromised
243
274
  machine.
244
- - Single file, no sync. Syncing is your problem for now (and honestly, `cp`
245
- is a fine answer).
275
+ - Single file, no sync. Syncing is your problem for now.
246
276
 
247
277
  ## Running the checks
248
278
 
@@ -4,15 +4,15 @@
4
4
 
5
5
  <h1 align="center">The Paladin</h1>
6
6
 
7
- <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, no mercy.</em></p>
7
+ <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, just protection.</em></p>
8
8
 
9
9
  A password manager that is one encrypted file on my own disk and a small
10
10
  terminal app in front of it. No browser, no cloud, no daemon, no ai running
11
11
  in the background, no company holding my secrets. One file, one master password,
12
12
  that's the whole thing.
13
13
 
14
- (It installs as `basic-password-manager` because the name came after the project.
15
- It answers to `paladin` as is its name)
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
16
 
17
17
  ![unlocking the vault](assets/unlock.gif)
18
18
 
@@ -41,26 +41,28 @@ around, Enter copies the selected password to your clipboard. `n` adds,
41
41
  `e` edits, `d` deletes (it asks first), `q` quits. Every key is listed in
42
42
  the bottom bar so there's nothing to memorize.
43
43
 
44
- The search forgives you. Press `/` and type whatever fragments you remember
45
- — every word just has to appear *somewhere* in the entry, any field, any
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
46
  order. It even searches inside the passwords themselves, for the day all
47
- you remember is what you typed on that site (matches get highlighted in the
48
- theme's accent, except password matches, which quietly show the row and
49
- highlight nothing — passwords never get displayed, that's the deal).
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).
50
51
 
51
52
  ![searching the vault](assets/search.gif)
52
53
 
53
- The rarer stuff lives in the command palette — `ctrl+p`, type a few letters,
54
- done. Importing your browser's passwords and changing the master password
55
- both live there.
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.
56
58
 
57
59
  ![the command palette](assets/palette.gif)
58
60
 
59
- And themes. `t` opens the picker; the bundled ones — muted-slate (default),
61
+ And themes. `t` opens the theme picker; the bundled ones — muted-slate (default),
60
62
  dawn, matrix — are lifted with love from
61
63
  [tuxedo](https://github.com/webstonehq/tuxedo)'s palettes, and all of
62
64
  Textual's built-ins are in there too. Your pick sticks across runs. Even the
63
- knight on the unlock screen dresses to match.
65
+ knight on the unlock screen dresses to match your theme, try it ;)
64
66
 
65
67
  ![switching themes](assets/themes.gif)
66
68
 
@@ -71,16 +73,17 @@ Same vault, no interface, for when you just want the thing:
71
73
  | Command | What it does |
72
74
  |---|---|
73
75
  | `paladin init` | create a new empty vault |
74
- | `paladin add github` | store an entry (prompts for username/password/notes) |
75
- | `paladin add github --gen` | same, but it invents a strong random password for you |
76
- | `paladin get github` | copy the password to the clipboard, show the username |
77
- | `paladin edit github` | update an entry field by field (Enter keeps what's there) |
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) |
78
80
  | `paladin passwd` | change the master password |
79
81
  | `paladin ls` | list entry names |
80
- | `paladin rm github` | delete an entry |
82
+ | `paladin rm example` | delete an entry |
81
83
  | `paladin find "dt bank"` | search names, usernames, notes — every word must match, any order |
82
84
  | `paladin gen -l 32` | just print a random password |
83
85
  | `paladin import passwords.csv` | import a browser CSV export (see below) |
86
+ | `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
84
87
  | `paladin about` | meet the knight |
85
88
 
86
89
  The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
@@ -93,19 +96,47 @@ recovery codes, PINs, the answer to "what was your first pet".
93
96
 
94
97
  This is why the project exists, so here's the exit route:
95
98
 
96
- 1. Export: Chrome → `chrome://password-manager/settings` → "Export passwords".
97
- Firefox → `about:logins` → ⋯ menu → "Export passwords". Either way you get
98
- a CSV.
99
- 2. `paladin import passwords.csv`
100
- 3. **Delete that CSV immediately** — it's every password you own, in
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
101
105
  plaintext: `shred -u passwords.csv`
102
- 4. Turn off password saving in the browser and delete what it stored.
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.
103
135
 
104
136
  ## How it works
105
137
 
106
- Three layers, ~120 lines total, and I mean it about the reading order — this
107
- project exists because I wanted to understand this stuff, and the code is
108
- meant to be read:
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:
109
140
 
110
141
  ### 1. `crypto.py` — password → key, key → ciphertext
111
142
 
@@ -180,9 +211,8 @@ partition both systems mount and point both at it:
180
211
 
181
212
  ## Troubleshooting
182
213
 
183
- **"I forgot the master password."** The data is gone. Not "gone until
184
- support resets it" — mathematically gone. That's not a bug, that's the
185
- entire design: every "forgot password?" flow is a back door, and back doors
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
186
216
  don't check IDs. Pick a long phrase you can't forget (a sentence beats
187
217
  `Tr0ub4dor&3`), and know that backups protect you from losing the *file*,
188
218
  never from forgetting the *word*.
@@ -226,8 +256,7 @@ I'd rather you know these than find them:
226
256
  Malware already on your machine could read it — true of every password
227
257
  manager ever made; the vault protects the file at rest, not a compromised
228
258
  machine.
229
- - Single file, no sync. Syncing is your problem for now (and honestly, `cp`
230
- is a fine answer).
259
+ - Single file, no sync. Syncing is your problem for now.
231
260
 
232
261
  ## Running the checks
233
262
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-password-manager
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: The Paladin — a local encrypted password manager that lives in your terminal
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/Daisentaur/password-manager
@@ -11,6 +11,7 @@ License-File: LICENSE
11
11
  Requires-Dist: argon2-cffi
12
12
  Requires-Dist: cryptography
13
13
  Requires-Dist: textual
14
+ Requires-Dist: qrcode
14
15
  Dynamic: license-file
15
16
 
16
17
  <p align="center">
@@ -19,15 +20,15 @@ Dynamic: license-file
19
20
 
20
21
  <h1 align="center">The Paladin</h1>
21
22
 
22
- <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, no mercy.</em></p>
23
+ <p align="center"><em>Your passwords, guarded locally. No browser, no cloud, just protection.</em></p>
23
24
 
24
25
  A password manager that is one encrypted file on my own disk and a small
25
26
  terminal app in front of it. No browser, no cloud, no daemon, no ai running
26
27
  in the background, no company holding my secrets. One file, one master password,
27
28
  that's the whole thing.
28
29
 
29
- (It installs as `basic-password-manager` because the name came after the project.
30
- It answers to `paladin` as is its name)
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)
31
32
 
32
33
  ![unlocking the vault](assets/unlock.gif)
33
34
 
@@ -56,26 +57,28 @@ around, Enter copies the selected password to your clipboard. `n` adds,
56
57
  `e` edits, `d` deletes (it asks first), `q` quits. Every key is listed in
57
58
  the bottom bar so there's nothing to memorize.
58
59
 
59
- The search forgives you. Press `/` and type whatever fragments you remember
60
- — every word just has to appear *somewhere* in the entry, any field, any
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
61
62
  order. It even searches inside the passwords themselves, for the day all
62
- you remember is what you typed on that site (matches get highlighted in the
63
- theme's accent, except password matches, which quietly show the row and
64
- highlight nothing — passwords never get displayed, that's the deal).
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).
65
67
 
66
68
  ![searching the vault](assets/search.gif)
67
69
 
68
- The rarer stuff lives in the command palette — `ctrl+p`, type a few letters,
69
- done. Importing your browser's passwords and changing the master password
70
- both live there.
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.
71
74
 
72
75
  ![the command palette](assets/palette.gif)
73
76
 
74
- And themes. `t` opens the picker; the bundled ones — muted-slate (default),
77
+ And themes. `t` opens the theme picker; the bundled ones — muted-slate (default),
75
78
  dawn, matrix — are lifted with love from
76
79
  [tuxedo](https://github.com/webstonehq/tuxedo)'s palettes, and all of
77
80
  Textual's built-ins are in there too. Your pick sticks across runs. Even the
78
- knight on the unlock screen dresses to match.
81
+ knight on the unlock screen dresses to match your theme, try it ;)
79
82
 
80
83
  ![switching themes](assets/themes.gif)
81
84
 
@@ -86,16 +89,17 @@ Same vault, no interface, for when you just want the thing:
86
89
  | Command | What it does |
87
90
  |---|---|
88
91
  | `paladin init` | create a new empty vault |
89
- | `paladin add github` | store an entry (prompts for username/password/notes) |
90
- | `paladin add github --gen` | same, but it invents a strong random password for you |
91
- | `paladin get github` | copy the password to the clipboard, show the username |
92
- | `paladin edit github` | update an entry field by field (Enter keeps what's there) |
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) |
93
96
  | `paladin passwd` | change the master password |
94
97
  | `paladin ls` | list entry names |
95
- | `paladin rm github` | delete an entry |
98
+ | `paladin rm example` | delete an entry |
96
99
  | `paladin find "dt bank"` | search names, usernames, notes — every word must match, any order |
97
100
  | `paladin gen -l 32` | just print a random password |
98
101
  | `paladin import passwords.csv` | import a browser CSV export (see below) |
102
+ | `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
99
103
  | `paladin about` | meet the knight |
100
104
 
101
105
  The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
@@ -108,19 +112,47 @@ recovery codes, PINs, the answer to "what was your first pet".
108
112
 
109
113
  This is why the project exists, so here's the exit route:
110
114
 
111
- 1. Export: Chrome → `chrome://password-manager/settings` → "Export passwords".
112
- Firefox → `about:logins` → ⋯ menu → "Export passwords". Either way you get
113
- a CSV.
114
- 2. `paladin import passwords.csv`
115
- 3. **Delete that CSV immediately** — it's every password you own, in
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
116
121
  plaintext: `shred -u passwords.csv`
117
- 4. Turn off password saving in the browser and delete what it stored.
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.
118
151
 
119
152
  ## How it works
120
153
 
121
- Three layers, ~120 lines total, and I mean it about the reading order — this
122
- project exists because I wanted to understand this stuff, and the code is
123
- meant to be read:
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:
124
156
 
125
157
  ### 1. `crypto.py` — password → key, key → ciphertext
126
158
 
@@ -195,9 +227,8 @@ partition both systems mount and point both at it:
195
227
 
196
228
  ## Troubleshooting
197
229
 
198
- **"I forgot the master password."** The data is gone. Not "gone until
199
- support resets it" — mathematically gone. That's not a bug, that's the
200
- entire design: every "forgot password?" flow is a back door, and back doors
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
201
232
  don't check IDs. Pick a long phrase you can't forget (a sentence beats
202
233
  `Tr0ub4dor&3`), and know that backups protect you from losing the *file*,
203
234
  never from forgetting the *word*.
@@ -241,8 +272,7 @@ I'd rather you know these than find them:
241
272
  Malware already on your machine could read it — true of every password
242
273
  manager ever made; the vault protects the file at rest, not a compromised
243
274
  machine.
244
- - Single file, no sync. Syncing is your problem for now (and honestly, `cp`
245
- is a fine answer).
275
+ - Single file, no sync. Syncing is your problem for now.
246
276
 
247
277
  ## Running the checks
248
278
 
@@ -11,5 +11,8 @@ pw_manager/__init__.py
11
11
  pw_manager/cli.py
12
12
  pw_manager/crypto.py
13
13
  pw_manager/logo.py
14
+ pw_manager/mobile.py
14
15
  pw_manager/tui.py
15
- pw_manager/vault.py
16
+ pw_manager/vault.py
17
+ pw_manager/web/argon2.umd.min.js
18
+ pw_manager/web/page.html
@@ -55,6 +55,15 @@ def cmd_init(args):
55
55
  print(f"empty vault created at {VAULT_PATH}")
56
56
 
57
57
 
58
+ def cmd_mobile(args):
59
+ from . import mobile
60
+
61
+ try:
62
+ mobile.run(VAULT_PATH, stable_url=args.url)
63
+ except mobile.MobileError as e:
64
+ sys.exit(str(e))
65
+
66
+
58
67
  def cmd_about(args):
59
68
  from importlib.metadata import PackageNotFoundError, version
60
69
 
@@ -218,6 +227,8 @@ def main():
218
227
  f.add_argument("term")
219
228
  i = sub.add_parser("import", help="import a browser CSV export")
220
229
  i.add_argument("csv_file")
230
+ m = sub.add_parser("mobile", help="open your vault on your phone over an HTTPS tunnel")
231
+ m.add_argument("--url", help="serve at your own https subdomain instead of a quick tunnel")
221
232
  sub.add_parser("about", help="meet the knight")
222
233
 
223
234
  args = p.parse_args()
@@ -102,6 +102,29 @@ def rich_text(tint: str | None = None, accent: str | None = None) -> Text:
102
102
  return text
103
103
 
104
104
 
105
+ def svg(tint: str | None = None, accent: str | None = None, scale: int = 10) -> str:
106
+ """The knight as an SVG (one <rect> per pixel), for the mobile page.
107
+ Same tinting as the TUI: brightness kept, hue from the theme."""
108
+ tint_rgb = _hex_rgb(tint) if tint else None
109
+ accent_rgb = _hex_rgb(accent) if accent else tint_rgb
110
+ w, h = len(ROWS[0]), len(ROWS)
111
+ rects = []
112
+ for y, row in enumerate(ROWS):
113
+ for x, ch in enumerate(row):
114
+ pixel = _tinted(PALETTE.get(ch), tint_rgb, accent_rgb)
115
+ if pixel is None:
116
+ continue
117
+ rects.append(
118
+ '<rect x="%d" y="%d" width="1" height="1" fill="#%02x%02x%02x"/>'
119
+ % (x, y, *pixel)
120
+ )
121
+ return (
122
+ '<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
123
+ 'viewBox="0 0 %d %d" shape-rendering="crispEdges">%s</svg>'
124
+ % (w * scale, h * scale, w, h, "".join(rects))
125
+ )
126
+
127
+
105
128
  def ascii_art() -> str:
106
129
  """The knight as plain monochrome ASCII — two chars per pixel to keep
107
130
  his proportions in terminal cells, denser glyphs for brighter armor."""
@@ -0,0 +1,248 @@
1
+ """`paladin mobile`: serve the vault to your phone over an HTTPS tunnel.
2
+
3
+ The phone gets one self-contained page with the *encrypted* vault embedded.
4
+ The master password is typed on the phone and the key is derived and the
5
+ vault decrypted entirely in the phone's browser — the laptop, the tunnel,
6
+ and the network only ever carry ciphertext.
7
+ """
8
+
9
+ import base64
10
+ import hashlib
11
+ import http.server
12
+ import os
13
+ import platform
14
+ import re
15
+ import secrets
16
+ import shutil
17
+ import stat
18
+ import subprocess
19
+ import sys
20
+ import tarfile
21
+ import tempfile
22
+ import threading
23
+ import time
24
+ import urllib.request
25
+ from importlib.resources import files
26
+
27
+ from . import crypto, logo, vault
28
+
29
+ # Pinned so the binary is reproducible and the checksums below are meaningful.
30
+ CF_VERSION = "2026.7.2"
31
+ CF_SHA256 = {
32
+ "cloudflared-linux-amd64": "ec905ea7b7e327ff8abdde8cb64697a2152de74dbcdbf6aec9db8364eb3886cd",
33
+ "cloudflared-linux-arm64": "405df476437e027fc6d18729a5a77155c0a33a6082aeee60a799a688f3052e66",
34
+ "cloudflared-darwin-amd64.tgz": "4ee0d3b48a990a2f9b5faec5838f73ec1f400aa8e0a4864be576adfafec406cb",
35
+ "cloudflared-darwin-arm64.tgz": "2086e51c61d6565781d84117a5007d0c826d03ffdc74acb91c08c167f9f8cd7c",
36
+ "cloudflared-windows-amd64.exe": "cdb5d4432f6ae1595654a692a51308b69d2bf7af961f5578d9391837cf072df9",
37
+ }
38
+ TOKEN_TTL = 30 * 24 * 3600 # 30 days
39
+
40
+
41
+ class MobileError(Exception):
42
+ """Something stopped the mobile session, with a human message."""
43
+
44
+
45
+ def _data_dir() -> str:
46
+ d = os.path.dirname(
47
+ os.environ.get("PW_VAULT", os.path.expanduser("~/.local/share/pw-manager/vault"))
48
+ )
49
+ os.makedirs(d, exist_ok=True)
50
+ return d
51
+
52
+
53
+ def _asset_name() -> str:
54
+ system = platform.system()
55
+ arch = "arm64" if platform.machine().lower() in ("arm64", "aarch64") else "amd64"
56
+ if system == "Linux":
57
+ return f"cloudflared-linux-{arch}"
58
+ if system == "Darwin":
59
+ return f"cloudflared-darwin-{arch}.tgz"
60
+ if system == "Windows":
61
+ return f"cloudflared-windows-{arch}.exe"
62
+ raise MobileError(f"no cloudflared build for {system}; install it manually")
63
+
64
+
65
+ def cloudflared_path() -> str:
66
+ """Find cloudflared on PATH, or in our cache, or fetch the pinned official
67
+ binary (checksum-verified) into the cache."""
68
+ found = shutil.which("cloudflared")
69
+ if found:
70
+ return found
71
+ cache = os.path.join(_data_dir(), "cloudflared")
72
+ if os.path.exists(cache):
73
+ return cache
74
+
75
+ asset = _asset_name()
76
+ url = f"https://github.com/cloudflare/cloudflared/releases/download/{CF_VERSION}/{asset}"
77
+ print(f"fetching cloudflared {CF_VERSION} (one time)…", file=sys.stderr)
78
+ try:
79
+ with urllib.request.urlopen(url) as r:
80
+ blob = r.read()
81
+ except OSError as e:
82
+ raise MobileError(f"couldn't download cloudflared ({e}); check your connection")
83
+ if hashlib.sha256(blob).hexdigest() != CF_SHA256[asset]:
84
+ raise MobileError("cloudflared checksum mismatch — refusing to run it")
85
+
86
+ if asset.endswith(".tgz"):
87
+ with tempfile.TemporaryDirectory() as td:
88
+ tgz = os.path.join(td, asset)
89
+ open(tgz, "wb").write(blob)
90
+ with tarfile.open(tgz) as t:
91
+ member = next(m for m in t.getmembers() if m.name.endswith("cloudflared"))
92
+ t.extract(member, td)
93
+ shutil.move(os.path.join(td, member.name), cache)
94
+ else:
95
+ open(cache, "wb").write(blob)
96
+ os.chmod(cache, os.stat(cache).st_mode | stat.S_IEXEC | stat.S_IRUSR)
97
+ return cache
98
+
99
+
100
+ def _load_token() -> str:
101
+ """A path segment that rotates every 30 days (matters in --url mode; the
102
+ quick tunnel URL is already random)."""
103
+ path = os.path.join(_data_dir(), "mobile-token")
104
+ try:
105
+ created, token = open(path).read().split(":", 1)
106
+ if time.time() - float(created) < TOKEN_TTL:
107
+ return token.strip()
108
+ except (OSError, ValueError):
109
+ pass
110
+ token = secrets.token_urlsafe(16)
111
+ with open(path, "w") as f:
112
+ f.write(f"{time.time()}:{token}")
113
+ return token
114
+
115
+
116
+ def _theme_colors() -> dict:
117
+ """Resolve the theme saved by the TUI to a full color palette. The page
118
+ is baked with whatever theme is current when the QR is generated."""
119
+ default = {
120
+ "background": "#1A1D23", "surface": "#1F232B", "panel": "#2A2F38",
121
+ "foreground": "#C8CCD4", "primary": "#8AA9C9", "warning": "#D3B06A",
122
+ "error": "#E07A7A", "text-muted": "#6B7280",
123
+ }
124
+ try:
125
+ from textual.theme import BUILTIN_THEMES
126
+
127
+ from .tui import THEME_FILE, TUXEDO_THEMES
128
+
129
+ name = open(THEME_FILE).read().strip()
130
+ registry = {t.name: t for t in TUXEDO_THEMES}
131
+ registry.update(BUILTIN_THEMES)
132
+ cs = registry[name].to_color_system().generate()
133
+ return {**default, **cs}
134
+ except (OSError, KeyError, ImportError):
135
+ return default
136
+
137
+
138
+ def _build_page(raw_vault: bytes) -> bytes:
139
+ salt = raw_vault[4 : 4 + crypto.SALT_LEN]
140
+ blob = raw_vault[4 + crypto.SALT_LEN :] # nonce + ciphertext+tag
141
+ argon_js = files("pw_manager.web").joinpath("argon2.umd.min.js").read_text()
142
+ template = files("pw_manager.web").joinpath("page.html").read_text()
143
+ c = _theme_colors()
144
+ knight = logo.svg(tint=c["primary"], accent=c.get("warning", c["primary"]))
145
+ return (
146
+ template.replace("/*__ARGON2__*/", argon_js)
147
+ .replace("__KNIGHT__", f'<div id="knight">{knight}</div>')
148
+ .replace("__C_BG__", c["background"])
149
+ .replace("__C_SURFACE__", c["surface"])
150
+ .replace("__C_PANEL__", c["panel"])
151
+ .replace("__C_FG__", c["foreground"])
152
+ .replace("__C_PRIMARY__", c["primary"])
153
+ .replace("__C_ERROR__", c["error"])
154
+ .replace("__C_DIM__", c.get("text-muted", "#6B7280"))
155
+ .replace("__SALT__", base64.b64encode(salt).decode())
156
+ .replace("__BLOB__", base64.b64encode(blob).decode())
157
+ .replace("__NONCE_LEN__", str(crypto.NONCE_LEN))
158
+ .replace("__TIME__", "3")
159
+ .replace("__MEM_KIB__", str(64 * 1024))
160
+ .replace("__PAR__", "4")
161
+ .replace("__KEYLEN__", str(crypto.KEY_LEN))
162
+ ).encode()
163
+
164
+
165
+ def _serve(vault_path: str, token: str, port: int = 0) -> http.server.HTTPServer:
166
+ page_holder = {}
167
+
168
+ class Handler(http.server.BaseHTTPRequestHandler):
169
+ def do_GET(self):
170
+ if self.path.rstrip("/") != f"/{token}":
171
+ self.send_error(404)
172
+ return
173
+ # rebuild per request so edits made on the laptop show on refresh
174
+ page = _build_page(open(vault_path, "rb").read())
175
+ self.send_response(200)
176
+ self.send_header("Content-Type", "text/html; charset=utf-8")
177
+ self.send_header("Cache-Control", "no-store")
178
+ self.end_headers()
179
+ self.wfile.write(page)
180
+
181
+ def log_message(self, *a):
182
+ pass # keep the terminal clean
183
+
184
+ return http.server.HTTPServer(("127.0.0.1", port), Handler)
185
+
186
+
187
+ def _start_tunnel(cf: str, port: int) -> tuple[subprocess.Popen, str]:
188
+ proc = subprocess.Popen(
189
+ [cf, "tunnel", "--url", f"http://127.0.0.1:{port}"],
190
+ stdout=subprocess.PIPE,
191
+ stderr=subprocess.STDOUT,
192
+ text=True,
193
+ )
194
+ url_re = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
195
+ deadline = time.time() + 30
196
+ while time.time() < deadline:
197
+ line = proc.stdout.readline()
198
+ if not line and proc.poll() is not None:
199
+ break
200
+ m = url_re.search(line)
201
+ if m:
202
+ return proc, m.group(0)
203
+ proc.terminate()
204
+ raise MobileError("cloudflared didn't produce a tunnel URL in time")
205
+
206
+
207
+ def run(vault_path: str, stable_url: str | None = None, port: int = 0) -> None:
208
+ if not os.path.exists(vault_path):
209
+ raise MobileError(f"no vault at {vault_path} — run 'paladin init' first")
210
+
211
+ # env defaults so `paladin mobile` alone can serve your own subdomain
212
+ stable_url = stable_url or os.environ.get("PALADIN_MOBILE_URL")
213
+ if stable_url and not port:
214
+ port = int(os.environ.get("PALADIN_MOBILE_PORT", "8787"))
215
+
216
+ token = _load_token()
217
+ server = _serve(vault_path, token, port)
218
+ port = server.server_address[1]
219
+ threading.Thread(target=server.serve_forever, daemon=True).start()
220
+
221
+ proc = None
222
+ if stable_url:
223
+ base = stable_url.rstrip("/") # your reverse proxy points here → 127.0.0.1:port
224
+ else:
225
+ cf = cloudflared_path()
226
+ proc, tunnel_url = _start_tunnel(cf, port)
227
+ base = tunnel_url
228
+ url = f"{base}/{token}"
229
+
230
+ import qrcode
231
+
232
+ qr = qrcode.QRCode(border=1)
233
+ qr.add_data(url)
234
+ print("\n scan with your phone camera:\n")
235
+ qr.print_ascii(invert=True)
236
+ print(f"\n {url}\n")
237
+ print(" the master password is typed on your phone; only ciphertext leaves this machine.")
238
+ print(" Ctrl+C to stop the session.\n")
239
+
240
+ try:
241
+ while True:
242
+ time.sleep(1)
243
+ except KeyboardInterrupt:
244
+ print("\nsession ended.")
245
+ finally:
246
+ server.shutdown()
247
+ if proc:
248
+ proc.terminate()
@@ -361,6 +361,7 @@ class VaultCommands(Provider):
361
361
  app = self.app
362
362
  return [
363
363
  ("Import browser CSV", "add every entry from a Chrome/Firefox password export", app._import_csv),
364
+ ("Open on phone", "serve the vault to your phone over an HTTPS tunnel (QR code)", app._mobile),
364
365
  ("Change master password", "re-encrypt the vault under a new master password", app._change_master),
365
366
  ]
366
367
 
@@ -483,6 +484,15 @@ class PwApp(App):
483
484
 
484
485
  self.push_screen(ImportModal(), done)
485
486
 
487
+ def _mobile(self) -> None:
488
+ from . import mobile
489
+
490
+ with self.suspend(): # hand the terminal to the QR + tunnel until Ctrl+C
491
+ try:
492
+ mobile.run(VAULT_PATH)
493
+ except mobile.MobileError as e:
494
+ input(f"\n{e}\n\npress Enter to return… ")
495
+
486
496
  def _change_master(self) -> None:
487
497
  def done(new: str | None) -> None:
488
498
  if new:
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * hash-wasm (https://www.npmjs.com/package/hash-wasm)
3
+ * (c) Dani Biro
4
+ * @license MIT
5
+ */
6
+
7
+ !function(A,I){"object"==typeof exports&&"undefined"!=typeof module?I(exports):"function"==typeof define&&define.amd?define(["exports"],I):I((A="undefined"!=typeof globalThis?globalThis:A||self).hashwasm=A.hashwasm||{})}(this,(function(A){"use strict";function I(A,I,i,C){return new(i||(i=Promise))((function(g,Q){function B(A){try{e(C.next(A))}catch(A){Q(A)}}function h(A){try{e(C.throw(A))}catch(A){Q(A)}}function e(A){var I;A.done?g(A.value):(I=A.value,I instanceof i?I:new i((function(A){A(I)}))).then(B,h)}e((C=C.apply(A,I||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;var i,C={name:"argon2",data:"AGFzbQEAAAABKQVgAX8Bf2AAAX9gEH9/f39/f39/f39/f39/f38AYAR/f39/AGACf38AAwYFAAECAwQFBgEBAoCAAgYIAX8BQZCoBAsHQQQGbWVtb3J5AgASSGFzaF9TZXRNZW1vcnlTaXplAAAOSGFzaF9HZXRCdWZmZXIAAQ5IYXNoX0NhbGN1bGF0ZQAECvEyBVgBAn9BACEBAkAgAEEAKAKICCICRg0AAkAgACACayIAQRB2IABBgIB8cSAASWoiAEAAQX9HDQBB/wHADwtBACEBQQBBACkDiAggAEEQdK18NwOICAsgAcALcAECfwJAQQAoAoAIIgANAEEAPwBBEHQiADYCgAhBACgCiAgiAUGAgCBGDQACQEGAgCAgAWsiAEEQdiAAQYCAfHEgAElqIgBAAEF/Rw0AQQAPC0EAQQApA4gIIABBEHStfDcDiAhBACgCgAghAAsgAAvcDgECfiAAIAQpAwAiECAAKQMAIhF8IBFCAYZC/v///x+DIBBC/////w+DfnwiEDcDACAMIBAgDCkDAIVCIIkiEDcDACAIIBAgCCkDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgBCAQIAQpAwCFQiiJIhA3AwAgACAQIAApAwAiEXwgEEL/////D4MgEUIBhkL+////H4N+fCIQNwMAIAwgECAMKQMAhUIwiSIQNwMAIAggECAIKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACAEIBAgBCkDAIVCAYk3AwAgASAFKQMAIhAgASkDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgDSAQIA0pAwCFQiCJIhA3AwAgCSAQIAkpAwAiEXwgEUIBhkL+////H4MgEEL/////D4N+fCIQNwMAIAUgECAFKQMAhUIoiSIQNwMAIAEgECABKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACANIBAgDSkDAIVCMIkiEDcDACAJIBAgCSkDACIRfCAQQv////8PgyARQgGGQv7///8fg358IhA3AwAgBSAQIAUpAwCFQgGJNwMAIAIgBikDACIQIAIpAwAiEXwgEUIBhkL+////H4MgEEL/////D4N+fCIQNwMAIA4gECAOKQMAhUIgiSIQNwMAIAogECAKKQMAIhF8IBFCAYZC/v///x+DIBBC/////w+DfnwiEDcDACAGIBAgBikDAIVCKIkiEDcDACACIBAgAikDACIRfCAQQv////8PgyARQgGGQv7///8fg358IhA3AwAgDiAQIA4pAwCFQjCJIhA3AwAgCiAQIAopAwAiEXwgEEL/////D4MgEUIBhkL+////H4N+fCIQNwMAIAYgECAGKQMAhUIBiTcDACADIAcpAwAiECADKQMAIhF8IBFCAYZC/v///x+DIBBC/////w+DfnwiEDcDACAPIBAgDykDAIVCIIkiEDcDACALIBAgCykDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgByAQIAcpAwCFQiiJIhA3AwAgAyAQIAMpAwAiEXwgEEL/////D4MgEUIBhkL+////H4N+fCIQNwMAIA8gECAPKQMAhUIwiSIQNwMAIAsgECALKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACAHIBAgBykDAIVCAYk3AwAgACAFKQMAIhAgACkDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgDyAQIA8pAwCFQiCJIhA3AwAgCiAQIAopAwAiEXwgEUIBhkL+////H4MgEEL/////D4N+fCIQNwMAIAUgECAFKQMAhUIoiSIQNwMAIAAgECAAKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACAPIBAgDykDAIVCMIkiEDcDACAKIBAgCikDACIRfCAQQv////8PgyARQgGGQv7///8fg358IhA3AwAgBSAQIAUpAwCFQgGJNwMAIAEgBikDACIQIAEpAwAiEXwgEUIBhkL+////H4MgEEL/////D4N+fCIQNwMAIAwgECAMKQMAhUIgiSIQNwMAIAsgECALKQMAIhF8IBFCAYZC/v///x+DIBBC/////w+DfnwiEDcDACAGIBAgBikDAIVCKIkiEDcDACABIBAgASkDACIRfCAQQv////8PgyARQgGGQv7///8fg358IhA3AwAgDCAQIAwpAwCFQjCJIhA3AwAgCyAQIAspAwAiEXwgEEL/////D4MgEUIBhkL+////H4N+fCIQNwMAIAYgECAGKQMAhUIBiTcDACACIAcpAwAiECACKQMAIhF8IBFCAYZC/v///x+DIBBC/////w+DfnwiEDcDACANIBAgDSkDAIVCIIkiEDcDACAIIBAgCCkDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgByAQIAcpAwCFQiiJIhA3AwAgAiAQIAIpAwAiEXwgEEL/////D4MgEUIBhkL+////H4N+fCIQNwMAIA0gECANKQMAhUIwiSIQNwMAIAggECAIKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACAHIBAgBykDAIVCAYk3AwAgAyAEKQMAIhAgAykDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgDiAQIA4pAwCFQiCJIhA3AwAgCSAQIAkpAwAiEXwgEUIBhkL+////H4MgEEL/////D4N+fCIQNwMAIAQgECAEKQMAhUIoiSIQNwMAIAMgECADKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACAOIBAgDikDAIVCMIkiEDcDACAJIBAgCSkDACIRfCAQQv////8PgyARQgGGQv7///8fg358IhA3AwAgBCAQIAQpAwCFQgGJNwMAC98aAQN/QQAhBEEAIAIpAwAgASkDAIU3A5AIQQAgAikDCCABKQMIhTcDmAhBACACKQMQIAEpAxCFNwOgCEEAIAIpAxggASkDGIU3A6gIQQAgAikDICABKQMghTcDsAhBACACKQMoIAEpAyiFNwO4CEEAIAIpAzAgASkDMIU3A8AIQQAgAikDOCABKQM4hTcDyAhBACACKQNAIAEpA0CFNwPQCEEAIAIpA0ggASkDSIU3A9gIQQAgAikDUCABKQNQhTcD4AhBACACKQNYIAEpA1iFNwPoCEEAIAIpA2AgASkDYIU3A/AIQQAgAikDaCABKQNohTcD+AhBACACKQNwIAEpA3CFNwOACUEAIAIpA3ggASkDeIU3A4gJQQAgAikDgAEgASkDgAGFNwOQCUEAIAIpA4gBIAEpA4gBhTcDmAlBACACKQOQASABKQOQAYU3A6AJQQAgAikDmAEgASkDmAGFNwOoCUEAIAIpA6ABIAEpA6ABhTcDsAlBACACKQOoASABKQOoAYU3A7gJQQAgAikDsAEgASkDsAGFNwPACUEAIAIpA7gBIAEpA7gBhTcDyAlBACACKQPAASABKQPAAYU3A9AJQQAgAikDyAEgASkDyAGFNwPYCUEAIAIpA9ABIAEpA9ABhTcD4AlBACACKQPYASABKQPYAYU3A+gJQQAgAikD4AEgASkD4AGFNwPwCUEAIAIpA+gBIAEpA+gBhTcD+AlBACACKQPwASABKQPwAYU3A4AKQQAgAikD+AEgASkD+AGFNwOICkEAIAIpA4ACIAEpA4AChTcDkApBACACKQOIAiABKQOIAoU3A5gKQQAgAikDkAIgASkDkAKFNwOgCkEAIAIpA5gCIAEpA5gChTcDqApBACACKQOgAiABKQOgAoU3A7AKQQAgAikDqAIgASkDqAKFNwO4CkEAIAIpA7ACIAEpA7AChTcDwApBACACKQO4AiABKQO4AoU3A8gKQQAgAikDwAIgASkDwAKFNwPQCkEAIAIpA8gCIAEpA8gChTcD2ApBACACKQPQAiABKQPQAoU3A+AKQQAgAikD2AIgASkD2AKFNwPoCkEAIAIpA+ACIAEpA+AChTcD8ApBACACKQPoAiABKQPoAoU3A/gKQQAgAikD8AIgASkD8AKFNwOAC0EAIAIpA/gCIAEpA/gChTcDiAtBACACKQOAAyABKQOAA4U3A5ALQQAgAikDiAMgASkDiAOFNwOYC0EAIAIpA5ADIAEpA5ADhTcDoAtBACACKQOYAyABKQOYA4U3A6gLQQAgAikDoAMgASkDoAOFNwOwC0EAIAIpA6gDIAEpA6gDhTcDuAtBACACKQOwAyABKQOwA4U3A8ALQQAgAikDuAMgASkDuAOFNwPIC0EAIAIpA8ADIAEpA8ADhTcD0AtBACACKQPIAyABKQPIA4U3A9gLQQAgAikD0AMgASkD0AOFNwPgC0EAIAIpA9gDIAEpA9gDhTcD6AtBACACKQPgAyABKQPgA4U3A/ALQQAgAikD6AMgASkD6AOFNwP4C0EAIAIpA/ADIAEpA/ADhTcDgAxBACACKQP4AyABKQP4A4U3A4gMQQAgAikDgAQgASkDgASFNwOQDEEAIAIpA4gEIAEpA4gEhTcDmAxBACACKQOQBCABKQOQBIU3A6AMQQAgAikDmAQgASkDmASFNwOoDEEAIAIpA6AEIAEpA6AEhTcDsAxBACACKQOoBCABKQOoBIU3A7gMQQAgAikDsAQgASkDsASFNwPADEEAIAIpA7gEIAEpA7gEhTcDyAxBACACKQPABCABKQPABIU3A9AMQQAgAikDyAQgASkDyASFNwPYDEEAIAIpA9AEIAEpA9AEhTcD4AxBACACKQPYBCABKQPYBIU3A+gMQQAgAikD4AQgASkD4ASFNwPwDEEAIAIpA+gEIAEpA+gEhTcD+AxBACACKQPwBCABKQPwBIU3A4ANQQAgAikD+AQgASkD+ASFNwOIDUEAIAIpA4AFIAEpA4AFhTcDkA1BACACKQOIBSABKQOIBYU3A5gNQQAgAikDkAUgASkDkAWFNwOgDUEAIAIpA5gFIAEpA5gFhTcDqA1BACACKQOgBSABKQOgBYU3A7ANQQAgAikDqAUgASkDqAWFNwO4DUEAIAIpA7AFIAEpA7AFhTcDwA1BACACKQO4BSABKQO4BYU3A8gNQQAgAikDwAUgASkDwAWFNwPQDUEAIAIpA8gFIAEpA8gFhTcD2A1BACACKQPQBSABKQPQBYU3A+ANQQAgAikD2AUgASkD2AWFNwPoDUEAIAIpA+AFIAEpA+AFhTcD8A1BACACKQPoBSABKQPoBYU3A/gNQQAgAikD8AUgASkD8AWFNwOADkEAIAIpA/gFIAEpA/gFhTcDiA5BACACKQOABiABKQOABoU3A5AOQQAgAikDiAYgASkDiAaFNwOYDkEAIAIpA5AGIAEpA5AGhTcDoA5BACACKQOYBiABKQOYBoU3A6gOQQAgAikDoAYgASkDoAaFNwOwDkEAIAIpA6gGIAEpA6gGhTcDuA5BACACKQOwBiABKQOwBoU3A8AOQQAgAikDuAYgASkDuAaFNwPIDkEAIAIpA8AGIAEpA8AGhTcD0A5BACACKQPIBiABKQPIBoU3A9gOQQAgAikD0AYgASkD0AaFNwPgDkEAIAIpA9gGIAEpA9gGhTcD6A5BACACKQPgBiABKQPgBoU3A/AOQQAgAikD6AYgASkD6AaFNwP4DkEAIAIpA/AGIAEpA/AGhTcDgA9BACACKQP4BiABKQP4BoU3A4gPQQAgAikDgAcgASkDgAeFNwOQD0EAIAIpA4gHIAEpA4gHhTcDmA9BACACKQOQByABKQOQB4U3A6APQQAgAikDmAcgASkDmAeFNwOoD0EAIAIpA6AHIAEpA6AHhTcDsA9BACACKQOoByABKQOoB4U3A7gPQQAgAikDsAcgASkDsAeFNwPAD0EAIAIpA7gHIAEpA7gHhTcDyA9BACACKQPAByABKQPAB4U3A9APQQAgAikDyAcgASkDyAeFNwPYD0EAIAIpA9AHIAEpA9AHhTcD4A9BACACKQPYByABKQPYB4U3A+gPQQAgAikD4AcgASkD4AeFNwPwD0EAIAIpA+gHIAEpA+gHhTcD+A9BACACKQPwByABKQPwB4U3A4AQQQAgAikD+AcgASkD+AeFNwOIEEGQCEGYCEGgCEGoCEGwCEG4CEHACEHICEHQCEHYCEHgCEHoCEHwCEH4CEGACUGICRACQZAJQZgJQaAJQagJQbAJQbgJQcAJQcgJQdAJQdgJQeAJQegJQfAJQfgJQYAKQYgKEAJBkApBmApBoApBqApBsApBuApBwApByApB0ApB2ApB4ApB6ApB8ApB+ApBgAtBiAsQAkGQC0GYC0GgC0GoC0GwC0G4C0HAC0HIC0HQC0HYC0HgC0HoC0HwC0H4C0GADEGIDBACQZAMQZgMQaAMQagMQbAMQbgMQcAMQcgMQdAMQdgMQeAMQegMQfAMQfgMQYANQYgNEAJBkA1BmA1BoA1BqA1BsA1BuA1BwA1ByA1B0A1B2A1B4A1B6A1B8A1B+A1BgA5BiA4QAkGQDkGYDkGgDkGoDkGwDkG4DkHADkHIDkHQDkHYDkHgDkHoDkHwDkH4DkGAD0GIDxACQZAPQZgPQaAPQagPQbAPQbgPQcAPQcgPQdAPQdgPQeAPQegPQfAPQfgPQYAQQYgQEAJBkAhBmAhBkAlBmAlBkApBmApBkAtBmAtBkAxBmAxBkA1BmA1BkA5BmA5BkA9BmA8QAkGgCEGoCEGgCUGoCUGgCkGoCkGgC0GoC0GgDEGoDEGgDUGoDUGgDkGoDkGgD0GoDxACQbAIQbgIQbAJQbgJQbAKQbgKQbALQbgLQbAMQbgMQbANQbgNQbAOQbgOQbAPQbgPEAJBwAhByAhBwAlByAlBwApByApBwAtByAtBwAxByAxBwA1ByA1BwA5ByA5BwA9ByA8QAkHQCEHYCEHQCUHYCUHQCkHYCkHQC0HYC0HQDEHYDEHQDUHYDUHQDkHYDkHQD0HYDxACQeAIQegIQeAJQegJQeAKQegKQeALQegLQeAMQegMQeANQegNQeAOQegOQeAPQegPEAJB8AhB+AhB8AlB+AlB8ApB+ApB8AtB+AtB8AxB+AxB8A1B+A1B8A5B+A5B8A9B+A8QAkGACUGICUGACkGICkGAC0GIC0GADEGIDEGADUGIDUGADkGIDkGAD0GID0GAEEGIEBACAkACQCADRQ0AA0AgACAEaiIDIAIgBGoiBSkDACABIARqIgYpAwCFIARBkAhqKQMAhSADKQMAhTcDACADQQhqIgMgBUEIaikDACAGQQhqKQMAhSAEQZgIaikDAIUgAykDAIU3AwAgBEEQaiIEQYAIRw0ADAILC0EAIQQDQCAAIARqIgMgAiAEaiIFKQMAIAEgBGoiBikDAIUgBEGQCGopAwCFNwMAIANBCGogBUEIaikDACAGQQhqKQMAhSAEQZgIaikDAIU3AwAgBEEQaiIEQYAIRw0ACwsL5QcMBX8BfgR/An4BfwF+AX8Bfgd/AX4DfwF+AkBBACgCgAgiAiABQQp0aiIDKAIIIAFHDQAgAygCDCEEIAMoAgAhBUEAIAMoAhQiBq03A7gQQQAgBK0iBzcDsBBBACAFIAEgBUECdG4iCGwiCUECdK03A6gQAkACQAJAAkAgBEUNAEF/IQogBUUNASAIQQNsIQsgCEECdCIErSEMIAWtIQ0gBkF/akECSSEOQgAhDwNAQQAgDzcDkBAgD6chEEIAIRFBACEBA0BBACARNwOgECAPIBGEUCIDIA5xIRIgBkEBRiAPUCITIAZBAkYgEUICVHFxciEUQX8gAUEBakEDcSAIbEF/aiATGyEVIAEgEHIhFiABIAhsIRcgA0EBdCEYQgAhGQNAQQBCADcDwBBBACAZNwOYECAYIQECQCASRQ0AQQBCATcDwBBBkBhBkBBBkCBBABADQZAYQZAYQZAgQQAQA0ECIQELAkAgASAITw0AIAQgGaciGmwgF2ogAWohAwNAIANBACAEIAEbQQAgEVAiGxtqQX9qIRwCQAJAIBQNAEEAKAKACCICIBxBCnQiHGohCgwBCwJAIAFB/wBxIgINAEEAQQApA8AQQgF8NwPAEEGQGEGQEEGQIEEAEANBkBhBkBhBkCBBABADCyAcQQp0IRwgAkEDdEGQGGohCkEAKAKACCECCyACIANBCnRqIAIgHGogAiAKKQMAIh1CIIinIAVwIBogFhsiHCAEbCABIAFBACAZIBytUSIcGyIKIBsbIBdqIAogC2ogExsgAUUgHHJrIhsgFWqtIB1C/////w+DIh0gHX5CIIggG61+QiCIfSAMgqdqQQp0akEBEAMgA0EBaiEDIAggAUEBaiIBRw0ACwsgGUIBfCIZIA1SDQALIBFCAXwiEachASARQgRSDQALIA9CAXwiDyAHUg0AC0EAKAKACCECCyAJQQx0QYB4aiEXIAVBf2oiCkUNAgwBC0EAQgM3A6AQQQAgBEF/aq03A5AQQYB4IRcLIAIgF2ohGyAIQQx0IQhBACEcA0AgCCAcQQFqIhxsQYB4aiEEQQAhAQNAIBsgAWoiAyADKQMAIAIgBCABamopAwCFNwMAIANBCGoiAyADKQMAIAIgBCABQQhyamopAwCFNwMAIAFBCGohAyABQRBqIQEgA0H4B0kNAAsgHCAKRw0ACwsgAiAXaiEbQXghAQNAIAIgAWoiA0EIaiAbIAFqIgRBCGopAwA3AwAgA0EQaiAEQRBqKQMANwMAIANBGGogBEEYaikDADcDACADQSBqIARBIGopAwA3AwAgAUEgaiIBQfgHSQ0ACwsL",hash:"e4cdc523"};class g{constructor(){this.mutex=Promise.resolve()}lock(){let A=()=>{};return this.mutex=this.mutex.then((()=>new Promise(A))),new Promise((I=>{A=I}))}dispatch(A){return I(this,void 0,void 0,(function*(){const I=yield this.lock();try{return yield Promise.resolve(A())}finally{I()}}))}}const Q="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,B=null!==(i=Q.Buffer)&&void 0!==i?i:null,h=Q.TextEncoder?new Q.TextEncoder:null;function e(A,I){return(15&A)+(A>>6|A>>3&8)<<4|(15&I)+(I>>6|I>>3&8)}function w(A,I){const i=I.length>>1;for(let C=0;C<i;C++){const i=C<<1;A[C]=e(I.charCodeAt(i),I.charCodeAt(i+1))}}const t="a".charCodeAt(0)-10,o="0".charCodeAt(0);function E(A,I,i){let C=0;for(let g=0;g<i;g++){let i=I[g]>>>4;A[C++]=i>9?i+t:i+o,i=15&I[g],A[C++]=i>9?i+t:i+o}return String.fromCharCode.apply(null,A)}const n=null!==B?A=>{if("string"==typeof A){const I=B.from(A,"utf8");return new Uint8Array(I.buffer,I.byteOffset,I.length)}if(B.isBuffer(A))return new Uint8Array(A.buffer,A.byteOffset,A.length);if(ArrayBuffer.isView(A))return new Uint8Array(A.buffer,A.byteOffset,A.byteLength);throw new Error("Invalid data type!")}:A=>{if("string"==typeof A)return h.encode(A);if(ArrayBuffer.isView(A))return new Uint8Array(A.buffer,A.byteOffset,A.byteLength);throw new Error("Invalid data type!")},r="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",D=new Uint8Array(256);for(let A=0;A<r.length;A++)D[r.charCodeAt(A)]=A;function F(A,I=!0){const i=A.length,C=i%3,g=[],Q=i-C;for(let I=0;I<Q;I+=3){const i=(A[I]<<16&16711680)+(A[I+1]<<8&65280)+(255&A[I+2]),C=r.charAt(i>>18&63)+r.charAt(i>>12&63)+r.charAt(i>>6&63)+r.charAt(63&i);g.push(C)}if(1===C){const C=A[i-1],Q=r.charAt(C>>2),B=r.charAt(C<<4&63);g.push(`${Q}${B}`),I&&g.push("==")}else if(2===C){const C=(A[i-2]<<8)+A[i-1],Q=r.charAt(C>>10),B=r.charAt(C>>4&63),h=r.charAt(C<<2&63);g.push(`${Q}${B}${h}`),I&&g.push("=")}return g.join("")}function f(A){let I=Math.floor(.75*A.length);const i=A.length;return"="===A[i-1]&&(I-=1,"="===A[i-2]&&(I-=1)),I}function k(A){const I=f(A),i=A.length,C=new Uint8Array(I);let g=0;for(let I=0;I<i;I+=4){const i=D[A.charCodeAt(I)],Q=D[A.charCodeAt(I+1)],B=D[A.charCodeAt(I+2)],h=D[A.charCodeAt(I+3)];C[g]=i<<2|Q>>4,g+=1,C[g]=(15&Q)<<4|B>>2,g+=1,C[g]=(3&B)<<6|63&h,g+=1}return C}const a=16384,s=new g,S=new Map;function U(A,i){return I(this,void 0,void 0,(function*(){let C=null,g=null,Q=!1;if("undefined"==typeof WebAssembly)throw new Error("WebAssembly is not supported in this environment!");const B=()=>new DataView(C.exports.memory.buffer).getUint32(C.exports.STATE_SIZE,!0),h=s.dispatch((()=>I(this,void 0,void 0,(function*(){if(!S.has(A.name)){const I=k(A.data),i=WebAssembly.compile(I);S.set(A.name,i)}const I=yield S.get(A.name);C=yield WebAssembly.instantiate(I,{})})))),t=(A=null)=>{Q=!0,C.exports.Hash_Init(A)},o=A=>{if(!Q)throw new Error("update() called before init()");(A=>{let I=0;for(;I<A.length;){const i=A.subarray(I,I+a);I+=i.length,g.set(i),C.exports.Hash_Update(i.length)}})(n(A))},r=new Uint8Array(2*i),D=(A,I=null)=>{if(!Q)throw new Error("digest() called before init()");return Q=!1,C.exports.Hash_Final(I),"binary"===A?g.slice(0,i):E(r,g,i)},F=A=>"string"==typeof A?A.length<4096:A.byteLength<a;let f=F;switch(A.name){case"argon2":case"scrypt":f=()=>!0;break;case"blake2b":case"blake2s":f=(A,I)=>I<=512&&F(A);break;case"blake3":f=(A,I)=>0===I&&F(A);break;case"xxhash64":case"xxhash3":case"xxhash128":case"crc64":f=()=>!1}return yield(()=>I(this,void 0,void 0,(function*(){C||(yield h);const A=C.exports.Hash_GetBuffer(),I=C.exports.memory.buffer;g=new Uint8Array(I,A,a)})))(),{getMemory:()=>g,writeMemory:(A,I=0)=>{g.set(A,I)},getExports:()=>C.exports,setMemorySize:A=>{C.exports.Hash_SetMemorySize(A);const I=C.exports.Hash_GetBuffer(),i=C.exports.memory.buffer;g=new Uint8Array(i,I,A)},init:t,update:o,digest:D,save:()=>{if(!Q)throw new Error("save() can only be called after init() and before digest()");const I=C.exports.Hash_GetState(),i=B(),g=C.exports.memory.buffer,h=new Uint8Array(g,I,i),e=new Uint8Array(4+i);return w(e,A.hash),e.set(h,4),e},load:I=>{if(!(I instanceof Uint8Array))throw new Error("load() expects an Uint8Array generated by save()");const i=C.exports.Hash_GetState(),g=B(),h=4+g,w=C.exports.memory.buffer;if(I.length!==h)throw new Error(`Bad state length (expected ${h} bytes, got ${I.length})`);if(!function(A,I){if(A.length!==2*I.length)return!1;for(let i=0;i<I.length;i++){const C=i<<1;if(I[i]!==e(A.charCodeAt(C),A.charCodeAt(C+1)))return!1}return!0}(A.hash,I.subarray(0,4)))throw new Error("This state was written by an incompatible hash implementation");const t=I.subarray(4);new Uint8Array(w,i,g).set(t),Q=!0},calculate:(A,I=null,Q=null)=>{if(!f(A,I))return t(I),o(A),D("hex",Q);const B=n(A);return g.set(B),C.exports.Hash_Calculate(B.length,I,Q),E(r,g,i)},hashLength:i}}))}var c={name:"blake2b",data:"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",hash:"c6f286e6"};function G(A){return!Number.isInteger(A)||A<8||A>512||A%8!=0?new Error("Invalid variant! Valid values: 8, 16, ..., 512"):null}function p(A=512,I=null){if(G(A))return Promise.reject(G(A));let i=null,C=A;if(null!==I){if(i=n(I),i.length>64)return Promise.reject(new Error("Max key length is 64 bytes"));g=A,Q=i.length,C=g|Q<<16}var g,Q;const B=A/8;return U(c,B).then((A=>{C>512&&A.writeMemory(i),A.init(C);const I={init:C>512?()=>(A.writeMemory(i),A.init(C),I):()=>(A.init(C),I),update:i=>(A.update(i),I),digest:I=>A.digest(I),save:()=>A.save(),load:i=>(A.load(i),I),blockSize:128,digestSize:B};return I}))}new g;const y=new DataView(new ArrayBuffer(4));function J(A){return y.setInt32(0,A,!0),new Uint8Array(y.buffer)}function l(A,i,C){return I(this,void 0,void 0,(function*(){if(C<=64){const A=yield p(8*C);return A.update(J(C)),A.update(i),A.digest("binary")}const I=Math.ceil(C/32)-2,g=new Uint8Array(C);A.init(),A.update(J(C)),A.update(i);let Q=A.digest("binary");g.set(Q.subarray(0,32),0);for(let i=1;i<I;i++)A.init(),A.update(Q),Q=A.digest("binary"),g.set(Q.subarray(0,32),32*i);const B=C-32*I;let h;return 64===B?(h=A,h.init()):h=yield p(8*B),h.update(Q),Q=h.digest("binary"),g.set(Q.subarray(0,B),32*I),g}))}function u(A){return I(this,void 0,void 0,(function*(){var I;const{parallelism:i,iterations:g,hashLength:Q}=A,B=n(A.password),h=n(A.salt),e=function(A){switch(A){case"d":return 0;case"i":return 1;default:return 2}}(A.hashType),{memorySize:t}=A,o=n(null!==(I=A.secret)&&void 0!==I?I:""),[r,D]=yield Promise.all([U(C,1024),p(512)]);r.setMemorySize(1024*t+1024);const f=new Uint8Array(24),k=new DataView(f.buffer);k.setInt32(0,i,!0),k.setInt32(4,Q,!0),k.setInt32(8,t,!0),k.setInt32(12,g,!0),k.setInt32(16,19,!0),k.setInt32(20,e,!0),r.writeMemory(f,1024*t),D.init(),D.update(f),D.update(J(B.length)),D.update(B),D.update(J(h.length)),D.update(h),D.update(J(o.length)),D.update(o),D.update(J(0));const a=4*Math.floor(t/(4*i)),s=new Uint8Array(72),S=D.digest("binary");s.set(S);for(let A=0;A<i;A++){s.set(J(0),64),s.set(J(A),68);let I=A*a,i=yield l(D,s,1024);r.writeMemory(i,1024*I),I+=1,s.set(J(1),64),i=yield l(D,s,1024),r.writeMemory(i,1024*I)}const c=new Uint8Array(1024);w(c,r.calculate(new Uint8Array([]),t));const G=yield l(D,c,Q);if("hex"===A.outputType){return E(new Uint8Array(2*Q),G,Q)}return"encoded"===A.outputType?function(A,I,i){const C=[`m=${I.memorySize}`,`t=${I.iterations}`,`p=${I.parallelism}`].join(",");return`$argon2${I.hashType}$v=19$${C}$${F(A,!1)}$${F(i,!1)}`}(h,A,G):G}))}const H=A=>{var I;if(!A||"object"!=typeof A)throw new Error("Invalid options parameter. It requires an object.");if(!A.password)throw new Error("Password must be specified");if(A.password=n(A.password),A.password.length<1)throw new Error("Password must be specified");if(!A.salt)throw new Error("Salt must be specified");if(A.salt=n(A.salt),A.salt.length<8)throw new Error("Salt should be at least 8 bytes long");if(A.secret=n(null!==(I=A.secret)&&void 0!==I?I:""),!Number.isInteger(A.iterations)||A.iterations<1)throw new Error("Iterations should be a positive number");if(!Number.isInteger(A.parallelism)||A.parallelism<1)throw new Error("Parallelism should be a positive number");if(!Number.isInteger(A.hashLength)||A.hashLength<4)throw new Error("Hash length should be at least 4 bytes.");if(!Number.isInteger(A.memorySize))throw new Error("Memory size should be specified.");if(A.memorySize<8*A.parallelism)throw new Error("Memory size should be at least 8 * parallelism.");if(void 0===A.outputType&&(A.outputType="hex"),!["hex","binary","encoded"].includes(A.outputType))throw new Error(`Insupported output type ${A.outputType}. Valid values: ['hex', 'binary', 'encoded']`)};A.argon2Verify=function(A){return I(this,void 0,void 0,(function*(){(A=>{if(!A||"object"!=typeof A)throw new Error("Invalid options parameter. It requires an object.");if(void 0===A.hash||"string"!=typeof A.hash)throw new Error("Hash should be specified")})(A);const I=((A,I,i)=>{const C=I.match(/^\$argon2(id|i|d)\$v=([0-9]+)\$((?:[mtp]=[0-9]+,){2}[mtp]=[0-9]+)\$([A-Za-z0-9+/]+)\$([A-Za-z0-9+/]+)$/);if(!C)throw new Error("Invalid hash");const[,g,Q,B,h,e]=C;if("19"!==Q)throw new Error(`Unsupported version: ${Q}`);const w={},t={m:"memorySize",p:"parallelism",t:"iterations"};for(const A of B.split(",")){const[I,i]=A.split("=");w[t[I]]=Number(i)}return Object.assign(Object.assign({},w),{password:A,secret:i,hashType:g,salt:k(h),hashLength:f(e),outputType:"encoded"})})(A.password,A.hash,A.secret);H(I);const i=A.hash.lastIndexOf("$")+1;return(yield u(I)).substring(i)===A.hash.substring(i)}))},A.argon2d=function(A){return I(this,void 0,void 0,(function*(){return H(A),u(Object.assign(Object.assign({},A),{hashType:"d"}))}))},A.argon2i=function(A){return I(this,void 0,void 0,(function*(){return H(A),u(Object.assign(Object.assign({},A),{hashType:"i"}))}))},A.argon2id=function(A){return I(this,void 0,void 0,(function*(){return H(A),u(Object.assign(Object.assign({},A),{hashType:"id"}))}))}}));
@@ -0,0 +1,151 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
6
+ <title>The Paladin</title>
7
+ <style>
8
+ :root {
9
+ --bg:__C_BG__; --panel:__C_SURFACE__; --border:__C_PANEL__; --fg:__C_FG__;
10
+ --dim:__C_DIM__; --accent:__C_PRIMARY__; --sel:__C_PANEL__; --err:__C_ERROR__;
11
+ }
12
+ * { box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
13
+ body {
14
+ margin:0; background:var(--bg); color:var(--fg); min-height:100vh;
15
+ font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
16
+ }
17
+ #app { max-width:640px; margin:0 auto; padding:16px; }
18
+ h1 { color:var(--accent); font-size:1.3rem; margin:8px 0 20px; }
19
+ input {
20
+ width:100%; padding:14px; font-size:16px; color:var(--fg);
21
+ background:var(--panel); border:1px solid var(--border); border-radius:8px;
22
+ }
23
+ input:focus { outline:none; border-color:var(--accent); }
24
+ button {
25
+ width:100%; padding:14px; margin-top:12px; font-size:16px; font-weight:600;
26
+ color:var(--bg); background:var(--accent); border:none; border-radius:8px;
27
+ }
28
+ .hint { color:var(--dim); font-size:0.85rem; margin-top:14px; }
29
+ .err { color:var(--err); margin-top:12px; min-height:1.2em; }
30
+ .row {
31
+ padding:14px; margin-top:8px; background:var(--panel);
32
+ border:1px solid var(--border); border-radius:8px;
33
+ }
34
+ .row .name { font-weight:600; }
35
+ .row .user { color:var(--dim); font-size:0.9rem; }
36
+ .row .pw {
37
+ margin-top:8px; font-family:ui-monospace,monospace; font-size:1.05rem;
38
+ word-break:break-all; color:var(--accent); display:none;
39
+ }
40
+ .row.open .pw { display:block; }
41
+ .copied { color:var(--accent); font-size:0.8rem; margin-left:8px; }
42
+ mark { background:none; color:var(--accent); font-weight:600; }
43
+ #knight { text-align:center; margin:16px 0 4px; }
44
+ #knight svg { image-rendering:pixelated; }
45
+ </style>
46
+ </head>
47
+ <body>
48
+ <div id="app">
49
+ __KNIGHT__
50
+ <h1>The Paladin</h1>
51
+ <div id="lock">
52
+ <input id="master" type="password" placeholder="master password" autocomplete="off"
53
+ autocapitalize="off" autocorrect="off">
54
+ <button id="unlock">unlock</button>
55
+ <div class="err" id="err"></div>
56
+ <div class="hint">Decrypted here on your phone. Nothing is saved. Auto-locks after 5 minutes idle.</div>
57
+ </div>
58
+ <div id="vault" style="display:none">
59
+ <input id="search" placeholder="search" autocapitalize="off" autocorrect="off">
60
+ <div id="list"></div>
61
+ </div>
62
+ </div>
63
+
64
+ <script>/*__ARGON2__*/</script>
65
+ <script>
66
+ const SALT = b64("__SALT__"), BLOB = b64("__BLOB__");
67
+ const NONCE_LEN = __NONCE_LEN__, TIME = __TIME__, MEM = __MEM_KIB__, PAR = __PAR__, KEYLEN = __KEYLEN__;
68
+ let ENTRIES = null, IDLE = null;
69
+
70
+ function b64(s){ return Uint8Array.from(atob(s), c => c.charCodeAt(0)); }
71
+
72
+ async function unlock() {
73
+ const err = document.getElementById("err");
74
+ const pw = document.getElementById("master").value;
75
+ if (!pw) return;
76
+ err.textContent = "unlocking…";
77
+ try {
78
+ const keyBytes = await hashwasm.argon2id({
79
+ password: pw, salt: SALT, parallelism: PAR, iterations: TIME,
80
+ memorySize: MEM, hashLength: KEYLEN, outputType: "binary",
81
+ });
82
+ const key = await crypto.subtle.importKey("raw", keyBytes, "AES-GCM", false, ["decrypt"]);
83
+ const nonce = BLOB.slice(0, NONCE_LEN), ct = BLOB.slice(NONCE_LEN);
84
+ const pt = await crypto.subtle.decrypt({ name:"AES-GCM", iv:nonce }, key, ct);
85
+ ENTRIES = JSON.parse(new TextDecoder().decode(pt));
86
+ } catch (e) {
87
+ err.textContent = "wrong master password";
88
+ return;
89
+ }
90
+ document.getElementById("master").value = "";
91
+ document.getElementById("lock").style.display = "none";
92
+ document.getElementById("vault").style.display = "block";
93
+ render("");
94
+ resetIdle();
95
+ }
96
+
97
+ function esc(s){ return s.replace(/[&<>]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;"}[c])); }
98
+ function hl(s, words){
99
+ let out = esc(s);
100
+ for (const w of words) if (w) out = out.replace(
101
+ new RegExp("("+w.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+")","ig"), "<mark>$1</mark>");
102
+ return out;
103
+ }
104
+
105
+ function render(q) {
106
+ const words = q.toLowerCase().split(/\s+/).filter(Boolean);
107
+ const list = document.getElementById("list");
108
+ list.innerHTML = "";
109
+ for (const name of Object.keys(ENTRIES).sort()) {
110
+ const e = ENTRIES[name];
111
+ const hay = (name+" "+e.user+" "+(e.notes||"")+" "+e.pw).toLowerCase();
112
+ if (!words.every(w => hay.includes(w))) continue;
113
+ const div = document.createElement("div");
114
+ div.className = "row";
115
+ div.innerHTML = `<div class="name">${hl(name,words)}</div>`
116
+ + `<div class="user">${hl(e.user,words)}</div>`
117
+ + `<div class="pw"></div>`;
118
+ div.querySelector(".pw").textContent = e.pw; // never highlight the password
119
+ div.onclick = () => reveal(div, e.pw);
120
+ list.appendChild(div);
121
+ }
122
+ }
123
+
124
+ async function reveal(div, pw) {
125
+ div.classList.toggle("open");
126
+ resetIdle();
127
+ try {
128
+ await navigator.clipboard.writeText(pw);
129
+ const tag = document.createElement("span");
130
+ tag.className = "copied"; tag.textContent = "copied";
131
+ div.querySelector(".name").appendChild(tag);
132
+ setTimeout(() => tag.remove(), 1500);
133
+ } catch (e) { /* reveal is the fallback */ }
134
+ }
135
+
136
+ function lock() {
137
+ ENTRIES = null;
138
+ document.getElementById("vault").style.display = "none";
139
+ document.getElementById("list").innerHTML = "";
140
+ document.getElementById("lock").style.display = "block";
141
+ document.getElementById("err").textContent = "";
142
+ }
143
+ function resetIdle(){ clearTimeout(IDLE); IDLE = setTimeout(lock, 5*60*1000); }
144
+
145
+ document.getElementById("unlock").onclick = unlock;
146
+ document.getElementById("master").addEventListener("keydown", e => { if (e.key==="Enter") unlock(); });
147
+ document.getElementById("search").addEventListener("input", e => { render(e.target.value); resetIdle(); });
148
+ ["touchstart","keydown"].forEach(ev => document.addEventListener(ev, () => { if (ENTRIES) resetIdle(); }));
149
+ </script>
150
+ </body>
151
+ </html>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "basic-password-manager"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "The Paladin — a local encrypted password manager that lives in your terminal"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -13,11 +13,15 @@ dependencies = [
13
13
  "argon2-cffi",
14
14
  "cryptography",
15
15
  "textual",
16
+ "qrcode",
16
17
  ]
17
18
 
18
19
  [tool.setuptools]
19
20
  packages = ["pw_manager"]
20
21
 
22
+ [tool.setuptools.package-data]
23
+ pw_manager = ["web/*"]
24
+
21
25
  [project.scripts]
22
26
  paladin = "pw_manager.cli:main"
23
27