passlocker 1.0.1__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.
- passlocker-1.0.1/LICENSE +19 -0
- passlocker-1.0.1/PKG-INFO +83 -0
- passlocker-1.0.1/README.md +55 -0
- passlocker-1.0.1/passlocker/__init__.py +1 -0
- passlocker-1.0.1/passlocker/__main__.py +4 -0
- passlocker-1.0.1/passlocker/cui.py +430 -0
- passlocker-1.0.1/passlocker/passlocker.py +585 -0
- passlocker-1.0.1/passlocker.egg-info/PKG-INFO +83 -0
- passlocker-1.0.1/passlocker.egg-info/SOURCES.txt +16 -0
- passlocker-1.0.1/passlocker.egg-info/dependency_links.txt +1 -0
- passlocker-1.0.1/passlocker.egg-info/entry_points.txt +2 -0
- passlocker-1.0.1/passlocker.egg-info/requires.txt +13 -0
- passlocker-1.0.1/passlocker.egg-info/top_level.txt +1 -0
- passlocker-1.0.1/pyproject.toml +51 -0
- passlocker-1.0.1/setup.cfg +4 -0
- passlocker-1.0.1/setup.py +6 -0
- passlocker-1.0.1/tests/test_keycheck.py +16 -0
- passlocker-1.0.1/tests/test_passlocker.py +181 -0
passlocker-1.0.1/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License
|
|
2
|
+
|
|
3
|
+
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License").
|
|
4
|
+
|
|
5
|
+
You may not use the material for commercial purposes.
|
|
6
|
+
|
|
7
|
+
You are free to:
|
|
8
|
+
- Share — copy and redistribute the material in any medium or format
|
|
9
|
+
- Adapt — remix, transform, and build upon the material
|
|
10
|
+
|
|
11
|
+
Under the following terms:
|
|
12
|
+
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
|
|
13
|
+
- NonCommercial — You may not use the material for commercial purposes.
|
|
14
|
+
- ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
|
|
15
|
+
- No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
|
|
16
|
+
|
|
17
|
+
For the full license text, see: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
|
|
18
|
+
|
|
19
|
+
SPDX-License-Identifier: CC-BY-NC-SA-4.0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: passlocker
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Password Encryption
|
|
5
|
+
Author-email: Chris Lee <github@chrislee.dhs.org>
|
|
6
|
+
License: CC-BY-NC-SA-4.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/chrislee35/passlocker
|
|
8
|
+
Keywords: encryption,passwords,security
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Topic :: Utilities
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: pycryptodome>=3.23.0
|
|
16
|
+
Requires-Dist: pyperclip>=1.11.0
|
|
17
|
+
Requires-Dist: pwinput>=1.0.3
|
|
18
|
+
Requires-Dist: argon2-cffi>=25.1.0
|
|
19
|
+
Requires-Dist: requests>=2.32.5
|
|
20
|
+
Requires-Dist: qrcode>=8.2
|
|
21
|
+
Requires-Dist: colorama>=0.4.6
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build; extra == "dev"
|
|
24
|
+
Requires-Dist: twine; extra == "dev"
|
|
25
|
+
Requires-Dist: black; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Overview
|
|
30
|
+
|
|
31
|
+
PassLocker creates a password repository.
|
|
32
|
+
|
|
33
|
+
# View help
|
|
34
|
+
|
|
35
|
+
passlocker help
|
|
36
|
+
|
|
37
|
+
# Listing Accounts
|
|
38
|
+
|
|
39
|
+
passlocker list
|
|
40
|
+
|
|
41
|
+
# Get information on a named account
|
|
42
|
+
|
|
43
|
+
passlocker info "test account"
|
|
44
|
+
passlocker info hex 74657374206163636f756e74
|
|
45
|
+
passlocker info b64 dGVzdCBhY2NvdW50Cg==
|
|
46
|
+
|
|
47
|
+
# Get the active password on a named account
|
|
48
|
+
|
|
49
|
+
passlocker get "test account"
|
|
50
|
+
|
|
51
|
+
# Add a simple password-based account
|
|
52
|
+
|
|
53
|
+
passlocker add "test account 2" password
|
|
54
|
+
|
|
55
|
+
# Add a One-Time-Password account
|
|
56
|
+
|
|
57
|
+
passlocker add "otp account" otp
|
|
58
|
+
|
|
59
|
+
# Add a Time-based One-Time-Password account
|
|
60
|
+
|
|
61
|
+
passlocker add "totp account" totp [start_time] [interval] [num_digits] [hash_algo]
|
|
62
|
+
|
|
63
|
+
# Set the username for an account
|
|
64
|
+
|
|
65
|
+
passlocker user "test account 2" "<username>"
|
|
66
|
+
|
|
67
|
+
# Add a note to an account
|
|
68
|
+
|
|
69
|
+
passlocker note "test account 2" "<note>"
|
|
70
|
+
|
|
71
|
+
# Add a password to a password account
|
|
72
|
+
|
|
73
|
+
passlocker pw "test account 2" [raw|hex|b64] [file <filename>]
|
|
74
|
+
|
|
75
|
+
# Add OTP passwords
|
|
76
|
+
|
|
77
|
+
passlocker pw "otp account" [raw|hex|b64] [file <filename>]
|
|
78
|
+
|
|
79
|
+
# Add a TOTP secret
|
|
80
|
+
|
|
81
|
+
passlocker pw "totp account" [raw|hex|b64] [file <filename>]
|
|
82
|
+
|
|
83
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
PassLocker creates a password repository.
|
|
4
|
+
|
|
5
|
+
# View help
|
|
6
|
+
|
|
7
|
+
passlocker help
|
|
8
|
+
|
|
9
|
+
# Listing Accounts
|
|
10
|
+
|
|
11
|
+
passlocker list
|
|
12
|
+
|
|
13
|
+
# Get information on a named account
|
|
14
|
+
|
|
15
|
+
passlocker info "test account"
|
|
16
|
+
passlocker info hex 74657374206163636f756e74
|
|
17
|
+
passlocker info b64 dGVzdCBhY2NvdW50Cg==
|
|
18
|
+
|
|
19
|
+
# Get the active password on a named account
|
|
20
|
+
|
|
21
|
+
passlocker get "test account"
|
|
22
|
+
|
|
23
|
+
# Add a simple password-based account
|
|
24
|
+
|
|
25
|
+
passlocker add "test account 2" password
|
|
26
|
+
|
|
27
|
+
# Add a One-Time-Password account
|
|
28
|
+
|
|
29
|
+
passlocker add "otp account" otp
|
|
30
|
+
|
|
31
|
+
# Add a Time-based One-Time-Password account
|
|
32
|
+
|
|
33
|
+
passlocker add "totp account" totp [start_time] [interval] [num_digits] [hash_algo]
|
|
34
|
+
|
|
35
|
+
# Set the username for an account
|
|
36
|
+
|
|
37
|
+
passlocker user "test account 2" "<username>"
|
|
38
|
+
|
|
39
|
+
# Add a note to an account
|
|
40
|
+
|
|
41
|
+
passlocker note "test account 2" "<note>"
|
|
42
|
+
|
|
43
|
+
# Add a password to a password account
|
|
44
|
+
|
|
45
|
+
passlocker pw "test account 2" [raw|hex|b64] [file <filename>]
|
|
46
|
+
|
|
47
|
+
# Add OTP passwords
|
|
48
|
+
|
|
49
|
+
passlocker pw "otp account" [raw|hex|b64] [file <filename>]
|
|
50
|
+
|
|
51
|
+
# Add a TOTP secret
|
|
52
|
+
|
|
53
|
+
passlocker pw "totp account" [raw|hex|b64] [file <filename>]
|
|
54
|
+
|
|
55
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .passlocker import PassLocker
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
from _io import StringIO
|
|
2
|
+
from qrcode import QRCode
|
|
3
|
+
from typing import Callable, Literal, LiteralString, cast
|
|
4
|
+
from passlocker.models.account import Account
|
|
5
|
+
from .passlocker import PassLocker
|
|
6
|
+
import getpass
|
|
7
|
+
import io
|
|
8
|
+
import os
|
|
9
|
+
import base64
|
|
10
|
+
import pyperclip
|
|
11
|
+
from binascii import unhexlify
|
|
12
|
+
from pprint import pprint
|
|
13
|
+
import secrets
|
|
14
|
+
from colorama import Fore, Style
|
|
15
|
+
|
|
16
|
+
def dec(item: str, encoding: str='raw') -> bytes | str:
|
|
17
|
+
if encoding == 'hex':
|
|
18
|
+
return unhexlify(item)
|
|
19
|
+
elif encoding == 'b64':
|
|
20
|
+
return base64.b64decode(item)
|
|
21
|
+
elif encoding == 'raw':
|
|
22
|
+
return item.encode('UTF-8')
|
|
23
|
+
elif encoding == 'bytes':
|
|
24
|
+
return item
|
|
25
|
+
return item
|
|
26
|
+
|
|
27
|
+
def pl_prompt(message: str, default: str = "", options: list[str] | None = None) -> str:
|
|
28
|
+
while True:
|
|
29
|
+
if options:
|
|
30
|
+
print("Options: [%s]" % ", ".join(options))
|
|
31
|
+
|
|
32
|
+
a: str = input(f'{message}: [{default}] ')
|
|
33
|
+
if a == "":
|
|
34
|
+
a = default
|
|
35
|
+
|
|
36
|
+
if options:
|
|
37
|
+
if a in options:
|
|
38
|
+
return a
|
|
39
|
+
if a in [o[0] for o in options]:
|
|
40
|
+
return a
|
|
41
|
+
else:
|
|
42
|
+
return a
|
|
43
|
+
|
|
44
|
+
def pl_prompt_int(message: str, default: int = 0) -> int:
|
|
45
|
+
while True:
|
|
46
|
+
a: str = input(f'{message}: [{default}] ')
|
|
47
|
+
if a == "":
|
|
48
|
+
return default
|
|
49
|
+
try:
|
|
50
|
+
return int(a)
|
|
51
|
+
except ValueError:
|
|
52
|
+
return default
|
|
53
|
+
|
|
54
|
+
class CUI:
|
|
55
|
+
def __init__(self,
|
|
56
|
+
password_callback: Callable[[str], bytes] | None = None,
|
|
57
|
+
dbdir: str | None = None
|
|
58
|
+
) -> None:
|
|
59
|
+
self.dbdir: str = '%s/.passlocker' % os.environ['HOME']
|
|
60
|
+
if dbdir:
|
|
61
|
+
self.dbdir = dbdir
|
|
62
|
+
self.words: list[str] = list[str]()
|
|
63
|
+
if password_callback is None:
|
|
64
|
+
password_callback = self.get_master_password
|
|
65
|
+
self.pl = PassLocker(password_callback, dbdir=self.dbdir)
|
|
66
|
+
|
|
67
|
+
def next_word(self) -> str | None:
|
|
68
|
+
if len(self.words) == 0:
|
|
69
|
+
return None
|
|
70
|
+
word: str = self.words.pop(0)
|
|
71
|
+
return word
|
|
72
|
+
|
|
73
|
+
def help_main(self):
|
|
74
|
+
print("""help prints out this help message
|
|
75
|
+
list list password entries
|
|
76
|
+
add creates a new password entry
|
|
77
|
+
genpass randomly generates passwords
|
|
78
|
+
chpass change the master password
|
|
79
|
+
pwned check all passwords (via hash) against haveibeenpwned.com
|
|
80
|
+
exit exits the tool""")
|
|
81
|
+
|
|
82
|
+
def help_list(self):
|
|
83
|
+
print("""help prints out this help message
|
|
84
|
+
info provides information about a password entry
|
|
85
|
+
get retrieves password for a password entry
|
|
86
|
+
addpw adds or updates the password for a password entry
|
|
87
|
+
del deletes a password entry
|
|
88
|
+
note add a note to the password entry
|
|
89
|
+
rename rename this account
|
|
90
|
+
pwned check current password (via hash) against haveibeenpwned.com
|
|
91
|
+
exit returns to the top menu""")
|
|
92
|
+
|
|
93
|
+
def menu(self):
|
|
94
|
+
cmd = pl_prompt("Main menu", "exit", ["list", "add", "genpass", "chpass", "clear", "help", "pwned", "version", "exit"])
|
|
95
|
+
if cmd in ["help", 'h']:
|
|
96
|
+
self.help_main()
|
|
97
|
+
elif cmd in ["list", "l"]:
|
|
98
|
+
self.list_accounts()
|
|
99
|
+
elif cmd in ["add", 'a']:
|
|
100
|
+
self.add_account()
|
|
101
|
+
elif cmd in ["genpass", 'g']:
|
|
102
|
+
_ = self.generate_password_menu()
|
|
103
|
+
elif cmd in ["chpass", "c"]:
|
|
104
|
+
self.pl.change_master_password()
|
|
105
|
+
elif cmd in ['exit', 'e']:
|
|
106
|
+
return False
|
|
107
|
+
elif cmd in ["clear"]:
|
|
108
|
+
_ = os.system("clear")
|
|
109
|
+
elif cmd in ["pwned", "p"]:
|
|
110
|
+
self.check_all_passwords()
|
|
111
|
+
elif cmd in ["version", "v"]:
|
|
112
|
+
print(f"Passlocker version {self.pl.PASSLOCKER_VERSION}")
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
def list_accounts(self):
|
|
116
|
+
filt: str = input("search filter: ")
|
|
117
|
+
item: int = 0
|
|
118
|
+
accounts: list[Account] = [acc for acc in self.pl.accounts() if filt in acc.account or filt in acc.username]
|
|
119
|
+
item = 0
|
|
120
|
+
if len(accounts) == 0:
|
|
121
|
+
print("No accounts found")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
for acc in accounts:
|
|
125
|
+
print(f"{item}\t{acc.account}\t{acc.username}")
|
|
126
|
+
item += 1
|
|
127
|
+
|
|
128
|
+
idx: int = pl_prompt_int("entry #", 0)
|
|
129
|
+
if idx >= len(accounts):
|
|
130
|
+
return
|
|
131
|
+
acc: Account = accounts[idx]
|
|
132
|
+
self.edit_account(acc)
|
|
133
|
+
|
|
134
|
+
def edit_account(self, account: Account):
|
|
135
|
+
accname = account.account
|
|
136
|
+
username = account.username
|
|
137
|
+
|
|
138
|
+
while True:
|
|
139
|
+
cmd = pl_prompt(f"{accname} {username}",
|
|
140
|
+
default="exit",
|
|
141
|
+
options=["help","info","get","copy","qrcode","addpw","genpass","test","del","exit","note","rename","clear","pwned"]
|
|
142
|
+
)
|
|
143
|
+
if cmd in ["help", 'h']:
|
|
144
|
+
self.help_list()
|
|
145
|
+
elif cmd in ["info", 'i']:
|
|
146
|
+
pprint(account)
|
|
147
|
+
elif cmd in ["get", 'g']:
|
|
148
|
+
try:
|
|
149
|
+
pw: bytes = account.get_active_password()
|
|
150
|
+
print(Fore.RED+pw.decode('utf-8')+Style.RESET_ALL)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
print(e)
|
|
153
|
+
elif cmd in ["copy", "c"]:
|
|
154
|
+
try:
|
|
155
|
+
pw: bytes = account.get_active_password()
|
|
156
|
+
pyperclip.copy(pw.decode('utf-8'))
|
|
157
|
+
except Exception as e:
|
|
158
|
+
print(e)
|
|
159
|
+
elif cmd in ["qrcode", "q"]:
|
|
160
|
+
try:
|
|
161
|
+
pw: bytes = account.get_active_password()
|
|
162
|
+
self.print_qrcode(pw)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
print(e)
|
|
165
|
+
elif cmd in ["addpw", 'a']:
|
|
166
|
+
new_password: str = getpass.getpass(f'Enter password for account, {accname}: ')
|
|
167
|
+
if new_password == "":
|
|
168
|
+
return
|
|
169
|
+
account.add_password(new_password)
|
|
170
|
+
account.save()
|
|
171
|
+
elif cmd in ["genpass", "g"]:
|
|
172
|
+
generated_password: str | None = self.generate_password_menu()
|
|
173
|
+
if generated_password:
|
|
174
|
+
account.add_password(generated_password)
|
|
175
|
+
account.save()
|
|
176
|
+
elif cmd in ["test", "t"]:
|
|
177
|
+
pw: bytes = account.get_active_password()
|
|
178
|
+
test: str = getpass.getpass("Type in the password to test: ")
|
|
179
|
+
if pw.decode('utf-8') == test:
|
|
180
|
+
print(Fore.GREEN+"You got it!"+Style.RESET_ALL)
|
|
181
|
+
else:
|
|
182
|
+
print(Fore.RED+"Nope, that's not it."+Style.RESET_ALL)
|
|
183
|
+
elif cmd in ["del", 'd']:
|
|
184
|
+
confirm = pl_prompt('Delete account (yes|no)', 'no')
|
|
185
|
+
if confirm and confirm.lower() in ['yes', 'y'] :
|
|
186
|
+
deleted = account.delete()
|
|
187
|
+
if deleted:
|
|
188
|
+
print(f"Account, {account.account}, deleted.")
|
|
189
|
+
return
|
|
190
|
+
else:
|
|
191
|
+
print(f"Cound not delete {account.account}.")
|
|
192
|
+
elif cmd in ["note", 'n']:
|
|
193
|
+
note = input("Note: ")
|
|
194
|
+
if note and len(note) > 0:
|
|
195
|
+
account.add_note(note)
|
|
196
|
+
account.save()
|
|
197
|
+
elif cmd in ["rename", "r"]:
|
|
198
|
+
new_account_name: str = pl_prompt("New account name", accname)
|
|
199
|
+
if not new_account_name:
|
|
200
|
+
continue
|
|
201
|
+
new_username: str = pl_prompt("New user name", username)
|
|
202
|
+
if not new_username:
|
|
203
|
+
continue
|
|
204
|
+
if new_account_name == account.account and new_username == account.username:
|
|
205
|
+
continue
|
|
206
|
+
account.change_account_name(new_account_name)
|
|
207
|
+
account.change_username(new_username)
|
|
208
|
+
account.save()
|
|
209
|
+
accname = account.account
|
|
210
|
+
username = account.username
|
|
211
|
+
elif cmd in ["pwned", "p"]:
|
|
212
|
+
|
|
213
|
+
if account.type != "password":
|
|
214
|
+
print(Fore.GREEN+"Everything's good."+Style.RESET_ALL)
|
|
215
|
+
else:
|
|
216
|
+
pw: bytes = account.get_active_password()
|
|
217
|
+
password: str = pw.decode('utf-8')
|
|
218
|
+
if self.pl.check_pwnedpasswords(password):
|
|
219
|
+
print(Fore.RED+"PWNED!"+Style.RESET_ALL)
|
|
220
|
+
else:
|
|
221
|
+
print(Fore.GREEN+"Everything's good."+Style.RESET_ALL)
|
|
222
|
+
elif cmd in ["clear"]:
|
|
223
|
+
_ = os.system("clear")
|
|
224
|
+
elif cmd in ['exit', 'e']:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
def add_account(self):
|
|
228
|
+
try:
|
|
229
|
+
acctype = pl_prompt("Which account type?", "password", ["password", "otp", "totp"])
|
|
230
|
+
if acctype in ["password", "pw", "p"]:
|
|
231
|
+
self.add_password_account()
|
|
232
|
+
elif acctype == "otp":
|
|
233
|
+
self.add_otp_account()
|
|
234
|
+
elif acctype == "totp":
|
|
235
|
+
self.add_totp_account()
|
|
236
|
+
else:
|
|
237
|
+
return
|
|
238
|
+
except Exception as e:
|
|
239
|
+
#traceback.print_exc(file=sys.stdout)
|
|
240
|
+
print(e)
|
|
241
|
+
|
|
242
|
+
def add_password_account(self):
|
|
243
|
+
account_name: str = self.next_word() or pl_prompt("Account name?")
|
|
244
|
+
username: str = self.next_word() or pl_prompt("Username")
|
|
245
|
+
|
|
246
|
+
acc: Account = self.pl.add_account(account_name, username, acc_type='password')
|
|
247
|
+
password: str | None = self.next_word()
|
|
248
|
+
if password:
|
|
249
|
+
if password == '-':
|
|
250
|
+
password = getpass.getpass(f'Enter password for account, {account_name} {username}: ')
|
|
251
|
+
if password == "":
|
|
252
|
+
return
|
|
253
|
+
acc.add_password(password)
|
|
254
|
+
else:
|
|
255
|
+
acc.add_password(password)
|
|
256
|
+
acc.save()
|
|
257
|
+
else:
|
|
258
|
+
self.edit_account(acc)
|
|
259
|
+
|
|
260
|
+
def add_otp_account(self):
|
|
261
|
+
accname: str = self.next_word() or pl_prompt("Account name?")
|
|
262
|
+
username: str = self.next_word() or pl_prompt("Username")
|
|
263
|
+
acc: Account = self.pl.add_account(accname, username, acc_type='otp')
|
|
264
|
+
password: str | None = self.next_word()
|
|
265
|
+
if password:
|
|
266
|
+
# there's a decision that is needed here. If we add a batch of OTP passwords to
|
|
267
|
+
# a list of existing passwords, should I keep the active password where it's at
|
|
268
|
+
# or point it to the first item of the added items.
|
|
269
|
+
# For now, I will leave the active password index where it's at.
|
|
270
|
+
pa: int = acc.active
|
|
271
|
+
if pa == 0:
|
|
272
|
+
pa = 1
|
|
273
|
+
while password:
|
|
274
|
+
acc.add_password(password)
|
|
275
|
+
password = self.next_word()
|
|
276
|
+
acc.set_active_password(pa)
|
|
277
|
+
acc.save()
|
|
278
|
+
else:
|
|
279
|
+
self.edit_account(acc)
|
|
280
|
+
|
|
281
|
+
def add_totp_account(self):
|
|
282
|
+
accname = self.next_word() or pl_prompt("Account name?")
|
|
283
|
+
username = self.next_word() or pl_prompt("Username")
|
|
284
|
+
word = self.next_word()
|
|
285
|
+
if word is None:
|
|
286
|
+
epoch_start = pl_prompt_int('Start time (epoch seconds)', 0)
|
|
287
|
+
else:
|
|
288
|
+
epoch_start = int(word)
|
|
289
|
+
word = self.next_word()
|
|
290
|
+
if word is None:
|
|
291
|
+
time_interval = pl_prompt_int('Time interval (seconds)', 30)
|
|
292
|
+
else:
|
|
293
|
+
time_interval = int(word)
|
|
294
|
+
word = self.next_word()
|
|
295
|
+
if word is None:
|
|
296
|
+
num_digits = pl_prompt_int('Number of digits to return', 6)
|
|
297
|
+
else:
|
|
298
|
+
num_digits = int(word)
|
|
299
|
+
word = self.next_word()
|
|
300
|
+
if word is None or word not in ['sha1', 'md5', 'sha256']:
|
|
301
|
+
hash_algorithm = pl_prompt('Which hash algorithm to use', 'sha1', ['sha1', 'md5', 'sha256', 'sha512'])
|
|
302
|
+
else:
|
|
303
|
+
hash_algorithm = word
|
|
304
|
+
hash_algorithm_literal: Literal['sha1', 'sha256', 'sha512', 'md5'] = cast(Literal["sha1", "sha256", "sha512", "md5"], hash_algorithm)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
acc: Account = self.pl.add_account(accname, username, acc_type='totp',
|
|
308
|
+
totp_epoch_start=epoch_start, totp_time_interval=time_interval, totp_num_digits=num_digits,
|
|
309
|
+
totp_hash_algorithm=hash_algorithm_literal)
|
|
310
|
+
acc.save()
|
|
311
|
+
|
|
312
|
+
self.edit_account(acc)
|
|
313
|
+
|
|
314
|
+
def del_account(self):
|
|
315
|
+
accname = self.next_word() or pl_prompt("Account name?")
|
|
316
|
+
username = self.next_word() or pl_prompt("Username")
|
|
317
|
+
confirm = self.next_word()
|
|
318
|
+
if confirm is None:
|
|
319
|
+
confirm = pl_prompt('Delete account (yes|no)', 'no')
|
|
320
|
+
if confirm and confirm.lower() == 'yes':
|
|
321
|
+
acc = self.pl.get_account(accname, username)
|
|
322
|
+
deleted = acc.delete()
|
|
323
|
+
if deleted:
|
|
324
|
+
print("Account, {accname}, deleted.".format(accname=accname))
|
|
325
|
+
else:
|
|
326
|
+
print("Cound not delete {accname}".format(accname=accname))
|
|
327
|
+
|
|
328
|
+
def get_master_password(self, prompt: str) -> bytes:
|
|
329
|
+
return getpass.getpass(prompt=prompt).encode('UTF-8')
|
|
330
|
+
|
|
331
|
+
def print_qrcode(self, data: str) -> None:
|
|
332
|
+
qr = QRCode()
|
|
333
|
+
qr.add_data(data)
|
|
334
|
+
f: StringIO = io.StringIO()
|
|
335
|
+
qr.print_ascii(out=f, invert=True)
|
|
336
|
+
_ = f.seek(0)
|
|
337
|
+
print(f.read())
|
|
338
|
+
|
|
339
|
+
def generate_password_menu(self) -> str | None:
|
|
340
|
+
gentype: str = pl_prompt("Password type", "memorable", ["memorable", "random", "numbers"])
|
|
341
|
+
length: int = pl_prompt_int("Length", 12)
|
|
342
|
+
count: int = pl_prompt_int("How many passwords?", 1)
|
|
343
|
+
while True:
|
|
344
|
+
passwords: list[str] = []
|
|
345
|
+
for idx in range(count):
|
|
346
|
+
if gentype == "memorable":
|
|
347
|
+
password = self.generate_memorable(length)
|
|
348
|
+
elif gentype == "random":
|
|
349
|
+
password = self.generate_random(length)
|
|
350
|
+
elif gentype == "numbers":
|
|
351
|
+
password = self.generate_numbers(length)
|
|
352
|
+
else:
|
|
353
|
+
raise Exception(f"Unknown password generation algorithm: {gentype}")
|
|
354
|
+
passwords.append(password)
|
|
355
|
+
print(f"{idx}\t{password}")
|
|
356
|
+
option: str = pl_prompt("Select a password to add. [q] to cancel without adding. Any other character regenerates the list.")
|
|
357
|
+
if option == 'q':
|
|
358
|
+
return None
|
|
359
|
+
if option == 'm':
|
|
360
|
+
continue
|
|
361
|
+
if option.isnumeric():
|
|
362
|
+
idx = int(option)
|
|
363
|
+
if idx > len(passwords):
|
|
364
|
+
print("You selected an invalid index.")
|
|
365
|
+
else:
|
|
366
|
+
return passwords[idx]
|
|
367
|
+
|
|
368
|
+
def generate_memorable(self, length: int) -> str:
|
|
369
|
+
if length <= 8:
|
|
370
|
+
maxnum = 100
|
|
371
|
+
special = 0
|
|
372
|
+
elif length <= 16:
|
|
373
|
+
maxnum = 1000
|
|
374
|
+
special = 1
|
|
375
|
+
elif length <= 24:
|
|
376
|
+
maxnum = 10000
|
|
377
|
+
special = 2
|
|
378
|
+
else:
|
|
379
|
+
maxnum = 100000
|
|
380
|
+
special = 3
|
|
381
|
+
|
|
382
|
+
random_int: int = secrets.randbelow(maxnum)
|
|
383
|
+
random_special: LiteralString = ''.join(secrets.choice(['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '+', '=', '{', '}', '[', ']', ':', ';', '<', '>', '.', ',', '?', '/', '~', '`']) for i in range(special))
|
|
384
|
+
|
|
385
|
+
secret_sauce: str = '%d%s' % (random_int, random_special)
|
|
386
|
+
password: str = ""
|
|
387
|
+
|
|
388
|
+
with open('/usr/share/dict/words') as f:
|
|
389
|
+
words: list[str] = [word.strip() for word in f]
|
|
390
|
+
genlen = 0
|
|
391
|
+
while genlen != length:
|
|
392
|
+
password = secret_sauce.join(secrets.choice(words) for i in range(2))
|
|
393
|
+
genlen: int = len(password)
|
|
394
|
+
|
|
395
|
+
return password
|
|
396
|
+
|
|
397
|
+
def generate_random(self, length:int) -> str:
|
|
398
|
+
return secrets.token_urlsafe(32)[0:length]
|
|
399
|
+
|
|
400
|
+
def generate_numbers(self, length:int) -> str:
|
|
401
|
+
return ''.join([str(secrets.choice(range(10))) for _ in range(length)])
|
|
402
|
+
|
|
403
|
+
def check_all_passwords(self) -> None:
|
|
404
|
+
idx = 0
|
|
405
|
+
recs: list[Account] = []
|
|
406
|
+
for acc in self.pl.accounts():
|
|
407
|
+
account_name = acc.account
|
|
408
|
+
user_name = acc.username
|
|
409
|
+
# be very careful here, if you did this to OTP accounts, it would mess up the current index
|
|
410
|
+
# this is meaningless for TOTP accounts, so skip those too
|
|
411
|
+
if acc.type != "password":
|
|
412
|
+
continue
|
|
413
|
+
pw: bytes = acc.get_active_password()
|
|
414
|
+
# check if this "password" is a pin number
|
|
415
|
+
pin_check: str = pw.decode('utf-8')
|
|
416
|
+
if pin_check.isnumeric() and len(pin_check) < 8:
|
|
417
|
+
continue
|
|
418
|
+
if self.pl.check_pwnedpasswords(pw):
|
|
419
|
+
print(f"{idx} {account_name} {user_name} {Fore.RED}PWNED!{Style.RESET_ALL} {pw}")
|
|
420
|
+
recs.append(acc)
|
|
421
|
+
idx += 1
|
|
422
|
+
|
|
423
|
+
idx: int = pl_prompt_int("entry #", 0)
|
|
424
|
+
acc: Account = recs[idx]
|
|
425
|
+
self.edit_account(acc)
|
|
426
|
+
|
|
427
|
+
def main() -> None:
|
|
428
|
+
cui: CUI = CUI()
|
|
429
|
+
while cui.menu():
|
|
430
|
+
pass
|