basic-password-manager 0.4.0__tar.gz → 0.4.2__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.0 → basic_password_manager-0.4.2}/PKG-INFO +1 -1
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/basic_password_manager.egg-info/PKG-INFO +1 -1
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pw_manager/mobile.py +40 -15
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pw_manager/tui.py +96 -7
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pyproject.toml +1 -1
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/LICENSE +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/README.md +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/basic_password_manager.egg-info/SOURCES.txt +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/basic_password_manager.egg-info/dependency_links.txt +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/basic_password_manager.egg-info/entry_points.txt +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/basic_password_manager.egg-info/requires.txt +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/basic_password_manager.egg-info/top_level.txt +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pw_manager/__init__.py +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pw_manager/cli.py +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pw_manager/crypto.py +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pw_manager/logo.py +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pw_manager/vault.py +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pw_manager/web/argon2.umd.min.js +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pw_manager/web/page.html +0 -0
- {basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/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.2
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-password-manager
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -162,9 +162,7 @@ def _build_page(raw_vault: bytes) -> bytes:
|
|
|
162
162
|
).encode()
|
|
163
163
|
|
|
164
164
|
|
|
165
|
-
def _serve(vault_path: str, token: str, port: int = 0) -> http.server.HTTPServer:
|
|
166
|
-
page_holder = {}
|
|
167
|
-
|
|
165
|
+
def _serve(vault_path: str, token: str, port: int = 0, on_fetch=None) -> http.server.HTTPServer:
|
|
168
166
|
class Handler(http.server.BaseHTTPRequestHandler):
|
|
169
167
|
def do_GET(self):
|
|
170
168
|
if self.path.rstrip("/") != f"/{token}":
|
|
@@ -177,11 +175,14 @@ def _serve(vault_path: str, token: str, port: int = 0) -> http.server.HTTPServer
|
|
|
177
175
|
self.send_header("Cache-Control", "no-store")
|
|
178
176
|
self.end_headers()
|
|
179
177
|
self.wfile.write(page)
|
|
178
|
+
if on_fetch:
|
|
179
|
+
on_fetch()
|
|
180
180
|
|
|
181
181
|
def log_message(self, *a):
|
|
182
|
-
pass #
|
|
182
|
+
pass # fetch visibility goes through on_fetch instead
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
# threading server: a phone holding a connection open can't stall shutdown
|
|
185
|
+
return http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
|
185
186
|
|
|
186
187
|
|
|
187
188
|
def _start_tunnel(cf: str, port: int) -> tuple[subprocess.Popen, str]:
|
|
@@ -204,7 +205,11 @@ def _start_tunnel(cf: str, port: int) -> tuple[subprocess.Popen, str]:
|
|
|
204
205
|
raise MobileError("cloudflared didn't produce a tunnel URL in time")
|
|
205
206
|
|
|
206
207
|
|
|
207
|
-
def
|
|
208
|
+
def start_session(
|
|
209
|
+
vault_path: str, stable_url: str | None = None, port: int = 0, on_fetch=None
|
|
210
|
+
) -> tuple[http.server.HTTPServer, subprocess.Popen | None, str]:
|
|
211
|
+
"""Start server (+ tunnel unless a stable URL is configured). Returns
|
|
212
|
+
(server, tunnel_process_or_None, full_url)."""
|
|
208
213
|
if not os.path.exists(vault_path):
|
|
209
214
|
raise MobileError(f"no vault at {vault_path} — run 'paladin init' first")
|
|
210
215
|
|
|
@@ -214,7 +219,7 @@ def run(vault_path: str, stable_url: str | None = None, port: int = 0) -> None:
|
|
|
214
219
|
port = int(os.environ.get("PALADIN_MOBILE_PORT", "8787"))
|
|
215
220
|
|
|
216
221
|
token = _load_token()
|
|
217
|
-
server = _serve(vault_path, token, port)
|
|
222
|
+
server = _serve(vault_path, token, port, on_fetch=on_fetch)
|
|
218
223
|
port = server.server_address[1]
|
|
219
224
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
220
225
|
|
|
@@ -222,10 +227,32 @@ def run(vault_path: str, stable_url: str | None = None, port: int = 0) -> None:
|
|
|
222
227
|
if stable_url:
|
|
223
228
|
base = stable_url.rstrip("/") # your reverse proxy points here → 127.0.0.1:port
|
|
224
229
|
else:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
try:
|
|
231
|
+
cf = cloudflared_path()
|
|
232
|
+
proc, base = _start_tunnel(cf, port)
|
|
233
|
+
except BaseException:
|
|
234
|
+
server.shutdown()
|
|
235
|
+
raise
|
|
236
|
+
return server, proc, f"{base}/{token}"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def stop_session(server, proc) -> None:
|
|
240
|
+
"""Tunnel first (no new requests), then server; a second Ctrl+C during
|
|
241
|
+
cleanup must never produce a traceback."""
|
|
242
|
+
try:
|
|
243
|
+
if proc:
|
|
244
|
+
proc.terminate()
|
|
245
|
+
server.shutdown()
|
|
246
|
+
except KeyboardInterrupt:
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def run(vault_path: str, stable_url: str | None = None, port: int = 0) -> None:
|
|
251
|
+
def fetched():
|
|
252
|
+
print(f" vault page served to a device at {time.strftime('%H:%M:%S')}")
|
|
253
|
+
|
|
254
|
+
print("\n creating your secure link…", flush=True)
|
|
255
|
+
server, proc, url = start_session(vault_path, stable_url, port, on_fetch=fetched)
|
|
229
256
|
|
|
230
257
|
import qrcode
|
|
231
258
|
|
|
@@ -235,7 +262,7 @@ def run(vault_path: str, stable_url: str | None = None, port: int = 0) -> None:
|
|
|
235
262
|
qr.print_ascii(invert=True)
|
|
236
263
|
print(f"\n {url}\n")
|
|
237
264
|
print(" the master password is typed on your phone; only ciphertext leaves this machine.")
|
|
238
|
-
print(" Ctrl+C to stop the session.\n")
|
|
265
|
+
print(" every page load is logged below — Ctrl+C to stop the session.\n")
|
|
239
266
|
|
|
240
267
|
try:
|
|
241
268
|
while True:
|
|
@@ -243,6 +270,4 @@ def run(vault_path: str, stable_url: str | None = None, port: int = 0) -> None:
|
|
|
243
270
|
except KeyboardInterrupt:
|
|
244
271
|
print("\nsession ended.")
|
|
245
272
|
finally:
|
|
246
|
-
server
|
|
247
|
-
if proc:
|
|
248
|
-
proc.terminate()
|
|
273
|
+
stop_session(server, proc)
|
|
@@ -7,7 +7,9 @@ and Textual's own built-in themes are available there too.
|
|
|
7
7
|
|
|
8
8
|
import csv
|
|
9
9
|
import os
|
|
10
|
+
import time
|
|
10
11
|
|
|
12
|
+
from rich.style import Style
|
|
11
13
|
from rich.text import Text
|
|
12
14
|
from textual.app import App, ComposeResult
|
|
13
15
|
from textual.binding import Binding
|
|
@@ -158,6 +160,92 @@ class ImportModal(ModalScreen):
|
|
|
158
160
|
self.dismiss(os.path.expanduser(event.value.strip()))
|
|
159
161
|
|
|
160
162
|
|
|
163
|
+
def _qr_text(url: str, primary: str) -> tuple[Text, int]:
|
|
164
|
+
"""The QR as half-block art: theme-colored modules on white, darkened
|
|
165
|
+
until cameras can read it. Returns (text, width_in_cells) — the caller
|
|
166
|
+
must give the widget exactly that width: a wrapped QR is a dead QR."""
|
|
167
|
+
import qrcode
|
|
168
|
+
|
|
169
|
+
r, g, b = logo._hex_rgb(primary)
|
|
170
|
+
while (r + g + b) / 765 > 0.45:
|
|
171
|
+
r, g, b = int(r * 0.8), int(g * 0.8), int(b * 0.8)
|
|
172
|
+
dark, light = f"rgb({r},{g},{b})", "rgb(255,255,255)"
|
|
173
|
+
|
|
174
|
+
qr = qrcode.QRCode(border=2)
|
|
175
|
+
qr.add_data(url)
|
|
176
|
+
qr.make()
|
|
177
|
+
matrix = qr.get_matrix()
|
|
178
|
+
if len(matrix) % 2:
|
|
179
|
+
matrix.append([False] * len(matrix[0]))
|
|
180
|
+
text = Text(no_wrap=True)
|
|
181
|
+
for top, bottom in zip(matrix[::2], matrix[1::2]):
|
|
182
|
+
for t, btm in zip(top, bottom):
|
|
183
|
+
text.append("▀", Style(color=dark if t else light, bgcolor=dark if btm else light))
|
|
184
|
+
text.append("\n")
|
|
185
|
+
return text, len(matrix[0])
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class MobileModal(ModalScreen):
|
|
189
|
+
"""Serve the vault to a phone without leaving the TUI. The session lives
|
|
190
|
+
while this modal is open; Esc ends it."""
|
|
191
|
+
|
|
192
|
+
BINDINGS = [Binding("escape", "dismiss", "stop session")]
|
|
193
|
+
|
|
194
|
+
def __init__(self) -> None:
|
|
195
|
+
super().__init__()
|
|
196
|
+
self.server = None
|
|
197
|
+
self.proc = None
|
|
198
|
+
|
|
199
|
+
def compose(self) -> ComposeResult:
|
|
200
|
+
with Vertical(classes="modal-box", id="mobile-box"):
|
|
201
|
+
yield Label("open on phone", classes="modal-title")
|
|
202
|
+
yield Static("creating your secure link…", id="qr")
|
|
203
|
+
yield Static("", id="mobile-url")
|
|
204
|
+
yield Static("", id="mobile-status")
|
|
205
|
+
yield Static("esc ends the session", classes="modal-hint")
|
|
206
|
+
|
|
207
|
+
def on_mount(self) -> None:
|
|
208
|
+
self.run_worker(self._start, thread=True)
|
|
209
|
+
|
|
210
|
+
def _start(self) -> None:
|
|
211
|
+
from . import mobile
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
self.server, self.proc, url = mobile.start_session(
|
|
215
|
+
VAULT_PATH, on_fetch=self._on_fetch
|
|
216
|
+
)
|
|
217
|
+
except Exception as e: # show any failure in the modal, don't die silently
|
|
218
|
+
self.app.call_from_thread(
|
|
219
|
+
self.query_one("#qr", Static).update, f"couldn't start: {e}"
|
|
220
|
+
)
|
|
221
|
+
return
|
|
222
|
+
self.app.call_from_thread(self._show_qr, url)
|
|
223
|
+
|
|
224
|
+
def _show_qr(self, url: str) -> None:
|
|
225
|
+
qr, width = _qr_text(url, self.app.current_theme.primary)
|
|
226
|
+
widget = self.query_one("#qr", Static)
|
|
227
|
+
widget.update(qr)
|
|
228
|
+
# pin every width to the QR's real size — auto-layout wrapping a QR
|
|
229
|
+
# scrambles the modules and kills scannability
|
|
230
|
+
widget.styles.width = width
|
|
231
|
+
for wid in ("#mobile-url", "#mobile-status"):
|
|
232
|
+
self.query_one(wid, Static).styles.width = width
|
|
233
|
+
self.query_one("#mobile-box").styles.width = width + 6 # padding + border
|
|
234
|
+
self.query_one("#mobile-url", Static).update(url)
|
|
235
|
+
|
|
236
|
+
def _on_fetch(self) -> None:
|
|
237
|
+
self.app.call_from_thread(
|
|
238
|
+
self.query_one("#mobile-status", Static).update,
|
|
239
|
+
f"vault page served to a device at {time.strftime('%H:%M:%S')}",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def on_unmount(self) -> None:
|
|
243
|
+
from . import mobile
|
|
244
|
+
|
|
245
|
+
if self.server:
|
|
246
|
+
mobile.stop_session(self.server, self.proc)
|
|
247
|
+
|
|
248
|
+
|
|
161
249
|
class PasswdModal(ModalScreen):
|
|
162
250
|
"""Change the master password. Asks for the current one again so a
|
|
163
251
|
walked-up-to unlocked session can't lock the real owner out."""
|
|
@@ -436,6 +524,13 @@ class PwApp(App):
|
|
|
436
524
|
background: $surface;
|
|
437
525
|
border: round $primary;
|
|
438
526
|
}
|
|
527
|
+
#mobile-box {
|
|
528
|
+
width: auto;
|
|
529
|
+
}
|
|
530
|
+
#mobile-url {
|
|
531
|
+
color: $text-muted;
|
|
532
|
+
margin-top: 1;
|
|
533
|
+
}
|
|
439
534
|
.modal-title {
|
|
440
535
|
color: $primary;
|
|
441
536
|
text-style: bold;
|
|
@@ -485,13 +580,7 @@ class PwApp(App):
|
|
|
485
580
|
self.push_screen(ImportModal(), done)
|
|
486
581
|
|
|
487
582
|
def _mobile(self) -> None:
|
|
488
|
-
|
|
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… ")
|
|
583
|
+
self.push_screen(MobileModal())
|
|
495
584
|
|
|
496
585
|
def _change_master(self) -> None:
|
|
497
586
|
def done(new: str | None) -> None:
|
|
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
|
|
File without changes
|
|
File without changes
|
{basic_password_manager-0.4.0 → basic_password_manager-0.4.2}/pw_manager/web/argon2.umd.min.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|