basic-password-manager 0.4.5__tar.gz → 0.4.7__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.4.5 → basic_password_manager-0.4.7}/PKG-INFO +30 -10
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/README.md +29 -9
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/basic_password_manager.egg-info/PKG-INFO +30 -10
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pw_manager/cli.py +99 -13
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pw_manager/mobile.py +113 -10
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pw_manager/tui.py +17 -4
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pyproject.toml +1 -1
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/LICENSE +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/basic_password_manager.egg-info/SOURCES.txt +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/basic_password_manager.egg-info/dependency_links.txt +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/basic_password_manager.egg-info/entry_points.txt +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/basic_password_manager.egg-info/requires.txt +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/basic_password_manager.egg-info/top_level.txt +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pw_manager/__init__.py +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pw_manager/crypto.py +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pw_manager/logo.py +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pw_manager/vault.py +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pw_manager/web/argon2.umd.min.js +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pw_manager/web/page.html +0 -0
- {basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-password-manager
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.7
|
|
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
|
|
@@ -102,6 +102,7 @@ Same vault, no interface, for when you just want the thing:
|
|
|
102
102
|
| `paladin gen -l 32` | just print a random password |
|
|
103
103
|
| `paladin import passwords.csv` | import a browser CSV export (see below) |
|
|
104
104
|
| `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
|
|
105
|
+
| `paladin mobile -b` | same, but keeps running after you close the terminal |
|
|
105
106
|
| `paladin about` | meet the knight |
|
|
106
107
|
|
|
107
108
|
The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
|
|
@@ -154,11 +155,25 @@ use (reverse proxy, named tunnel — `PALADIN_MOBILE_PORT` changes the port).
|
|
|
154
155
|
From then on plain `paladin mobile` serves at your subdomain, every time, so
|
|
155
156
|
a phone bookmark keeps working.
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
Every new session opens at *your* address instead of a random one, and the
|
|
159
|
+
secret path at the end of the URL still expires every 30 days (your bookmark
|
|
160
|
+
needs refreshing then, on purpose).
|
|
161
|
+
|
|
162
|
+
### Keeping it up after you close the terminal
|
|
163
|
+
|
|
164
|
+
By default a session ends when you press Ctrl+C — fine for a quick lookup,
|
|
165
|
+
useless if you want the bookmark to work while you're out. So:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
paladin mobile -b # background: survives closing the terminal
|
|
169
|
+
paladin mobile --status # what's running, and when the link expires
|
|
170
|
+
paladin mobile --stop # end it now
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A background session keeps serving until you stop it or the link hits its
|
|
174
|
+
30-day expiry, at which point it shuts itself down rather than lingering
|
|
175
|
+
forever. What's exposed the whole time is the same encrypted blob as always
|
|
176
|
+
— useless without the master password — behind a random 128-bit path.
|
|
162
177
|
|
|
163
178
|
The one thing crossing the internet is the encrypted blob, which is useless
|
|
164
179
|
without the master password that never leaves your phone. The tunnel provider
|
|
@@ -211,8 +226,11 @@ Both are thin skins over the two modules above. The TUI (built with
|
|
|
211
226
|
[Textual](https://textual.textualize.io)) holds the decrypted entries in
|
|
212
227
|
memory for the session; the CLI re-derives the key per command.
|
|
213
228
|
|
|
214
|
-
Passwords go to the clipboard (`wl-copy`/`xclip`/`xsel`,
|
|
215
|
-
rather than the screen, so they don't sit in your terminal
|
|
229
|
+
Passwords go to the clipboard (`wl-copy`/`xclip`/`xsel`/`pbcopy`/`clip`,
|
|
230
|
+
whichever exists) rather than the screen, so they don't sit in your terminal
|
|
231
|
+
scrollback — and they're wiped from it 45 seconds later by a detached helper
|
|
232
|
+
(the CLI exits immediately, so an in-process timer would never fire). If
|
|
233
|
+
you've copied something else in the meantime, yours is left alone. One
|
|
216
234
|
deliberate asymmetry: CLI `find` does *not* search passwords, because a CLI
|
|
217
235
|
argument lands in your shell history forever — the TUI search box does,
|
|
218
236
|
because nothing you type there is logged anywhere.
|
|
@@ -283,8 +301,10 @@ accounts from 2013. `paladin rm <name>` the corpses, or prune the CSV first.
|
|
|
283
301
|
|
|
284
302
|
I'd rather you know these than find them:
|
|
285
303
|
|
|
286
|
-
-
|
|
287
|
-
|
|
304
|
+
- The 45-second clipboard window is still a window: anything watching your
|
|
305
|
+
clipboard in that time sees the password. Set
|
|
306
|
+
`PALADIN_CLIPBOARD_SECONDS` lower if that bothers you (or `0` to keep the
|
|
307
|
+
old forever-behaviour).
|
|
288
308
|
- While a command runs, decrypted data briefly exists in process memory.
|
|
289
309
|
Malware already on your machine could read it — true of every password
|
|
290
310
|
manager ever made; the vault protects the file at rest, not a compromised
|
|
@@ -86,6 +86,7 @@ Same vault, no interface, for when you just want the thing:
|
|
|
86
86
|
| `paladin gen -l 32` | just print a random password |
|
|
87
87
|
| `paladin import passwords.csv` | import a browser CSV export (see below) |
|
|
88
88
|
| `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
|
|
89
|
+
| `paladin mobile -b` | same, but keeps running after you close the terminal |
|
|
89
90
|
| `paladin about` | meet the knight |
|
|
90
91
|
|
|
91
92
|
The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
|
|
@@ -138,11 +139,25 @@ use (reverse proxy, named tunnel — `PALADIN_MOBILE_PORT` changes the port).
|
|
|
138
139
|
From then on plain `paladin mobile` serves at your subdomain, every time, so
|
|
139
140
|
a phone bookmark keeps working.
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
Every new session opens at *your* address instead of a random one, and the
|
|
143
|
+
secret path at the end of the URL still expires every 30 days (your bookmark
|
|
144
|
+
needs refreshing then, on purpose).
|
|
145
|
+
|
|
146
|
+
### Keeping it up after you close the terminal
|
|
147
|
+
|
|
148
|
+
By default a session ends when you press Ctrl+C — fine for a quick lookup,
|
|
149
|
+
useless if you want the bookmark to work while you're out. So:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
paladin mobile -b # background: survives closing the terminal
|
|
153
|
+
paladin mobile --status # what's running, and when the link expires
|
|
154
|
+
paladin mobile --stop # end it now
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A background session keeps serving until you stop it or the link hits its
|
|
158
|
+
30-day expiry, at which point it shuts itself down rather than lingering
|
|
159
|
+
forever. What's exposed the whole time is the same encrypted blob as always
|
|
160
|
+
— useless without the master password — behind a random 128-bit path.
|
|
146
161
|
|
|
147
162
|
The one thing crossing the internet is the encrypted blob, which is useless
|
|
148
163
|
without the master password that never leaves your phone. The tunnel provider
|
|
@@ -195,8 +210,11 @@ Both are thin skins over the two modules above. The TUI (built with
|
|
|
195
210
|
[Textual](https://textual.textualize.io)) holds the decrypted entries in
|
|
196
211
|
memory for the session; the CLI re-derives the key per command.
|
|
197
212
|
|
|
198
|
-
Passwords go to the clipboard (`wl-copy`/`xclip`/`xsel`,
|
|
199
|
-
rather than the screen, so they don't sit in your terminal
|
|
213
|
+
Passwords go to the clipboard (`wl-copy`/`xclip`/`xsel`/`pbcopy`/`clip`,
|
|
214
|
+
whichever exists) rather than the screen, so they don't sit in your terminal
|
|
215
|
+
scrollback — and they're wiped from it 45 seconds later by a detached helper
|
|
216
|
+
(the CLI exits immediately, so an in-process timer would never fire). If
|
|
217
|
+
you've copied something else in the meantime, yours is left alone. One
|
|
200
218
|
deliberate asymmetry: CLI `find` does *not* search passwords, because a CLI
|
|
201
219
|
argument lands in your shell history forever — the TUI search box does,
|
|
202
220
|
because nothing you type there is logged anywhere.
|
|
@@ -267,8 +285,10 @@ accounts from 2013. `paladin rm <name>` the corpses, or prune the CSV first.
|
|
|
267
285
|
|
|
268
286
|
I'd rather you know these than find them:
|
|
269
287
|
|
|
270
|
-
-
|
|
271
|
-
|
|
288
|
+
- The 45-second clipboard window is still a window: anything watching your
|
|
289
|
+
clipboard in that time sees the password. Set
|
|
290
|
+
`PALADIN_CLIPBOARD_SECONDS` lower if that bothers you (or `0` to keep the
|
|
291
|
+
old forever-behaviour).
|
|
272
292
|
- While a command runs, decrypted data briefly exists in process memory.
|
|
273
293
|
Malware already on your machine could read it — true of every password
|
|
274
294
|
manager ever made; the vault protects the file at rest, not a compromised
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-password-manager
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.7
|
|
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
|
|
@@ -102,6 +102,7 @@ Same vault, no interface, for when you just want the thing:
|
|
|
102
102
|
| `paladin gen -l 32` | just print a random password |
|
|
103
103
|
| `paladin import passwords.csv` | import a browser CSV export (see below) |
|
|
104
104
|
| `paladin mobile` | open your vault on your phone (QR + HTTPS tunnel) |
|
|
105
|
+
| `paladin mobile -b` | same, but keeps running after you close the terminal |
|
|
105
106
|
| `paladin about` | meet the knight |
|
|
106
107
|
|
|
107
108
|
The vault lives at `~/.local/share/pw-manager/vault`. Point the `PW_VAULT`
|
|
@@ -154,11 +155,25 @@ use (reverse proxy, named tunnel — `PALADIN_MOBILE_PORT` changes the port).
|
|
|
154
155
|
From then on plain `paladin mobile` serves at your subdomain, every time, so
|
|
155
156
|
a phone bookmark keeps working.
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
Every new session opens at *your* address instead of a random one, and the
|
|
159
|
+
secret path at the end of the URL still expires every 30 days (your bookmark
|
|
160
|
+
needs refreshing then, on purpose).
|
|
161
|
+
|
|
162
|
+
### Keeping it up after you close the terminal
|
|
163
|
+
|
|
164
|
+
By default a session ends when you press Ctrl+C — fine for a quick lookup,
|
|
165
|
+
useless if you want the bookmark to work while you're out. So:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
paladin mobile -b # background: survives closing the terminal
|
|
169
|
+
paladin mobile --status # what's running, and when the link expires
|
|
170
|
+
paladin mobile --stop # end it now
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A background session keeps serving until you stop it or the link hits its
|
|
174
|
+
30-day expiry, at which point it shuts itself down rather than lingering
|
|
175
|
+
forever. What's exposed the whole time is the same encrypted blob as always
|
|
176
|
+
— useless without the master password — behind a random 128-bit path.
|
|
162
177
|
|
|
163
178
|
The one thing crossing the internet is the encrypted blob, which is useless
|
|
164
179
|
without the master password that never leaves your phone. The tunnel provider
|
|
@@ -211,8 +226,11 @@ Both are thin skins over the two modules above. The TUI (built with
|
|
|
211
226
|
[Textual](https://textual.textualize.io)) holds the decrypted entries in
|
|
212
227
|
memory for the session; the CLI re-derives the key per command.
|
|
213
228
|
|
|
214
|
-
Passwords go to the clipboard (`wl-copy`/`xclip`/`xsel`,
|
|
215
|
-
rather than the screen, so they don't sit in your terminal
|
|
229
|
+
Passwords go to the clipboard (`wl-copy`/`xclip`/`xsel`/`pbcopy`/`clip`,
|
|
230
|
+
whichever exists) rather than the screen, so they don't sit in your terminal
|
|
231
|
+
scrollback — and they're wiped from it 45 seconds later by a detached helper
|
|
232
|
+
(the CLI exits immediately, so an in-process timer would never fire). If
|
|
233
|
+
you've copied something else in the meantime, yours is left alone. One
|
|
216
234
|
deliberate asymmetry: CLI `find` does *not* search passwords, because a CLI
|
|
217
235
|
argument lands in your shell history forever — the TUI search box does,
|
|
218
236
|
because nothing you type there is logged anywhere.
|
|
@@ -283,8 +301,10 @@ accounts from 2013. `paladin rm <name>` the corpses, or prune the CSV first.
|
|
|
283
301
|
|
|
284
302
|
I'd rather you know these than find them:
|
|
285
303
|
|
|
286
|
-
-
|
|
287
|
-
|
|
304
|
+
- The 45-second clipboard window is still a window: anything watching your
|
|
305
|
+
clipboard in that time sees the password. Set
|
|
306
|
+
`PALADIN_CLIPBOARD_SECONDS` lower if that bothers you (or `0` to keep the
|
|
307
|
+
old forever-behaviour).
|
|
288
308
|
- While a command runs, decrypted data briefly exists in process memory.
|
|
289
309
|
Malware already on your machine could read it — true of every password
|
|
290
310
|
manager ever made; the vault protects the file at rest, not a compromised
|
|
@@ -23,23 +23,84 @@ def generate(length: int = 20) -> str:
|
|
|
23
23
|
return "".join(secrets.choice(alphabet) for _ in range(length))
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
# seconds a copied password lingers before it's wiped; 0 disables
|
|
27
|
+
CLIPBOARD_TTL = int(os.environ.get("PALADIN_CLIPBOARD_SECONDS", "45"))
|
|
28
|
+
|
|
29
|
+
# linux needs a tool installed; macOS (pbcopy) and Windows (clip) ship theirs
|
|
30
|
+
_COPY_CMDS = (
|
|
31
|
+
["wl-copy"],
|
|
32
|
+
["xclip", "-selection", "clipboard"],
|
|
33
|
+
["xsel", "-b"],
|
|
34
|
+
["pbcopy"],
|
|
35
|
+
["clip"],
|
|
36
|
+
["clip.exe"], # windows tool as seen from inside WSL
|
|
37
|
+
)
|
|
38
|
+
_PASTE_CMDS = (
|
|
39
|
+
["wl-paste", "-n"],
|
|
40
|
+
["xclip", "-selection", "clipboard", "-o"],
|
|
41
|
+
["xsel", "-b"],
|
|
42
|
+
["pbpaste"],
|
|
43
|
+
["powershell.exe", "-noprofile", "-command", "Get-Clipboard"],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def to_clipboard(text: str, auto_clear: bool = True) -> bool:
|
|
48
|
+
for cmd in _COPY_CMDS:
|
|
37
49
|
if shutil.which(cmd[0]):
|
|
38
50
|
subprocess.run(cmd, input=text.encode(), check=True)
|
|
51
|
+
if auto_clear and text and CLIPBOARD_TTL > 0:
|
|
52
|
+
_clear_clipboard_later(text)
|
|
39
53
|
return True
|
|
40
54
|
return False
|
|
41
55
|
|
|
42
56
|
|
|
57
|
+
def from_clipboard() -> str | None:
|
|
58
|
+
"""Current clipboard contents, or None if we can't read it."""
|
|
59
|
+
for cmd in _PASTE_CMDS:
|
|
60
|
+
if shutil.which(cmd[0]):
|
|
61
|
+
try:
|
|
62
|
+
out = subprocess.run(cmd, capture_output=True, check=True).stdout
|
|
63
|
+
except subprocess.CalledProcessError:
|
|
64
|
+
return None
|
|
65
|
+
return out.decode(errors="replace").rstrip("\r\n")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _clear_clipboard_later(text: str) -> None:
|
|
70
|
+
"""Hand the password to a detached child that wipes it later — the CLI
|
|
71
|
+
exits immediately after copying, so an in-process timer would never fire."""
|
|
72
|
+
try:
|
|
73
|
+
proc = subprocess.Popen(
|
|
74
|
+
[sys.executable, "-c", "from pw_manager.cli import _clear_worker; _clear_worker()"],
|
|
75
|
+
stdin=subprocess.PIPE,
|
|
76
|
+
stdout=subprocess.DEVNULL,
|
|
77
|
+
stderr=subprocess.DEVNULL,
|
|
78
|
+
start_new_session=True,
|
|
79
|
+
)
|
|
80
|
+
proc.stdin.write(text.encode()) # via stdin, never argv (argv shows in `ps`)
|
|
81
|
+
proc.stdin.close()
|
|
82
|
+
except OSError:
|
|
83
|
+
pass # clipboard hygiene is best-effort; never break the copy itself
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _clear_worker() -> None:
|
|
87
|
+
"""Child: wait out the TTL, then clear the clipboard only if it still holds
|
|
88
|
+
the password we put there — never clobber something the user copied since."""
|
|
89
|
+
import time
|
|
90
|
+
|
|
91
|
+
copied = sys.stdin.buffer.read().decode()
|
|
92
|
+
time.sleep(CLIPBOARD_TTL)
|
|
93
|
+
current = from_clipboard()
|
|
94
|
+
if current is None or current == copied:
|
|
95
|
+
to_clipboard("", auto_clear=False) # no recursion
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def copied_msg() -> str:
|
|
99
|
+
if CLIPBOARD_TTL > 0:
|
|
100
|
+
return f"password copied to clipboard (clears in {CLIPBOARD_TTL}s)"
|
|
101
|
+
return "password copied to clipboard"
|
|
102
|
+
|
|
103
|
+
|
|
43
104
|
def ask_master(confirm: bool = False) -> str:
|
|
44
105
|
pw = getpass.getpass("master password: ")
|
|
45
106
|
if confirm:
|
|
@@ -64,9 +125,28 @@ def cmd_init(args):
|
|
|
64
125
|
|
|
65
126
|
|
|
66
127
|
def cmd_mobile(args):
|
|
128
|
+
import time
|
|
129
|
+
|
|
67
130
|
from . import mobile
|
|
68
131
|
|
|
69
132
|
try:
|
|
133
|
+
if args.stop:
|
|
134
|
+
print("background session stopped" if mobile.stop_daemon() else "nothing running")
|
|
135
|
+
return
|
|
136
|
+
if args.status:
|
|
137
|
+
state = mobile.read_state()
|
|
138
|
+
if not state:
|
|
139
|
+
print("no background session running")
|
|
140
|
+
return
|
|
141
|
+
left = (state["expires"] - time.time()) / 86400
|
|
142
|
+
print(f"serving {state['url']}\nlink expires in {left:.1f} days (pid {state['pid']})")
|
|
143
|
+
return
|
|
144
|
+
if args.background:
|
|
145
|
+
url = mobile.start_daemon(VAULT_PATH, stable_url=args.url)
|
|
146
|
+
mobile.print_qr(url)
|
|
147
|
+
print(" running in the background — it survives closing this terminal.")
|
|
148
|
+
print(" 'paladin mobile --status' to check, '--stop' to end it.\n")
|
|
149
|
+
return
|
|
70
150
|
mobile.run(VAULT_PATH, stable_url=args.url)
|
|
71
151
|
except mobile.MobileError as e:
|
|
72
152
|
sys.exit(str(e))
|
|
@@ -103,7 +183,7 @@ def cmd_add(args):
|
|
|
103
183
|
vault.save(VAULT_PATH, master, entries)
|
|
104
184
|
print(f"stored '{args.name}'")
|
|
105
185
|
if args.gen and to_clipboard(pw):
|
|
106
|
-
print(
|
|
186
|
+
print(copied_msg())
|
|
107
187
|
|
|
108
188
|
|
|
109
189
|
def cmd_get(args):
|
|
@@ -115,7 +195,7 @@ def cmd_get(args):
|
|
|
115
195
|
if e.get("notes"):
|
|
116
196
|
print(f"notes: {e['notes']}")
|
|
117
197
|
if to_clipboard(e["pw"]):
|
|
118
|
-
print(
|
|
198
|
+
print(copied_msg())
|
|
119
199
|
else:
|
|
120
200
|
# no clipboard tool found; showing it is the only option left
|
|
121
201
|
print(f"password: {e['pw']}")
|
|
@@ -237,6 +317,12 @@ def main():
|
|
|
237
317
|
i.add_argument("csv_file")
|
|
238
318
|
m = sub.add_parser("mobile", help="open your vault on your phone over an HTTPS tunnel")
|
|
239
319
|
m.add_argument("--url", help="serve at your own https subdomain instead of a quick tunnel")
|
|
320
|
+
m.add_argument(
|
|
321
|
+
"-b", "--background", action="store_true",
|
|
322
|
+
help="keep serving after this terminal closes (until the link expires)",
|
|
323
|
+
)
|
|
324
|
+
m.add_argument("--status", action="store_true", help="is a background session running?")
|
|
325
|
+
m.add_argument("--stop", action="store_true", help="stop the background session")
|
|
240
326
|
sub.add_parser("about", help="meet the knight")
|
|
241
327
|
|
|
242
328
|
args = p.parse_args()
|
|
@@ -9,11 +9,13 @@ and the network only ever carry ciphertext.
|
|
|
9
9
|
import base64
|
|
10
10
|
import hashlib
|
|
11
11
|
import http.server
|
|
12
|
+
import json
|
|
12
13
|
import os
|
|
13
14
|
import platform
|
|
14
15
|
import re
|
|
15
16
|
import secrets
|
|
16
17
|
import shutil
|
|
18
|
+
import signal
|
|
17
19
|
import stat
|
|
18
20
|
import subprocess
|
|
19
21
|
import sys
|
|
@@ -113,6 +115,81 @@ def _load_token() -> str:
|
|
|
113
115
|
return token
|
|
114
116
|
|
|
115
117
|
|
|
118
|
+
def token_expires_at() -> float:
|
|
119
|
+
"""Unix time when the current link stops working."""
|
|
120
|
+
try:
|
|
121
|
+
created, _ = open(os.path.join(_data_dir(), "mobile-token")).read().split(":", 1)
|
|
122
|
+
return float(created) + TOKEN_TTL
|
|
123
|
+
except (OSError, ValueError):
|
|
124
|
+
return time.time() + TOKEN_TTL
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _state_path() -> str:
|
|
128
|
+
return os.path.join(_data_dir(), "mobile-session.json")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def read_state() -> dict | None:
|
|
132
|
+
"""The running background session, or None if there isn't a live one."""
|
|
133
|
+
try:
|
|
134
|
+
with open(_state_path()) as f:
|
|
135
|
+
state = json.load(f)
|
|
136
|
+
os.kill(state["pid"], 0) # raises if that process is gone
|
|
137
|
+
return state
|
|
138
|
+
except (OSError, ValueError, KeyError):
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def stop_daemon() -> bool:
|
|
143
|
+
"""Stop a background session. True if one was running."""
|
|
144
|
+
state = read_state()
|
|
145
|
+
if not state:
|
|
146
|
+
return False
|
|
147
|
+
try:
|
|
148
|
+
os.kill(state["pid"], signal.SIGTERM)
|
|
149
|
+
except OSError:
|
|
150
|
+
pass
|
|
151
|
+
try:
|
|
152
|
+
os.remove(_state_path())
|
|
153
|
+
except OSError:
|
|
154
|
+
pass
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def start_daemon(vault_path: str, stable_url: str | None = None, port: int = 0) -> str:
|
|
159
|
+
"""Re-launch ourselves detached so the session outlives this shell.
|
|
160
|
+
Returns the URL the background session is serving."""
|
|
161
|
+
if read_state():
|
|
162
|
+
raise MobileError("a background session is already running — 'paladin mobile --stop' first")
|
|
163
|
+
if not os.path.exists(vault_path):
|
|
164
|
+
raise MobileError(f"no vault at {vault_path} — run 'paladin init' first")
|
|
165
|
+
|
|
166
|
+
log = os.path.join(_data_dir(), "mobile.log")
|
|
167
|
+
cmd = [sys.executable, "-m", "pw_manager.cli", "mobile"]
|
|
168
|
+
if stable_url:
|
|
169
|
+
cmd += ["--url", stable_url]
|
|
170
|
+
env = {**os.environ, "PALADIN_MOBILE_DAEMON_CHILD": "1"}
|
|
171
|
+
with open(log, "ab") as logfile:
|
|
172
|
+
proc = subprocess.Popen(
|
|
173
|
+
cmd,
|
|
174
|
+
stdout=logfile,
|
|
175
|
+
stderr=logfile,
|
|
176
|
+
stdin=subprocess.DEVNULL,
|
|
177
|
+
start_new_session=True, # survives this shell closing
|
|
178
|
+
env=env,
|
|
179
|
+
)
|
|
180
|
+
# wait for the child to publish its URL (tunnel start can take seconds)
|
|
181
|
+
deadline = time.time() + 45
|
|
182
|
+
while time.time() < deadline:
|
|
183
|
+
state = read_state()
|
|
184
|
+
if state and state.get("pid") == proc.pid:
|
|
185
|
+
return state["url"]
|
|
186
|
+
if proc.poll() is not None:
|
|
187
|
+
raise MobileError(f"background session failed to start — see {log}")
|
|
188
|
+
time.sleep(0.3)
|
|
189
|
+
proc.terminate()
|
|
190
|
+
raise MobileError(f"background session didn't come up in time — see {log}")
|
|
191
|
+
|
|
192
|
+
|
|
116
193
|
def _theme_colors() -> dict:
|
|
117
194
|
"""Resolve the theme saved by the TUI to a full color palette. The page
|
|
118
195
|
is baked with whatever theme is current when the QR is generated."""
|
|
@@ -251,13 +328,7 @@ def stop_session(server, proc) -> None:
|
|
|
251
328
|
pass
|
|
252
329
|
|
|
253
330
|
|
|
254
|
-
def
|
|
255
|
-
def fetched():
|
|
256
|
-
print(f" vault page served to a device at {time.strftime('%H:%M:%S')}")
|
|
257
|
-
|
|
258
|
-
print("\n creating your secure link…", flush=True)
|
|
259
|
-
server, proc, url = start_session(vault_path, stable_url, port, on_fetch=fetched)
|
|
260
|
-
|
|
331
|
+
def print_qr(url: str) -> None:
|
|
261
332
|
import qrcode
|
|
262
333
|
|
|
263
334
|
qr = qrcode.QRCode(border=1)
|
|
@@ -265,13 +336,45 @@ def run(vault_path: str, stable_url: str | None = None, port: int = 0) -> None:
|
|
|
265
336
|
print("\n scan with your phone camera:\n")
|
|
266
337
|
qr.print_ascii(invert=True)
|
|
267
338
|
print(f"\n {url}\n")
|
|
268
|
-
|
|
269
|
-
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def run(vault_path: str, stable_url: str | None = None, port: int = 0) -> None:
|
|
342
|
+
is_child = os.environ.get("PALADIN_MOBILE_DAEMON_CHILD") == "1"
|
|
343
|
+
|
|
344
|
+
def fetched():
|
|
345
|
+
print(f" vault page served to a device at {time.strftime('%H:%M:%S')}", flush=True)
|
|
346
|
+
|
|
347
|
+
print("\n creating your secure link…", flush=True)
|
|
348
|
+
server, proc, url = start_session(vault_path, stable_url, port, on_fetch=fetched)
|
|
349
|
+
|
|
350
|
+
if proc is None: # stable mode: serving your own address, no quick tunnel
|
|
351
|
+
print(
|
|
352
|
+
f" serving your configured address on 127.0.0.1:{server.server_address[1]}"
|
|
353
|
+
" — your tunnel/proxy must forward to this port"
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
print(" using a temporary tunnel (set PALADIN_MOBILE_URL for your own subdomain)")
|
|
357
|
+
|
|
358
|
+
expires = token_expires_at()
|
|
359
|
+
if is_child: # background session: publish state so --status/--stop can find us
|
|
360
|
+
with open(_state_path(), "w") as f:
|
|
361
|
+
json.dump({"pid": os.getpid(), "url": url, "expires": expires}, f)
|
|
362
|
+
signal.signal(signal.SIGTERM, lambda *a: (_ for _ in ()).throw(KeyboardInterrupt))
|
|
363
|
+
else:
|
|
364
|
+
print_qr(url)
|
|
365
|
+
print(" the master password is typed on your phone; only ciphertext leaves this machine.")
|
|
366
|
+
print(" every page load is logged below — Ctrl+C to stop the session.\n")
|
|
270
367
|
|
|
271
368
|
try:
|
|
272
|
-
while
|
|
369
|
+
while time.time() < expires: # the link's 30 days, enforced for real
|
|
273
370
|
time.sleep(1)
|
|
371
|
+
print("\nlink expired after 30 days — run 'paladin mobile' again for a fresh one.")
|
|
274
372
|
except KeyboardInterrupt:
|
|
275
373
|
print("\nsession ended.")
|
|
276
374
|
finally:
|
|
277
375
|
stop_session(server, proc)
|
|
376
|
+
if is_child:
|
|
377
|
+
try:
|
|
378
|
+
os.remove(_state_path())
|
|
379
|
+
except OSError:
|
|
380
|
+
pass
|
|
@@ -11,7 +11,7 @@ import time
|
|
|
11
11
|
|
|
12
12
|
from rich.style import Style
|
|
13
13
|
from rich.text import Text
|
|
14
|
-
from textual.app import App, ComposeResult
|
|
14
|
+
from textual.app import App, ComposeResult, SystemCommand
|
|
15
15
|
from textual.binding import Binding
|
|
16
16
|
from textual.command import DiscoveryHit, Hit, Hits, Provider
|
|
17
17
|
from textual.containers import Vertical
|
|
@@ -21,7 +21,7 @@ from textual.widgets import DataTable, Footer, Input, Label, OptionList, Static
|
|
|
21
21
|
from textual.widgets.option_list import Option
|
|
22
22
|
|
|
23
23
|
from . import logo, vault
|
|
24
|
-
from .cli import VAULT_PATH, generate, import_csv, to_clipboard
|
|
24
|
+
from .cli import CLIPBOARD_TTL, VAULT_PATH, generate, import_csv, to_clipboard
|
|
25
25
|
|
|
26
26
|
THEME_FILE = os.path.expanduser("~/.local/share/pw-manager/theme")
|
|
27
27
|
|
|
@@ -422,7 +422,8 @@ class MainScreen(Screen):
|
|
|
422
422
|
if name is None:
|
|
423
423
|
return
|
|
424
424
|
if to_clipboard(self.app.entries[name]["pw"]):
|
|
425
|
-
|
|
425
|
+
ttl = f" (clears in {CLIPBOARD_TTL}s)" if CLIPBOARD_TTL > 0 else ""
|
|
426
|
+
self.notify(f"password for '{name}' copied{ttl}", severity="information")
|
|
426
427
|
else:
|
|
427
428
|
self.notify(
|
|
428
429
|
"no clipboard tool found (on Linux: install wl-clipboard)",
|
|
@@ -476,7 +477,7 @@ class MainScreen(Screen):
|
|
|
476
477
|
self.app.push_screen(ConfirmModal(f"delete '{name}'?"), done)
|
|
477
478
|
|
|
478
479
|
def action_theme(self) -> None:
|
|
479
|
-
self.app.
|
|
480
|
+
self.app._theme() # same picker the palette opens
|
|
480
481
|
|
|
481
482
|
def action_search(self) -> None:
|
|
482
483
|
self.query_one("#search", Input).focus()
|
|
@@ -612,6 +613,18 @@ class PwApp(App):
|
|
|
612
613
|
with open(THEME_FILE, "w") as f:
|
|
613
614
|
f.write(theme.name)
|
|
614
615
|
|
|
616
|
+
def get_system_commands(self, screen: Screen):
|
|
617
|
+
"""Swap Textual's built-in 'Theme' entry for our picker, so the
|
|
618
|
+
palette and the `t` key open the same thing."""
|
|
619
|
+
for command in super().get_system_commands(screen):
|
|
620
|
+
if command.title == "Theme":
|
|
621
|
+
yield SystemCommand("Theme", "Change the current theme", self._theme)
|
|
622
|
+
else:
|
|
623
|
+
yield command
|
|
624
|
+
|
|
625
|
+
def _theme(self) -> None:
|
|
626
|
+
self.push_screen(ThemeModal())
|
|
627
|
+
|
|
615
628
|
def _import_csv(self) -> None:
|
|
616
629
|
def done(path: str | None) -> None:
|
|
617
630
|
if not path:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{basic_password_manager-0.4.5 → basic_password_manager-0.4.7}/pw_manager/web/argon2.umd.min.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|