basic-password-manager 0.2.1__tar.gz → 0.2.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.2.1 → basic_password_manager-0.2.2}/PKG-INFO +12 -6
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/README.md +10 -5
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/PKG-INFO +12 -6
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pw_manager/cli.py +16 -11
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pw_manager/tui.py +92 -18
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pyproject.toml +2 -1
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/LICENSE +0 -0
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/SOURCES.txt +0 -0
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/dependency_links.txt +0 -0
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/entry_points.txt +0 -0
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/requires.txt +0 -0
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/basic_password_manager.egg-info/top_level.txt +0 -0
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pw_manager/__init__.py +0 -0
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pw_manager/crypto.py +0 -0
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/pw_manager/vault.py +0 -0
- {basic_password_manager-0.2.1 → basic_password_manager-0.2.2}/setup.cfg +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-password-manager
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: 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
|
|
7
|
+
Project-URL: Changelog, https://github.com/Daisentaur/password-manager/blob/main/CHANGELOG.md
|
|
7
8
|
Requires-Python: >=3.10
|
|
8
9
|
Description-Content-Type: text/markdown
|
|
9
10
|
License-File: LICENSE
|
|
@@ -48,8 +49,12 @@ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
|
|
|
48
49
|
Run `pw` with no arguments for the interactive TUI: unlock, then `/` to
|
|
49
50
|
filter, Enter to copy the selected password, `n` to add, `e` to edit,
|
|
50
51
|
`d` to delete, `t` to switch themes (persisted across runs), `q` to quit.
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
The search box matches every word of your query anywhere in an entry —
|
|
53
|
+
name, username, notes, and the password itself — with matches highlighted
|
|
54
|
+
in the theme's accent color (password matches show the row but highlight
|
|
55
|
+
nothing, since passwords are never displayed). Rarer operations live in the
|
|
56
|
+
command palette (`ctrl+p`): importing a browser CSV, changing the master
|
|
57
|
+
password.
|
|
53
58
|
The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
|
|
54
59
|
admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
|
|
55
60
|
built-in themes are in the picker too.
|
|
@@ -66,7 +71,7 @@ Or use the subcommands:
|
|
|
66
71
|
| `pw passwd` | change the master password |
|
|
67
72
|
| `pw ls` | list entry names |
|
|
68
73
|
| `pw rm github` | delete an entry |
|
|
69
|
-
| `pw find bank` | search
|
|
74
|
+
| `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
|
|
70
75
|
| `pw gen -l 32` | just print a random password |
|
|
71
76
|
| `pw import passwords.csv` | import a browser CSV export (see below) |
|
|
72
77
|
|
|
@@ -131,9 +136,10 @@ memory for the session; the CLI re-derives the key per command.
|
|
|
131
136
|
|
|
132
137
|
argparse subcommands over the two modules above. Passwords are copied to the
|
|
133
138
|
clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
|
|
134
|
-
so they don't sit in your terminal scrollback. `find` searches names,
|
|
139
|
+
so they don't sit in your terminal scrollback. CLI `find` searches names,
|
|
135
140
|
usernames, and notes — deliberately *not* passwords, so fragments of secrets
|
|
136
|
-
never land in your shell history
|
|
141
|
+
never land in your shell history; the TUI search box does cover passwords,
|
|
142
|
+
because nothing typed there is logged anywhere.
|
|
137
143
|
|
|
138
144
|
## Backups
|
|
139
145
|
|
|
@@ -34,8 +34,12 @@ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
|
|
|
34
34
|
Run `pw` with no arguments for the interactive TUI: unlock, then `/` to
|
|
35
35
|
filter, Enter to copy the selected password, `n` to add, `e` to edit,
|
|
36
36
|
`d` to delete, `t` to switch themes (persisted across runs), `q` to quit.
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
The search box matches every word of your query anywhere in an entry —
|
|
38
|
+
name, username, notes, and the password itself — with matches highlighted
|
|
39
|
+
in the theme's accent color (password matches show the row but highlight
|
|
40
|
+
nothing, since passwords are never displayed). Rarer operations live in the
|
|
41
|
+
command palette (`ctrl+p`): importing a browser CSV, changing the master
|
|
42
|
+
password.
|
|
39
43
|
The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
|
|
40
44
|
admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
|
|
41
45
|
built-in themes are in the picker too.
|
|
@@ -52,7 +56,7 @@ Or use the subcommands:
|
|
|
52
56
|
| `pw passwd` | change the master password |
|
|
53
57
|
| `pw ls` | list entry names |
|
|
54
58
|
| `pw rm github` | delete an entry |
|
|
55
|
-
| `pw find bank` | search
|
|
59
|
+
| `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
|
|
56
60
|
| `pw gen -l 32` | just print a random password |
|
|
57
61
|
| `pw import passwords.csv` | import a browser CSV export (see below) |
|
|
58
62
|
|
|
@@ -117,9 +121,10 @@ memory for the session; the CLI re-derives the key per command.
|
|
|
117
121
|
|
|
118
122
|
argparse subcommands over the two modules above. Passwords are copied to the
|
|
119
123
|
clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
|
|
120
|
-
so they don't sit in your terminal scrollback. `find` searches names,
|
|
124
|
+
so they don't sit in your terminal scrollback. CLI `find` searches names,
|
|
121
125
|
usernames, and notes — deliberately *not* passwords, so fragments of secrets
|
|
122
|
-
never land in your shell history
|
|
126
|
+
never land in your shell history; the TUI search box does cover passwords,
|
|
127
|
+
because nothing typed there is logged anywhere.
|
|
123
128
|
|
|
124
129
|
## Backups
|
|
125
130
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-password-manager
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: 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
|
|
7
|
+
Project-URL: Changelog, https://github.com/Daisentaur/password-manager/blob/main/CHANGELOG.md
|
|
7
8
|
Requires-Python: >=3.10
|
|
8
9
|
Description-Content-Type: text/markdown
|
|
9
10
|
License-File: LICENSE
|
|
@@ -48,8 +49,12 @@ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
|
|
|
48
49
|
Run `pw` with no arguments for the interactive TUI: unlock, then `/` to
|
|
49
50
|
filter, Enter to copy the selected password, `n` to add, `e` to edit,
|
|
50
51
|
`d` to delete, `t` to switch themes (persisted across runs), `q` to quit.
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
The search box matches every word of your query anywhere in an entry —
|
|
53
|
+
name, username, notes, and the password itself — with matches highlighted
|
|
54
|
+
in the theme's accent color (password matches show the row but highlight
|
|
55
|
+
nothing, since passwords are never displayed). Rarer operations live in the
|
|
56
|
+
command palette (`ctrl+p`): importing a browser CSV, changing the master
|
|
57
|
+
password.
|
|
53
58
|
The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
|
|
54
59
|
admiration from [tuxedo](https://github.com/webstonehq/tuxedo); Textual's
|
|
55
60
|
built-in themes are in the picker too.
|
|
@@ -66,7 +71,7 @@ Or use the subcommands:
|
|
|
66
71
|
| `pw passwd` | change the master password |
|
|
67
72
|
| `pw ls` | list entry names |
|
|
68
73
|
| `pw rm github` | delete an entry |
|
|
69
|
-
| `pw find bank` | search
|
|
74
|
+
| `pw find "dt bank"` | search names, usernames, notes — every word must match, any order |
|
|
70
75
|
| `pw gen -l 32` | just print a random password |
|
|
71
76
|
| `pw import passwords.csv` | import a browser CSV export (see below) |
|
|
72
77
|
|
|
@@ -131,9 +136,10 @@ memory for the session; the CLI re-derives the key per command.
|
|
|
131
136
|
|
|
132
137
|
argparse subcommands over the two modules above. Passwords are copied to the
|
|
133
138
|
clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
|
|
134
|
-
so they don't sit in your terminal scrollback. `find` searches names,
|
|
139
|
+
so they don't sit in your terminal scrollback. CLI `find` searches names,
|
|
135
140
|
usernames, and notes — deliberately *not* passwords, so fragments of secrets
|
|
136
|
-
never land in your shell history
|
|
141
|
+
never land in your shell history; the TUI search box does cover passwords,
|
|
142
|
+
because nothing typed there is logged anywhere.
|
|
137
143
|
|
|
138
144
|
## Backups
|
|
139
145
|
|
|
@@ -135,16 +135,15 @@ def cmd_gen(args):
|
|
|
135
135
|
|
|
136
136
|
|
|
137
137
|
def cmd_find(args):
|
|
138
|
-
"""
|
|
139
|
-
passwords: a password fragment typed as
|
|
138
|
+
"""Every word of the query must appear somewhere in name/username/notes.
|
|
139
|
+
Deliberately not over passwords: a password fragment typed as a CLI
|
|
140
|
+
argument lands in shell history (the TUI search does cover passwords)."""
|
|
140
141
|
entries = vault.load(VAULT_PATH, ask_master())
|
|
141
|
-
|
|
142
|
+
words = args.term.lower().split()
|
|
142
143
|
hits = {
|
|
143
144
|
name: e
|
|
144
145
|
for name, e in sorted(entries.items())
|
|
145
|
-
if
|
|
146
|
-
or term in e["user"].lower()
|
|
147
|
-
or term in e.get("notes", "").lower()
|
|
146
|
+
if all(w in f"{name} {e['user']} {e.get('notes', '')}".lower() for w in words)
|
|
148
147
|
}
|
|
149
148
|
for name, e in hits.items():
|
|
150
149
|
print(f"{name} ({e['user']})")
|
|
@@ -152,12 +151,11 @@ def cmd_find(args):
|
|
|
152
151
|
sys.exit(f"nothing matches '{args.term}'")
|
|
153
152
|
|
|
154
153
|
|
|
155
|
-
def
|
|
156
|
-
"""
|
|
157
|
-
|
|
158
|
-
entries = vault.load(VAULT_PATH, master)
|
|
154
|
+
def import_csv(csv_file: str, entries: dict) -> int:
|
|
155
|
+
"""Merge a browser CSV export (Chrome: name,url,username,password /
|
|
156
|
+
Firefox: url,...) into entries. Returns how many were added."""
|
|
159
157
|
added = 0
|
|
160
|
-
with open(
|
|
158
|
+
with open(csv_file, newline="") as f:
|
|
161
159
|
for row in csv.DictReader(f):
|
|
162
160
|
name = row.get("name") or urlparse(row.get("url", "")).netloc
|
|
163
161
|
pw = row.get("password")
|
|
@@ -165,6 +163,13 @@ def cmd_import(args):
|
|
|
165
163
|
continue
|
|
166
164
|
entries[name] = {"user": row.get("username", ""), "pw": pw, "notes": ""}
|
|
167
165
|
added += 1
|
|
166
|
+
return added
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cmd_import(args):
|
|
170
|
+
master = ask_master()
|
|
171
|
+
entries = vault.load(VAULT_PATH, master)
|
|
172
|
+
added = import_csv(args.csv_file, entries)
|
|
168
173
|
vault.save(VAULT_PATH, master, entries)
|
|
169
174
|
print(f"imported {added} entries — now delete {args.csv_file} securely")
|
|
170
175
|
|
|
@@ -5,18 +5,20 @@ src/theme.rs); "muted-slate" is the default. `t` opens the theme picker,
|
|
|
5
5
|
and Textual's own built-in themes are available there too.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import csv
|
|
8
9
|
import os
|
|
9
|
-
from typing import Iterable
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from textual.app import App, ComposeResult
|
|
12
13
|
from textual.binding import Binding
|
|
14
|
+
from textual.command import DiscoveryHit, Hit, Hits, Provider
|
|
13
15
|
from textual.containers import Vertical
|
|
14
16
|
from textual.screen import ModalScreen, Screen
|
|
15
17
|
from textual.theme import Theme
|
|
16
18
|
from textual.widgets import DataTable, Footer, Input, Label, Static
|
|
17
19
|
|
|
18
20
|
from . import vault
|
|
19
|
-
from .cli import VAULT_PATH, generate, to_clipboard
|
|
21
|
+
from .cli import VAULT_PATH, generate, import_csv, to_clipboard
|
|
20
22
|
|
|
21
23
|
THEME_FILE = os.path.expanduser("~/.local/share/pw-manager/theme")
|
|
22
24
|
|
|
@@ -126,6 +128,22 @@ class EntryModal(ModalScreen):
|
|
|
126
128
|
)
|
|
127
129
|
|
|
128
130
|
|
|
131
|
+
class ImportModal(ModalScreen):
|
|
132
|
+
"""Ask for the path of a browser CSV export."""
|
|
133
|
+
|
|
134
|
+
BINDINGS = [Binding("escape", "dismiss", "cancel")]
|
|
135
|
+
|
|
136
|
+
def compose(self) -> ComposeResult:
|
|
137
|
+
with Vertical(classes="modal-box"):
|
|
138
|
+
yield Label("import browser CSV", classes="modal-title")
|
|
139
|
+
yield Input(placeholder="path to export, e.g. ~/Downloads/passwords.csv", id="i-path")
|
|
140
|
+
yield Static("enter imports · esc cancels", classes="modal-hint")
|
|
141
|
+
|
|
142
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
143
|
+
if event.value.strip():
|
|
144
|
+
self.dismiss(os.path.expanduser(event.value.strip()))
|
|
145
|
+
|
|
146
|
+
|
|
129
147
|
class PasswdModal(ModalScreen):
|
|
130
148
|
"""Change the master password. Asks for the current one again so a
|
|
131
149
|
walked-up-to unlocked session can't lock the real owner out."""
|
|
@@ -203,18 +221,40 @@ class MainScreen(Screen):
|
|
|
203
221
|
table.add_columns("name", "username", "notes")
|
|
204
222
|
self.refresh_table()
|
|
205
223
|
table.focus()
|
|
224
|
+
# match-highlight color comes from the theme, so repaint on switch
|
|
225
|
+
self.app.theme_changed_signal.subscribe(self, self._on_theme_change)
|
|
226
|
+
|
|
227
|
+
def _on_theme_change(self, _theme: Theme) -> None:
|
|
228
|
+
self.refresh_table()
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _highlight(value: str, words: list[str], style: str) -> Text:
|
|
232
|
+
text = Text(value)
|
|
233
|
+
low = value.lower()
|
|
234
|
+
for w in words:
|
|
235
|
+
start = 0
|
|
236
|
+
while (i := low.find(w, start)) != -1:
|
|
237
|
+
text.stylize(style, i, i + len(w))
|
|
238
|
+
start = i + 1
|
|
239
|
+
return text
|
|
206
240
|
|
|
207
241
|
def refresh_table(self) -> None:
|
|
242
|
+
"""Every search word must appear somewhere in the entry — name,
|
|
243
|
+
username, notes, or the password itself. Passwords are searched but
|
|
244
|
+
never displayed, so matches there can't be highlighted (by design)."""
|
|
208
245
|
table = self.query_one(DataTable)
|
|
209
246
|
table.clear()
|
|
210
|
-
|
|
247
|
+
words = self.query_one("#search", Input).value.lower().split()
|
|
248
|
+
style = f"bold {self.app.current_theme.primary}"
|
|
211
249
|
for name, e in sorted(self.app.entries.items()):
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
250
|
+
hay = f"{name} {e['user']} {e.get('notes', '')} {e['pw']}".lower()
|
|
251
|
+
if all(w in hay for w in words):
|
|
252
|
+
table.add_row(
|
|
253
|
+
self._highlight(name, words, style),
|
|
254
|
+
self._highlight(e["user"], words, style),
|
|
255
|
+
self._highlight(e.get("notes", ""), words, style),
|
|
256
|
+
key=name,
|
|
257
|
+
)
|
|
218
258
|
|
|
219
259
|
def save(self) -> None:
|
|
220
260
|
vault.save(VAULT_PATH, self.app.master, self.app.entries)
|
|
@@ -295,8 +335,35 @@ class MainScreen(Screen):
|
|
|
295
335
|
self.query_one(DataTable).focus()
|
|
296
336
|
|
|
297
337
|
|
|
338
|
+
class VaultCommands(Provider):
|
|
339
|
+
"""Palette commands for the unlocked vault. Unlike Textual's built-in
|
|
340
|
+
provider, the query matches descriptions too, not just titles."""
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def _commands(self):
|
|
344
|
+
if not isinstance(self.screen, MainScreen):
|
|
345
|
+
return []
|
|
346
|
+
app = self.app
|
|
347
|
+
return [
|
|
348
|
+
("Import browser CSV", "add every entry from a Chrome/Firefox password export", app._import_csv),
|
|
349
|
+
("Change master password", "re-encrypt the vault under a new master password", app._change_master),
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
async def search(self, query: str) -> Hits:
|
|
353
|
+
matcher = self.matcher(query)
|
|
354
|
+
for title, help_text, callback in self._commands:
|
|
355
|
+
score = matcher.match(f"{title} {help_text}")
|
|
356
|
+
if score > 0:
|
|
357
|
+
yield Hit(score, matcher.highlight(title), callback, help=help_text)
|
|
358
|
+
|
|
359
|
+
async def discover(self) -> Hits:
|
|
360
|
+
for title, help_text, callback in self._commands:
|
|
361
|
+
yield DiscoveryHit(title, callback, help=help_text)
|
|
362
|
+
|
|
363
|
+
|
|
298
364
|
class PwApp(App):
|
|
299
365
|
TITLE = "pw-manager"
|
|
366
|
+
COMMANDS = App.COMMANDS | {VaultCommands}
|
|
300
367
|
|
|
301
368
|
# Colors come from $theme-variables so every theme applies everywhere;
|
|
302
369
|
# widgets without rules here (table, footer, toasts) use Textual's own
|
|
@@ -372,14 +439,21 @@ class PwApp(App):
|
|
|
372
439
|
with open(THEME_FILE, "w") as f:
|
|
373
440
|
f.write(theme.name)
|
|
374
441
|
|
|
375
|
-
def
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
442
|
+
def _import_csv(self) -> None:
|
|
443
|
+
def done(path: str | None) -> None:
|
|
444
|
+
if not path:
|
|
445
|
+
return
|
|
446
|
+
try:
|
|
447
|
+
added = import_csv(path, self.entries)
|
|
448
|
+
except (OSError, csv.Error) as e:
|
|
449
|
+
self.notify(str(e), severity="error")
|
|
450
|
+
return
|
|
451
|
+
vault.save(VAULT_PATH, self.master, self.entries)
|
|
452
|
+
if isinstance(self.screen, MainScreen):
|
|
453
|
+
self.screen.refresh_table()
|
|
454
|
+
self.notify(f"imported {added} entries — delete the CSV securely")
|
|
455
|
+
|
|
456
|
+
self.push_screen(ImportModal(), done)
|
|
383
457
|
|
|
384
458
|
def _change_master(self) -> None:
|
|
385
459
|
def done(new: str | None) -> None:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "basic-password-manager"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Local encrypted password manager that lives in your terminal"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -20,3 +20,4 @@ pw = "pw_manager.cli:main"
|
|
|
20
20
|
|
|
21
21
|
[project.urls]
|
|
22
22
|
Repository = "https://github.com/Daisentaur/password-manager"
|
|
23
|
+
Changelog = "https://github.com/Daisentaur/password-manager/blob/main/CHANGELOG.md"
|
|
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
|