-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
354 lines (274 loc) · 10.3 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
import argon2
from getpass import getpass
import sqlite3
from os import urandom
import pyperclip
from cryptography.fernet import Fernet
import base64
from secrets import choice
import string
def new_profile():
"""
Creates a new profile. Asks and stores username, master password and salt for PBKDF
"""
def create_profile():
while True:
account_name = input('Enter account name: ')
password = getpass('Enter password: ')
password_again = getpass('Enter password again: ')
if password != password_again:
print("Wrong password entered. Rerunning setup...")
else:
ph = argon2.PasswordHasher()
password_hash = ph.hash(password)
salt = ''.join([choice(string.ascii_letters + string.digits) for i in range(16)])
del password, password_again
return (account_name, password_hash, salt)
account_name, password_hash, salt = create_profile()
# store profile
con = sqlite3.connect('passwords.db')
with con:
# try saving the profile, if account_name already exists, raises an error and re-runs the create_profile func
# tries inserting again
while True:
try:
con.execute(
"""
INSERT INTO user_login (account_name, password_hash, salt)
VALUES (?, ?, ?);
""",
(account_name, password_hash, salt))
break
except:
print("An account with this name already exists!")
account_name, password_hash, salt = create_profile()
con.close()
print("Profile has been created")
def login():
"""
Logs in the user. Returns plaintext password and salt if user is successful in logging in, else returns False boolean
"""
account_name = input("Enter account name: ")
con = sqlite3.connect('passwords.db')
query = con.execute("SELECT password_hash, salt FROM user_login WHERE account_name = ?;", (account_name,)).fetchone()
con.close()
if query is None:
print("Account with this name does not exist.")
return False
password_hash, salt = query
password = getpass("Enter master password: ")
ph = argon2.PasswordHasher()
try:
ph.verify(password_hash, password)
return account_name, password, salt
except:
print("Failed to verify entered password. Try again.")
return False
def menu(account_name: str, plaintext_password: str, salt: str):
"""
Main menu upon logging in
"""
while True:
print(
"""
Welcome! What do you wish to do?
> (R)etrieve login details
> (S)ave new website login details
> (D)elete website login details
> Delete profile (type: delete profile)
> (L)ogout
""")
inp = input(">> ")
if inp.lower()[0] == 'r':
retrieve_profile_menu(account_name, plaintext_password, salt)
elif inp.lower()[0] == 's':
save_login_menu(account_name, plaintext_password, salt)
elif inp.lower() == 'delete profile':
status = delete_profile_menu(account_name)
if status:
exit()
elif inp.lower()[0] == 'd':
delete_login_menu(account_name)
elif inp.upper()[0] == 'L':
exit()
def retrieve_profile_menu(account_name: str, plaintext_password: str, salt: str):
"""
Function run when user presses "R"
"""
con = sqlite3.connect('passwords.db')
# create prompt for user to select from
records = con.execute("SELECT * FROM passwords WHERE account_name = ?", (account_name,)).fetchall()
con.close()
prompt = ""
counter = 0
for record in records:
counter += 1
prompt += "{0}. Website/app: {1} // Username: {2}\n".format(counter, record[2], record[1])
prompt += "\nPress 'E' to exit"
print("Choose which password to see:\n", prompt)
# input
while True:
inp = input(">> ")
if inp.lower() == 'e':
print("Exiting")
return
try:
inp = int(inp)
if inp in range(1, counter+1):
break
raise
except:
print("Enter a number that is within the range")
# hash plaintext master password, with salt specific to the user, and use that as key to decrypt
record = records[inp - 1]
hashed_password = argon2.low_level.hash_secret(
secret = plaintext_password.encode(),
salt = salt.encode(),
time_cost = 3,
memory_cost = 65536,
parallelism = 4, hash_len = 30,
type = argon2.Type.ID)
key = Fernet(base64.urlsafe_b64encode(hashed_password[-32:]))
plaintext_website_password = key.decrypt(record[3]).decode()
pyperclip.copy(plaintext_website_password)
print(">>>>>>>>> Website password copied to clipboard!")
def save_login_menu(account_name: str, plaintext_password: str, salt: str):
"""
Function run when user presses "S"
"""
def cancel_wizard(inp: str):
if inp.lower() == 'c':
print("Login wizard cancelled.")
return True
return False
# Enter website name and username
print("New login wizard\nPress (C) to cancel anytime.")
print("Enter website name:")
website = input(">> ")
if cancel_wizard(website):
return
print("Enter username for the site:")
username = input(">> ")
if cancel_wizard(username):
return
# Create a random password or or enter your own
while True:
print("Create random password (R) OR enter your own (O)?")
pass_prompt_answer = input(">> ")
if cancel_wizard(pass_prompt_answer):
return
elif pass_prompt_answer.lower()[0] == 'r':
login_password = ''.join(choice(string.printable) for i in range(30))
break
elif pass_prompt_answer.lower()[0] == 'o':
login_password = getpass("Enter your password:")
break
else:
print("Invalid character entered. Try again or cancel with (C)")
# Encrypt password using key
hashed_password = argon2.low_level.hash_secret(
secret = plaintext_password.encode(),
salt = salt.encode(),
time_cost = 3,
memory_cost = 65536,
parallelism = 4, hash_len = 30,
type = argon2.Type.ID)
key = Fernet(base64.urlsafe_b64encode(hashed_password[-32:]))
encrypted_login_password = key.encrypt(login_password.encode())
con = sqlite3.connect('passwords.db')
with con:
try:
con.execute("""
INSERT INTO passwords (account_name, username, website, encrypted_password)
VALUES (?, ?, ?, ?)
""", (account_name, username, website, encrypted_login_password.decode())
)
except:
print("An error occured.")
else:
print("Login details saved!")
con.close()
def delete_login_menu(account_name: str):
"""
Function run when user presses "D"
"""
con = sqlite3.connect('passwords.db')
# create prompt for user to select from
records = con.execute("SELECT * FROM passwords WHERE account_name = ?", (account_name,)).fetchall()
prompt = ""
counter = 0
for record in records:
counter += 1
prompt += "{0}. Website/app: {1} // Username: {2}\n".format(counter, record[2], record[1])
prompt += "\nPress 'E' to exit"
print("Choose which login details to delete:\n", prompt)
# input
while True:
inp = input(">> ")
if inp.lower() == 'e':
print('Exiting')
con.close()
return
try:
inp = int(inp)
if inp in range(1, counter+1):
break
raise
except:
print("Enter a number that is within the range")
record = records[inp - 1]
print("Are you sure you want to delete these login details? Type the username if you wish to delete it: ")
entered_username = input(">> ")
if record[1] == entered_username:
with con:
con.execute("DELETE FROM passwords WHERE account_name = ? AND username = ? AND website = ?", (account_name, record[1], record[2]))
print("Login details for this username and website sucessfully deleted.")
else:
print("Username does not match with what was entered. Process cancelled.")
con.close()
def delete_profile_menu(account_name) -> bool:
"""
Function run when user types "delete profile"
"""
password = getpass("Enter password for this account:")
# return variable status returns whether the account was deleted or not
status = False
con = sqlite3.connect('passwords.db')
password_hash = con.execute("SELECT password_hash FROM user_login WHERE account_name = ?", (account_name,)).fetchone()[0]
ph = argon2.PasswordHasher()
try:
ph.verify(password_hash, password)
except:
print("Incorrect master password entered.")
else:
print("Are you sure you want to delete the account? Type 'DELETE ACCOUNT' to confirm.")
inp = input(">> ")
if inp.upper() == 'DELETE ACCOUNT':
print("Deleting account...")
with con:
con.execute("DELETE FROM passwords WHERE account_name = ?", (account_name,))
con.execute("DELETE FROM user_login WHERE account_name = ?", (account_name,))
print("Account deleted.")
status = True
else:
print("Cancelled process.")
con.close()
return status
if __name__ == '__main__':
while True:
print("""
Create (N)ew profile,
(L)og in to existing profile
""")
inp = input(">> ")
if inp.lower()[0] == "n":
new_profile()
elif inp.upper()[0] == "L":
out = login()
if type(out) == tuple:
account_name, plaintext_password, salt = out
else:
continue
menu(account_name, plaintext_password, salt)
else:
print("Option does not exist.")