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.
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/PKG-INFO +63 -33
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/README.md +61 -32
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/PKG-INFO +63 -33
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/SOURCES.txt +4 -1
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/requires.txt +1 -0
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/cli.py +11 -0
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/logo.py +23 -0
- basic_password_manager-0.4.0/pw_manager/mobile.py +248 -0
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/tui.py +10 -0
- basic_password_manager-0.4.0/pw_manager/web/argon2.umd.min.js +7 -0
- basic_password_manager-0.4.0/pw_manager/web/page.html +151 -0
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pyproject.toml +5 -1
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/LICENSE +0 -0
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/dependency_links.txt +0 -0
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/entry_points.txt +0 -0
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/basic_password_manager.egg-info/top_level.txt +0 -0
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/__init__.py +0 -0
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/crypto.py +0 -0
- {basic_password_manager-0.3.0 → basic_password_manager-0.4.0}/pw_manager/vault.py +0 -0
- {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
|
+
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,
|
|
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
|
|
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
|

|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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
|

|
|
67
69
|
|
|
68
|
-
The rarer stuff lives in the command palette — `
|
|
69
|
-
|
|
70
|
-
|
|
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
|

|
|
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
|

|
|
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
|
|
90
|
-
| `paladin add
|
|
91
|
-
| `paladin get
|
|
92
|
-
| `paladin edit
|
|
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
|
|
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:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
3.
|
|
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
|
-
|
|
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,
|
|
122
|
-
|
|
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.
|
|
199
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|

|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|

|
|
52
53
|
|
|
53
|
-
The rarer stuff lives in the command palette — `
|
|
54
|
-
|
|
55
|
-
|
|
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
|

|
|
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
|

|
|
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
|
|
75
|
-
| `paladin add
|
|
76
|
-
| `paladin get
|
|
77
|
-
| `paladin edit
|
|
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
|
|
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:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
3.
|
|
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
|
-
|
|
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,
|
|
107
|
-
|
|
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.
|
|
184
|
-
|
|
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
|
|
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
|
+
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,
|
|
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
|
|
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
|

|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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
|

|
|
67
69
|
|
|
68
|
-
The rarer stuff lives in the command palette — `
|
|
69
|
-
|
|
70
|
-
|
|
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
|

|
|
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
|

|
|
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
|
|
90
|
-
| `paladin add
|
|
91
|
-
| `paladin get
|
|
92
|
-
| `paladin edit
|
|
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
|
|
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:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
3.
|
|
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
|
-
|
|
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,
|
|
122
|
-
|
|
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.
|
|
199
|
-
|
|
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
|
|
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
|
|
|
@@ -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 => ({"&":"&","<":"<",">":">"}[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.
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|