basic-password-manager 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Darsh Thakur (Daisentaur)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: basic-password-manager
3
+ Version: 0.1.0
4
+ Summary: Local encrypted password manager that lives in your terminal
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/Daisentaur/password-manager
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: argon2-cffi
11
+ Requires-Dist: cryptography
12
+ Dynamic: license-file
13
+
14
+ # pw-manager
15
+
16
+ A local, encrypted password manager that lives in your terminal. No browser,
17
+ no cloud, no daemon — one encrypted file on disk and a small Python CLI in
18
+ front of it.
19
+
20
+ ```
21
+ $ python pw.py get github
22
+ master password:
23
+ username: dt@example.com
24
+ password copied to clipboard
25
+ ```
26
+
27
+ ## Install
28
+
29
+ From PyPI (recommended — [pipx](https://pipx.pypa.io) keeps CLI tools isolated):
30
+
31
+ ```bash
32
+ pipx install basic-password-manager # or: pip install basic-password-manager
33
+ pw init
34
+ ```
35
+
36
+ From source:
37
+
38
+ ```bash
39
+ git clone https://github.com/Daisentaur/password-manager && cd password-manager
40
+ pipx install .
41
+ ```
42
+
43
+ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
44
+
45
+ ## Usage
46
+
47
+ | Command | What it does |
48
+ |---|---|
49
+ | `pw init` | create a new empty vault |
50
+ | `pw add github` | store an entry (prompts for username/password/notes) |
51
+ | `pw add github --gen` | same, but generates a strong random password |
52
+ | `pw get github` | copy the password to the clipboard, show the username |
53
+ | `pw ls` | list entry names |
54
+ | `pw rm github` | delete an entry |
55
+ | `pw find bank` | search entry names, usernames, and notes |
56
+ | `pw gen -l 32` | just print a random password |
57
+ | `pw import passwords.csv` | import a browser CSV export (see below) |
58
+
59
+ The vault lives at `~/.local/share/pw-manager/vault`. Set the `PW_VAULT`
60
+ environment variable to put it somewhere else.
61
+
62
+ The `notes` field takes any text you want kept secret alongside the password —
63
+ recovery codes, PINs, the answer to "what was your first pet".
64
+
65
+ ### Moving off browser password storage
66
+
67
+ 1. Export: Chrome → `chrome://password-manager/settings` → "Export passwords".
68
+ Firefox → `about:logins` → ⋯ menu → "Export passwords". Both produce a CSV.
69
+ 2. `pw import passwords.csv`
70
+ 3. **Delete the CSV** — it is every password you own in plaintext:
71
+ `shred -u passwords.csv`
72
+ 4. Turn off saving and delete saved passwords in the browser settings.
73
+
74
+ ## How it works
75
+
76
+ Three layers, ~120 lines total. Read them in this order:
77
+
78
+ ### 1. `crypto.py` — password → key, key → ciphertext
79
+
80
+ An AES key must be 32 unpredictable bytes; your master password is neither.
81
+ **Argon2id** bridges the gap: `derive_key(password, salt)` always returns the
82
+ same 32 bytes for the same inputs — that's what makes unlocking possible.
83
+ It is deliberately slow and memory-hungry (64 MiB per guess), so an attacker
84
+ who steals the vault file can't brute-force short passwords on a GPU the way
85
+ they could against a plain hash.
86
+
87
+ The **salt** is 16 random bytes stored *unencrypted* in the vault file. It's
88
+ not a secret: its job is making your derived key unique so precomputed
89
+ attack tables are useless.
90
+
91
+ Encryption is **AES-256-GCM**, which is *authenticated*: decrypting with the
92
+ wrong key, or decrypting data that was modified by even one bit, fails loudly
93
+ instead of returning garbage. Each encryption uses a fresh random 12-byte
94
+ **nonce** (also stored unencrypted — also not a secret). The one iron rule of
95
+ GCM is that a (key, nonce) pair must never repeat, which is why it's random
96
+ every time.
97
+
98
+ ### 2. `vault.py` — the file format
99
+
100
+ ```
101
+ [4 bytes "PWV1"][16-byte salt][12-byte nonce][ciphertext...]
102
+ ```
103
+
104
+ The ciphertext is just your entries as JSON, encrypted. Load = read →
105
+ derive key → decrypt → `json.loads`. Save = the reverse, with a fresh salt
106
+ and nonce, written to a temp file and atomically renamed so a crash mid-write
107
+ can't destroy the vault.
108
+
109
+ Your master password is never stored anywhere, in any form. "Wrong password"
110
+ is detected purely by GCM authentication failing.
111
+
112
+ ### 3. `cli.py` — the CLI
113
+
114
+ argparse subcommands over the two modules above. Passwords are copied to the
115
+ clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
116
+ so they don't sit in your terminal scrollback. `find` searches names,
117
+ usernames, and notes — deliberately *not* passwords, so fragments of secrets
118
+ never land in your shell history.
119
+
120
+ ## Backups
121
+
122
+ Every save automatically keeps the previous version as `vault.bak` next to
123
+ the vault — one-step undo if a save goes wrong or you delete the wrong entry.
124
+
125
+ That protects against mistakes, not against the disk dying. For that, the
126
+ vault is one file: copy it anywhere — another disk, a USB stick, even
127
+ somewhere untrusted, since it's useless without the master password.
128
+
129
+ ```bash
130
+ cp ~/.local/share/pw-manager/vault /some/backup/location/
131
+ ```
132
+
133
+ ## Troubleshooting
134
+
135
+ **"I forgot the master password."** The data is gone. Not "gone until support
136
+ resets it" — mathematically gone; that's the entire design. This is why you
137
+ keep the master password memorable (a long passphrase beats `Tr0ub4dor&3`)
138
+ and why backups protect you from file loss but not from forgetting.
139
+
140
+ **"wrong master password (or the vault file is corrupted)"** — 99% of the
141
+ time it's a typo'd password. If you're *certain* the password is right, the
142
+ file was damaged; restore the automatic backup
143
+ (`cp ~/.local/share/pw-manager/vault.bak ~/.local/share/pw-manager/vault`)
144
+ or an off-machine copy.
145
+
146
+ **Deleted or overwrote an entry by mistake** — the state before your last
147
+ save is in `vault.bak`; restore it as above. Only one generation is kept, so
148
+ do it before the next save.
149
+
150
+ **"not a vault file (bad magic bytes)"** — the file at the vault path isn't
151
+ one of ours (truncated, overwritten, or wrong path). Check `echo $PW_VAULT`
152
+ and restore from a backup.
153
+
154
+ **"no vault at ..."** — run `pw init`, or `PW_VAULT` points somewhere
155
+ unexpected.
156
+
157
+ **Password gets printed instead of copied** — no clipboard tool was found.
158
+ Install one: `sudo apt install wl-clipboard` (Wayland) or `xclip` (X11).
159
+
160
+ **Unlock feels slow (~1s)** — that's Argon2 doing its job; the delay is the
161
+ brute-force resistance. It's per-command, not per-keystroke.
162
+
163
+ **`MemoryError` from Argon2** — the KDF needs 64 MiB free; something is
164
+ eating your RAM.
165
+
166
+ **Import brought in junk entries** — browser CSVs include everything, even
167
+ ancient accounts. `pw rm <name>` the ones you don't want, or edit the CSV
168
+ before importing.
169
+
170
+ ## Honest limitations
171
+
172
+ - No clipboard auto-clear: the password stays on the clipboard until you copy
173
+ over it. Copy something else when you're done.
174
+ - While a command runs, decrypted data briefly exists in process memory.
175
+ Malware already on your machine could read it — true of every password
176
+ manager; the vault protects the file at rest, not a compromised machine.
177
+ - Single file, no sync. Syncing is your problem (and `cp` is a fine answer).
178
+
179
+ ## Running the checks
180
+
181
+ ```bash
182
+ ./venv/bin/python test_vault.py
183
+ ```
@@ -0,0 +1,170 @@
1
+ # pw-manager
2
+
3
+ A local, encrypted password manager that lives in your terminal. No browser,
4
+ no cloud, no daemon — one encrypted file on disk and a small Python CLI in
5
+ front of it.
6
+
7
+ ```
8
+ $ python pw.py get github
9
+ master password:
10
+ username: dt@example.com
11
+ password copied to clipboard
12
+ ```
13
+
14
+ ## Install
15
+
16
+ From PyPI (recommended — [pipx](https://pipx.pypa.io) keeps CLI tools isolated):
17
+
18
+ ```bash
19
+ pipx install basic-password-manager # or: pip install basic-password-manager
20
+ pw init
21
+ ```
22
+
23
+ From source:
24
+
25
+ ```bash
26
+ git clone https://github.com/Daisentaur/password-manager && cd password-manager
27
+ pipx install .
28
+ ```
29
+
30
+ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
31
+
32
+ ## Usage
33
+
34
+ | Command | What it does |
35
+ |---|---|
36
+ | `pw init` | create a new empty vault |
37
+ | `pw add github` | store an entry (prompts for username/password/notes) |
38
+ | `pw add github --gen` | same, but generates a strong random password |
39
+ | `pw get github` | copy the password to the clipboard, show the username |
40
+ | `pw ls` | list entry names |
41
+ | `pw rm github` | delete an entry |
42
+ | `pw find bank` | search entry names, usernames, and notes |
43
+ | `pw gen -l 32` | just print a random password |
44
+ | `pw import passwords.csv` | import a browser CSV export (see below) |
45
+
46
+ The vault lives at `~/.local/share/pw-manager/vault`. Set the `PW_VAULT`
47
+ environment variable to put it somewhere else.
48
+
49
+ The `notes` field takes any text you want kept secret alongside the password —
50
+ recovery codes, PINs, the answer to "what was your first pet".
51
+
52
+ ### Moving off browser password storage
53
+
54
+ 1. Export: Chrome → `chrome://password-manager/settings` → "Export passwords".
55
+ Firefox → `about:logins` → ⋯ menu → "Export passwords". Both produce a CSV.
56
+ 2. `pw import passwords.csv`
57
+ 3. **Delete the CSV** — it is every password you own in plaintext:
58
+ `shred -u passwords.csv`
59
+ 4. Turn off saving and delete saved passwords in the browser settings.
60
+
61
+ ## How it works
62
+
63
+ Three layers, ~120 lines total. Read them in this order:
64
+
65
+ ### 1. `crypto.py` — password → key, key → ciphertext
66
+
67
+ An AES key must be 32 unpredictable bytes; your master password is neither.
68
+ **Argon2id** bridges the gap: `derive_key(password, salt)` always returns the
69
+ same 32 bytes for the same inputs — that's what makes unlocking possible.
70
+ It is deliberately slow and memory-hungry (64 MiB per guess), so an attacker
71
+ who steals the vault file can't brute-force short passwords on a GPU the way
72
+ they could against a plain hash.
73
+
74
+ The **salt** is 16 random bytes stored *unencrypted* in the vault file. It's
75
+ not a secret: its job is making your derived key unique so precomputed
76
+ attack tables are useless.
77
+
78
+ Encryption is **AES-256-GCM**, which is *authenticated*: decrypting with the
79
+ wrong key, or decrypting data that was modified by even one bit, fails loudly
80
+ instead of returning garbage. Each encryption uses a fresh random 12-byte
81
+ **nonce** (also stored unencrypted — also not a secret). The one iron rule of
82
+ GCM is that a (key, nonce) pair must never repeat, which is why it's random
83
+ every time.
84
+
85
+ ### 2. `vault.py` — the file format
86
+
87
+ ```
88
+ [4 bytes "PWV1"][16-byte salt][12-byte nonce][ciphertext...]
89
+ ```
90
+
91
+ The ciphertext is just your entries as JSON, encrypted. Load = read →
92
+ derive key → decrypt → `json.loads`. Save = the reverse, with a fresh salt
93
+ and nonce, written to a temp file and atomically renamed so a crash mid-write
94
+ can't destroy the vault.
95
+
96
+ Your master password is never stored anywhere, in any form. "Wrong password"
97
+ is detected purely by GCM authentication failing.
98
+
99
+ ### 3. `cli.py` — the CLI
100
+
101
+ argparse subcommands over the two modules above. Passwords are copied to the
102
+ clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
103
+ so they don't sit in your terminal scrollback. `find` searches names,
104
+ usernames, and notes — deliberately *not* passwords, so fragments of secrets
105
+ never land in your shell history.
106
+
107
+ ## Backups
108
+
109
+ Every save automatically keeps the previous version as `vault.bak` next to
110
+ the vault — one-step undo if a save goes wrong or you delete the wrong entry.
111
+
112
+ That protects against mistakes, not against the disk dying. For that, the
113
+ vault is one file: copy it anywhere — another disk, a USB stick, even
114
+ somewhere untrusted, since it's useless without the master password.
115
+
116
+ ```bash
117
+ cp ~/.local/share/pw-manager/vault /some/backup/location/
118
+ ```
119
+
120
+ ## Troubleshooting
121
+
122
+ **"I forgot the master password."** The data is gone. Not "gone until support
123
+ resets it" — mathematically gone; that's the entire design. This is why you
124
+ keep the master password memorable (a long passphrase beats `Tr0ub4dor&3`)
125
+ and why backups protect you from file loss but not from forgetting.
126
+
127
+ **"wrong master password (or the vault file is corrupted)"** — 99% of the
128
+ time it's a typo'd password. If you're *certain* the password is right, the
129
+ file was damaged; restore the automatic backup
130
+ (`cp ~/.local/share/pw-manager/vault.bak ~/.local/share/pw-manager/vault`)
131
+ or an off-machine copy.
132
+
133
+ **Deleted or overwrote an entry by mistake** — the state before your last
134
+ save is in `vault.bak`; restore it as above. Only one generation is kept, so
135
+ do it before the next save.
136
+
137
+ **"not a vault file (bad magic bytes)"** — the file at the vault path isn't
138
+ one of ours (truncated, overwritten, or wrong path). Check `echo $PW_VAULT`
139
+ and restore from a backup.
140
+
141
+ **"no vault at ..."** — run `pw init`, or `PW_VAULT` points somewhere
142
+ unexpected.
143
+
144
+ **Password gets printed instead of copied** — no clipboard tool was found.
145
+ Install one: `sudo apt install wl-clipboard` (Wayland) or `xclip` (X11).
146
+
147
+ **Unlock feels slow (~1s)** — that's Argon2 doing its job; the delay is the
148
+ brute-force resistance. It's per-command, not per-keystroke.
149
+
150
+ **`MemoryError` from Argon2** — the KDF needs 64 MiB free; something is
151
+ eating your RAM.
152
+
153
+ **Import brought in junk entries** — browser CSVs include everything, even
154
+ ancient accounts. `pw rm <name>` the ones you don't want, or edit the CSV
155
+ before importing.
156
+
157
+ ## Honest limitations
158
+
159
+ - No clipboard auto-clear: the password stays on the clipboard until you copy
160
+ over it. Copy something else when you're done.
161
+ - While a command runs, decrypted data briefly exists in process memory.
162
+ Malware already on your machine could read it — true of every password
163
+ manager; the vault protects the file at rest, not a compromised machine.
164
+ - Single file, no sync. Syncing is your problem (and `cp` is a fine answer).
165
+
166
+ ## Running the checks
167
+
168
+ ```bash
169
+ ./venv/bin/python test_vault.py
170
+ ```
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: basic-password-manager
3
+ Version: 0.1.0
4
+ Summary: Local encrypted password manager that lives in your terminal
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/Daisentaur/password-manager
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: argon2-cffi
11
+ Requires-Dist: cryptography
12
+ Dynamic: license-file
13
+
14
+ # pw-manager
15
+
16
+ A local, encrypted password manager that lives in your terminal. No browser,
17
+ no cloud, no daemon — one encrypted file on disk and a small Python CLI in
18
+ front of it.
19
+
20
+ ```
21
+ $ python pw.py get github
22
+ master password:
23
+ username: dt@example.com
24
+ password copied to clipboard
25
+ ```
26
+
27
+ ## Install
28
+
29
+ From PyPI (recommended — [pipx](https://pipx.pypa.io) keeps CLI tools isolated):
30
+
31
+ ```bash
32
+ pipx install basic-password-manager # or: pip install basic-password-manager
33
+ pw init
34
+ ```
35
+
36
+ From source:
37
+
38
+ ```bash
39
+ git clone https://github.com/Daisentaur/password-manager && cd password-manager
40
+ pipx install .
41
+ ```
42
+
43
+ For development: `python3 -m venv venv && ./venv/bin/pip install -e .`
44
+
45
+ ## Usage
46
+
47
+ | Command | What it does |
48
+ |---|---|
49
+ | `pw init` | create a new empty vault |
50
+ | `pw add github` | store an entry (prompts for username/password/notes) |
51
+ | `pw add github --gen` | same, but generates a strong random password |
52
+ | `pw get github` | copy the password to the clipboard, show the username |
53
+ | `pw ls` | list entry names |
54
+ | `pw rm github` | delete an entry |
55
+ | `pw find bank` | search entry names, usernames, and notes |
56
+ | `pw gen -l 32` | just print a random password |
57
+ | `pw import passwords.csv` | import a browser CSV export (see below) |
58
+
59
+ The vault lives at `~/.local/share/pw-manager/vault`. Set the `PW_VAULT`
60
+ environment variable to put it somewhere else.
61
+
62
+ The `notes` field takes any text you want kept secret alongside the password —
63
+ recovery codes, PINs, the answer to "what was your first pet".
64
+
65
+ ### Moving off browser password storage
66
+
67
+ 1. Export: Chrome → `chrome://password-manager/settings` → "Export passwords".
68
+ Firefox → `about:logins` → ⋯ menu → "Export passwords". Both produce a CSV.
69
+ 2. `pw import passwords.csv`
70
+ 3. **Delete the CSV** — it is every password you own in plaintext:
71
+ `shred -u passwords.csv`
72
+ 4. Turn off saving and delete saved passwords in the browser settings.
73
+
74
+ ## How it works
75
+
76
+ Three layers, ~120 lines total. Read them in this order:
77
+
78
+ ### 1. `crypto.py` — password → key, key → ciphertext
79
+
80
+ An AES key must be 32 unpredictable bytes; your master password is neither.
81
+ **Argon2id** bridges the gap: `derive_key(password, salt)` always returns the
82
+ same 32 bytes for the same inputs — that's what makes unlocking possible.
83
+ It is deliberately slow and memory-hungry (64 MiB per guess), so an attacker
84
+ who steals the vault file can't brute-force short passwords on a GPU the way
85
+ they could against a plain hash.
86
+
87
+ The **salt** is 16 random bytes stored *unencrypted* in the vault file. It's
88
+ not a secret: its job is making your derived key unique so precomputed
89
+ attack tables are useless.
90
+
91
+ Encryption is **AES-256-GCM**, which is *authenticated*: decrypting with the
92
+ wrong key, or decrypting data that was modified by even one bit, fails loudly
93
+ instead of returning garbage. Each encryption uses a fresh random 12-byte
94
+ **nonce** (also stored unencrypted — also not a secret). The one iron rule of
95
+ GCM is that a (key, nonce) pair must never repeat, which is why it's random
96
+ every time.
97
+
98
+ ### 2. `vault.py` — the file format
99
+
100
+ ```
101
+ [4 bytes "PWV1"][16-byte salt][12-byte nonce][ciphertext...]
102
+ ```
103
+
104
+ The ciphertext is just your entries as JSON, encrypted. Load = read →
105
+ derive key → decrypt → `json.loads`. Save = the reverse, with a fresh salt
106
+ and nonce, written to a temp file and atomically renamed so a crash mid-write
107
+ can't destroy the vault.
108
+
109
+ Your master password is never stored anywhere, in any form. "Wrong password"
110
+ is detected purely by GCM authentication failing.
111
+
112
+ ### 3. `cli.py` — the CLI
113
+
114
+ argparse subcommands over the two modules above. Passwords are copied to the
115
+ clipboard (`wl-copy`/`xclip`/`xsel`, whichever exists) rather than printed,
116
+ so they don't sit in your terminal scrollback. `find` searches names,
117
+ usernames, and notes — deliberately *not* passwords, so fragments of secrets
118
+ never land in your shell history.
119
+
120
+ ## Backups
121
+
122
+ Every save automatically keeps the previous version as `vault.bak` next to
123
+ the vault — one-step undo if a save goes wrong or you delete the wrong entry.
124
+
125
+ That protects against mistakes, not against the disk dying. For that, the
126
+ vault is one file: copy it anywhere — another disk, a USB stick, even
127
+ somewhere untrusted, since it's useless without the master password.
128
+
129
+ ```bash
130
+ cp ~/.local/share/pw-manager/vault /some/backup/location/
131
+ ```
132
+
133
+ ## Troubleshooting
134
+
135
+ **"I forgot the master password."** The data is gone. Not "gone until support
136
+ resets it" — mathematically gone; that's the entire design. This is why you
137
+ keep the master password memorable (a long passphrase beats `Tr0ub4dor&3`)
138
+ and why backups protect you from file loss but not from forgetting.
139
+
140
+ **"wrong master password (or the vault file is corrupted)"** — 99% of the
141
+ time it's a typo'd password. If you're *certain* the password is right, the
142
+ file was damaged; restore the automatic backup
143
+ (`cp ~/.local/share/pw-manager/vault.bak ~/.local/share/pw-manager/vault`)
144
+ or an off-machine copy.
145
+
146
+ **Deleted or overwrote an entry by mistake** — the state before your last
147
+ save is in `vault.bak`; restore it as above. Only one generation is kept, so
148
+ do it before the next save.
149
+
150
+ **"not a vault file (bad magic bytes)"** — the file at the vault path isn't
151
+ one of ours (truncated, overwritten, or wrong path). Check `echo $PW_VAULT`
152
+ and restore from a backup.
153
+
154
+ **"no vault at ..."** — run `pw init`, or `PW_VAULT` points somewhere
155
+ unexpected.
156
+
157
+ **Password gets printed instead of copied** — no clipboard tool was found.
158
+ Install one: `sudo apt install wl-clipboard` (Wayland) or `xclip` (X11).
159
+
160
+ **Unlock feels slow (~1s)** — that's Argon2 doing its job; the delay is the
161
+ brute-force resistance. It's per-command, not per-keystroke.
162
+
163
+ **`MemoryError` from Argon2** — the KDF needs 64 MiB free; something is
164
+ eating your RAM.
165
+
166
+ **Import brought in junk entries** — browser CSVs include everything, even
167
+ ancient accounts. `pw rm <name>` the ones you don't want, or edit the CSV
168
+ before importing.
169
+
170
+ ## Honest limitations
171
+
172
+ - No clipboard auto-clear: the password stays on the clipboard until you copy
173
+ over it. Copy something else when you're done.
174
+ - While a command runs, decrypted data briefly exists in process memory.
175
+ Malware already on your machine could read it — true of every password
176
+ manager; the vault protects the file at rest, not a compromised machine.
177
+ - Single file, no sync. Syncing is your problem (and `cp` is a fine answer).
178
+
179
+ ## Running the checks
180
+
181
+ ```bash
182
+ ./venv/bin/python test_vault.py
183
+ ```
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ basic_password_manager.egg-info/PKG-INFO
5
+ basic_password_manager.egg-info/SOURCES.txt
6
+ basic_password_manager.egg-info/dependency_links.txt
7
+ basic_password_manager.egg-info/entry_points.txt
8
+ basic_password_manager.egg-info/requires.txt
9
+ basic_password_manager.egg-info/top_level.txt
10
+ pw_manager/__init__.py
11
+ pw_manager/cli.py
12
+ pw_manager/crypto.py
13
+ pw_manager/vault.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pw = pw_manager.cli:main
@@ -0,0 +1,2 @@
1
+ argon2-cffi
2
+ cryptography
File without changes
@@ -0,0 +1,174 @@
1
+ """CLI for the vault. Installed as the `pw` command."""
2
+
3
+ import argparse
4
+ import csv
5
+ import getpass
6
+ import os
7
+ import secrets
8
+ import shutil
9
+ import string
10
+ import subprocess
11
+ import sys
12
+ from urllib.parse import urlparse
13
+
14
+ from . import vault
15
+
16
+ VAULT_PATH = os.environ.get(
17
+ "PW_VAULT", os.path.expanduser("~/.local/share/pw-manager/vault")
18
+ )
19
+
20
+
21
+ def generate(length: int = 20) -> str:
22
+ alphabet = string.ascii_letters + string.digits + "!@#$%^&*-_=+"
23
+ return "".join(secrets.choice(alphabet) for _ in range(length))
24
+
25
+
26
+ def to_clipboard(text: str) -> bool:
27
+ # ponytail: no auto-clear timer; clear manually or copy something else over it
28
+ for cmd in (["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "-b"]):
29
+ if shutil.which(cmd[0]):
30
+ subprocess.run(cmd, input=text.encode(), check=True)
31
+ return True
32
+ return False
33
+
34
+
35
+ def ask_master(confirm: bool = False) -> str:
36
+ pw = getpass.getpass("master password: ")
37
+ if confirm:
38
+ if getpass.getpass("confirm master password: ") != pw:
39
+ sys.exit("passwords do not match")
40
+ if len(pw) < 8:
41
+ sys.exit("master password must be at least 8 characters")
42
+ return pw
43
+
44
+
45
+ def cmd_init(args):
46
+ if os.path.exists(VAULT_PATH):
47
+ sys.exit(f"vault already exists at {VAULT_PATH}")
48
+ os.makedirs(os.path.dirname(VAULT_PATH), exist_ok=True)
49
+ vault.save(VAULT_PATH, ask_master(confirm=True), {})
50
+ print(f"empty vault created at {VAULT_PATH}")
51
+
52
+
53
+ def cmd_add(args):
54
+ master = ask_master()
55
+ entries = vault.load(VAULT_PATH, master)
56
+ if args.name in entries and input(f"'{args.name}' exists, overwrite? [y/N] ").lower() != "y":
57
+ sys.exit("aborted")
58
+ user = input("username: ")
59
+ if args.gen:
60
+ pw = generate()
61
+ print("generated a 20-character password")
62
+ else:
63
+ pw = getpass.getpass("password: ")
64
+ notes = input("notes (optional): ")
65
+ entries[args.name] = {"user": user, "pw": pw, "notes": notes}
66
+ vault.save(VAULT_PATH, master, entries)
67
+ print(f"stored '{args.name}'")
68
+ if args.gen and to_clipboard(pw):
69
+ print("password copied to clipboard")
70
+
71
+
72
+ def cmd_get(args):
73
+ entries = vault.load(VAULT_PATH, ask_master())
74
+ if args.name not in entries:
75
+ sys.exit(f"no entry '{args.name}' — try 'ls'")
76
+ e = entries[args.name]
77
+ print(f"username: {e['user']}")
78
+ if e.get("notes"):
79
+ print(f"notes: {e['notes']}")
80
+ if to_clipboard(e["pw"]):
81
+ print("password copied to clipboard")
82
+ else:
83
+ # no clipboard tool found; showing it is the only option left
84
+ print(f"password: {e['pw']}")
85
+
86
+
87
+ def cmd_ls(args):
88
+ entries = vault.load(VAULT_PATH, ask_master())
89
+ for name in sorted(entries):
90
+ print(name)
91
+ if not entries:
92
+ print("(vault is empty)")
93
+
94
+
95
+ def cmd_rm(args):
96
+ master = ask_master()
97
+ entries = vault.load(VAULT_PATH, master)
98
+ if args.name not in entries:
99
+ sys.exit(f"no entry '{args.name}'")
100
+ del entries[args.name]
101
+ vault.save(VAULT_PATH, master, entries)
102
+ print(f"removed '{args.name}'")
103
+
104
+
105
+ def cmd_gen(args):
106
+ print(generate(args.length))
107
+
108
+
109
+ def cmd_find(args):
110
+ """Substring search over names, usernames, notes — deliberately not over
111
+ passwords: a password fragment typed as an argument lands in shell history."""
112
+ entries = vault.load(VAULT_PATH, ask_master())
113
+ term = args.term.lower()
114
+ hits = {
115
+ name: e
116
+ for name, e in sorted(entries.items())
117
+ if term in name.lower()
118
+ or term in e["user"].lower()
119
+ or term in e.get("notes", "").lower()
120
+ }
121
+ for name, e in hits.items():
122
+ print(f"{name} ({e['user']})")
123
+ if not hits:
124
+ sys.exit(f"nothing matches '{args.term}'")
125
+
126
+
127
+ def cmd_import(args):
128
+ """Browser CSV export (Chrome: name,url,username,password / Firefox: url,...)."""
129
+ master = ask_master()
130
+ entries = vault.load(VAULT_PATH, master)
131
+ added = 0
132
+ with open(args.csv_file, newline="") as f:
133
+ for row in csv.DictReader(f):
134
+ name = row.get("name") or urlparse(row.get("url", "")).netloc
135
+ pw = row.get("password")
136
+ if not name or not pw:
137
+ continue
138
+ entries[name] = {"user": row.get("username", ""), "pw": pw, "notes": ""}
139
+ added += 1
140
+ vault.save(VAULT_PATH, master, entries)
141
+ print(f"imported {added} entries — now delete {args.csv_file} securely")
142
+
143
+
144
+ def main():
145
+ p = argparse.ArgumentParser(description="local encrypted password manager")
146
+ sub = p.add_subparsers(dest="command", required=True)
147
+
148
+ sub.add_parser("init", help="create a new empty vault")
149
+ a = sub.add_parser("add", help="add or update an entry")
150
+ a.add_argument("name")
151
+ a.add_argument("--gen", action="store_true", help="generate the password")
152
+ g = sub.add_parser("get", help="copy an entry's password to clipboard")
153
+ g.add_argument("name")
154
+ sub.add_parser("ls", help="list entry names")
155
+ r = sub.add_parser("rm", help="delete an entry")
156
+ r.add_argument("name")
157
+ n = sub.add_parser("gen", help="print a random password")
158
+ n.add_argument("-l", "--length", type=int, default=20)
159
+ f = sub.add_parser("find", help="search entry names, usernames, notes")
160
+ f.add_argument("term")
161
+ i = sub.add_parser("import", help="import a browser CSV export")
162
+ i.add_argument("csv_file")
163
+
164
+ args = p.parse_args()
165
+ try:
166
+ globals()[f"cmd_{args.command}"](args)
167
+ except vault.VaultError as e:
168
+ sys.exit(str(e))
169
+ except KeyboardInterrupt:
170
+ sys.exit("\naborted")
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()
@@ -0,0 +1,34 @@
1
+ """Key derivation and authenticated encryption. No vault knowledge here."""
2
+
3
+ import os
4
+
5
+ from argon2.low_level import Type, hash_secret_raw
6
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
7
+
8
+ SALT_LEN = 16
9
+ NONCE_LEN = 12 # AES-GCM standard nonce size
10
+ KEY_LEN = 32 # AES-256
11
+
12
+
13
+ def derive_key(password: str, salt: bytes) -> bytes:
14
+ """Stretch a master password into a 32-byte key. Same inputs -> same key."""
15
+ return hash_secret_raw(
16
+ secret=password.encode(),
17
+ salt=salt,
18
+ time_cost=3,
19
+ memory_cost=64 * 1024, # 64 MiB — what makes GPU brute-force expensive
20
+ parallelism=4,
21
+ hash_len=KEY_LEN,
22
+ type=Type.ID, # Argon2id
23
+ )
24
+
25
+
26
+ def encrypt(key: bytes, plaintext: bytes) -> bytes:
27
+ """Returns nonce + ciphertext. Fresh random nonce every call — reuse breaks GCM."""
28
+ nonce = os.urandom(NONCE_LEN)
29
+ return nonce + AESGCM(key).encrypt(nonce, plaintext, None)
30
+
31
+
32
+ def decrypt(key: bytes, blob: bytes) -> bytes:
33
+ """Raises cryptography.exceptions.InvalidTag on wrong key or tampered data."""
34
+ return AESGCM(key).decrypt(blob[:NONCE_LEN], blob[NONCE_LEN:], None)
@@ -0,0 +1,51 @@
1
+ """Vault file: [4-byte magic "PWV1"][16-byte salt][12-byte nonce + ciphertext].
2
+
3
+ The ciphertext is JSON: {"entry name": {"user": ..., "pw": ..., "notes": ...}}.
4
+ Salt and nonce are not secrets; everything sensitive is inside the ciphertext.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import shutil
10
+
11
+ from cryptography.exceptions import InvalidTag
12
+
13
+ from . import crypto
14
+
15
+ MAGIC = b"PWV1"
16
+
17
+
18
+ class VaultError(Exception):
19
+ """Anything wrong with opening or reading a vault, with a human message."""
20
+
21
+
22
+ def load(path: str, password: str) -> dict:
23
+ try:
24
+ with open(path, "rb") as f:
25
+ raw = f.read()
26
+ except FileNotFoundError:
27
+ raise VaultError(f"no vault at {path} — run 'init' first")
28
+ if raw[:4] != MAGIC:
29
+ raise VaultError(f"{path} is not a vault file (bad magic bytes)")
30
+ salt = raw[4 : 4 + crypto.SALT_LEN]
31
+ key = crypto.derive_key(password, salt)
32
+ try:
33
+ plaintext = crypto.decrypt(key, raw[4 + crypto.SALT_LEN :])
34
+ except InvalidTag:
35
+ raise VaultError("wrong master password (or the vault file is corrupted)")
36
+ return json.loads(plaintext)
37
+
38
+
39
+ def save(path: str, password: str, entries: dict) -> None:
40
+ salt = os.urandom(crypto.SALT_LEN)
41
+ key = crypto.derive_key(password, salt)
42
+ blob = MAGIC + salt + crypto.encrypt(key, json.dumps(entries).encode())
43
+ # Keep the previous version as .bak: one-step undo for corruption or mistakes.
44
+ if os.path.exists(path):
45
+ shutil.copy2(path, path + ".bak")
46
+ # Write to a temp file then rename: a crash mid-write can't destroy the vault.
47
+ tmp = path + ".tmp"
48
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
49
+ with os.fdopen(fd, "wb") as f:
50
+ f.write(blob)
51
+ os.replace(tmp, path)
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "basic-password-manager"
7
+ version = "0.1.0"
8
+ description = "Local encrypted password manager that lives in your terminal"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "argon2-cffi",
14
+ "cryptography",
15
+ ]
16
+
17
+ [project.scripts]
18
+ pw = "pw_manager.cli:main"
19
+
20
+ [project.urls]
21
+ Repository = "https://github.com/Daisentaur/password-manager"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+