-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNotesApp.py
More file actions
453 lines (364 loc) · 18.6 KB
/
NotesApp.py
File metadata and controls
453 lines (364 loc) · 18.6 KB
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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
#Created by Robby Thornton(J00717042), Reeshi Ghosal (J00687363), William Starling (J00709906), Jack Mason (J00706241)
import tkinter as tk
from tkinter import simpledialog, messagebox, font
import os
import base64
import time
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from security_access import security_access
from notes_access import notes_access
class NoteGui:
#initilization for the app called at end of file
def __init__(self):
os.makedirs("Accounts", exist_ok=True)
self.accounts_path = os.path.join(os.getcwd(), "Accounts")
# Main window
self.notes = tk.Tk()
self.notes.title("Encrypted Notes Login")
self.notes.geometry("1000x600")
self.user_name = None
self.password_box = None
self.load_login()
self.notes.mainloop()
# --------------------------------------------------------
# LOGIN + ACCOUNT MANAGEMENT
# --------------------------------------------------------
#checks users login information
def login_check(self):
user = user_name.get()
pwd = password_box.get() # store password before destroying window
checked_return = self.check_user_ui(user, pwd)
user_name.delete(0, tk.END)
password_box.delete(0, tk.END)
#if valid user and pass, open the account
if checked_return == "Valid":
self.notes.withdraw()
self.open_notes_app(user, pwd)
elif checked_return == "Invalid":
messagebox.showerror("Error", "Invalid Username Or Password", parent=self.notes)
#UI for creating a new account.
def create_acct(self):
new_user:simpledialog
new_pass:simpledialog
can_add = False
users = security_access.Security.load_users()
#loop so it keeps popping up until you close it or put in a valid name
choosing_user = True
while choosing_user:
new_user = simpledialog.askstring("Input", "Enter New Account Name:", parent=self.notes)
if new_user not in users:
break
else:
messagebox.showerror("Error", "Invalid Username", parent=self.notes)
#loop so it keeps popping up until you close it or put in a valid password
chooing_password = True
while chooing_password:
new_pass = simpledialog.askstring("Input", "Enter A Password:", parent=self.notes)
if new_pass is None:
break
#check password is same in verify box
conf_pass = simpledialog.askstring("Input", "Confirm Password:", parent=self.notes)
if new_pass == conf_pass:
can_add = True
break
else:
messagebox.showerror("Error", "Passwords Do Not Match", parent=self.notes)
if can_add:
security_access.Security.add_new_user(new_user,new_pass)
#UI function for checking user credentials.
def check_user_ui(self, user_name:str, password:str):
checked_account, user = security_access.Security.check_user(user_name, password)
now = time.time()
#check if user can attempt login
if checked_account == "LockedOut":
remaining = int(user["lockoutUntil"] - now)
messagebox.showerror("Error", f"Account locked. Try again in {remaining} seconds.")
return "LockedOut"
elif checked_account == "Valid":
return "Valid"
#if the user failed to login at the max amount of times lock em out
elif checked_account == "Blocked":
if user["failedAttempts"] >= user.get("maxAttempts", 3):
user["lockoutUntil"] = now + user.get("timeoutTime", 30)
messagebox.showerror("Error",f"Too many failed attempts. Locked for {user['timeoutTime']} seconds.")
return "Invalid"
else:
return "Invalid"
#loads the login screen after logout
def load_login(self):
global user_name, password_box
login_name_frame = tk.Frame(self.notes)
login_name_frame.pack(pady=20)
tk.Label(login_name_frame, text="Username", width=15).pack(side=tk.LEFT, padx=5)
user_name = tk.Entry(login_name_frame, width=30)
user_name.pack(side=tk.LEFT, padx=5)
login_pass_frame = tk.Frame(self.notes)
login_pass_frame.pack(pady=10)
tk.Label(login_pass_frame, text="Password", width=15).pack(side=tk.LEFT, padx=5)
password_box = tk.Entry(login_pass_frame, width=30, show="*")
password_box.pack(side=tk.LEFT, padx=5)
tk.Button(self.notes, text="Login", width=15, command=self.login_check).pack(pady=20)
tk.Button(self.notes, text="Create New Account", width=15, command=self.create_acct).pack(pady=20)
#Opens new window upon successful login
def open_notes_app(self, acct_name:str, acct_password:str):
#dont touch stops an error
after_notes_app = None
current_user = {"username": acct_name, "password": acct_password}
#create app window
notes_app = tk.Toplevel(self.notes)
notes_app.title("Notes")
notes_app.geometry("1000x600")
big_font = font.Font(family="Helvetica", size=25)
acct_path = os.path.join(os.getcwd(),self.accounts_path,acct_name)
os.makedirs(acct_path, exist_ok=True)
#what to do if you close the first app
def on_notes_app_close():
notes_app.destroy()
self.notes.destroy()
notes_app.protocol("WM_DELETE_WINDOW", on_notes_app_close)
#default sort is aphabetical
sort = "Alpha"
def set_sort(by):
nonlocal sort
sort = by
def check_files():
nonlocal after_notes_app
after_notes_app = None
#remove old file buttons
for widget in scrollable_frame.winfo_children():
widget.destroy()
#all the notes in a dir
note_files = notes_access.NotesAccess.list_notes(acct_path)
#how to sort
if sort == "Alpha":
note_files.sort()
elif sort == "Recent":
note_files.sort(key=lambda f: os.path.getmtime(os.path.join(acct_path, f)), reverse=True)
#makes each file button
for file in note_files:
tk.Button(
scrollable_frame,
text=file[:-4],
font=big_font,
width=20,
height=2,
command=lambda f=file: self.open_note(acct_path, f, acct_password),
).pack(side=tk.TOP, pady=2)
#used to stop an error
#also restartes the check process every second
after_notes_app = notes_app.after(1000, check_files)
def create_file():
#name of file
file_name = simpledialog.askstring("Input", "Enter file name:", parent=notes_app)
if not file_name or file_name.strip() == "":
messagebox.showinfo("Error", "Invalid file name", parent=notes_app)
return
#path of file
filepath = os.path.join(acct_path, file_name + ".txt")
if os.path.exists(filepath):
messagebox.showinfo("Error", "File Already Exists", parent=notes_app)
return
#does it use its own password
use_separate = messagebox.askyesno("File Password", "Would you like to set a separate password for this file?")
#creates new password if use Seperate is yes
new_password = ""
if use_separate:
new_password = simpledialog.askstring("Input", "Enter a file password:", show="*")
if new_password is None or new_password.strip() == "":
messagebox.showinfo("Error", "File password cannot be blank.", parent=notes_app)
return
conf = simpledialog.askstring("Input", "Confirm file password:", show="*")
if new_password != conf:
messagebox.showinfo("Error", "Passwords do not match.", parent=notes_app)
return
#encrypted blank for a file thats empty
encrypted_blank = security_access.Security.create_file(new_password, use_separate, acct_path, acct_password, filepath)
notes_access.NotesAccess.save_encrypted_file(filepath, encrypted_blank)
#scroll control
def on_scroll(event):
if scrollable_frame.winfo_reqheight() > canvas.winfo_height(): #only scrolls if the amount is bigger than window height
if event.num == 4 or event.delta > 0:
canvas.yview_scroll(-1, "units")
elif event.num == 5 or event.delta < 0:
canvas.yview_scroll(1, "units")
#the clickable scroll bar
def on_frame_configure(event):
canvas.configure(scrollregion=canvas.bbox("all"))
#sets mouse wheel to scroll if on within anywhere on the window
def bind_mousewheel(event):
canvas.bind_all("<MouseWheel>", on_scroll)
#undoes the mouse bind
def unbind_mousewheel(event):
canvas.unbind_all("<MouseWheel>")
##new notes button
notes_button_frame = tk.Frame(notes_app)
notes_button_frame.pack(pady=5)
tk.Button(notes_button_frame, text="Create File", width=25, command=create_file).pack(pady=2)
##change settings button
change_user_settings_button = tk.Button(notes_button_frame,width=25, text = "Change Settings",command=lambda: change_settings(self))
change_user_settings_button.pack(pady=2)
sort_by_alpha = tk.Button(notes_button_frame, text="Sort A-Z", width=25, command=lambda: set_sort("Alpha"))
sort_by_alpha.pack(pady=2)
sort_by_recent = tk.Button(notes_button_frame,width=25, text = "Sort Recent",command=lambda: set_sort("Recent"))
sort_by_recent.pack(padx=2)
#used to change user settings
def change_settings(self):
settings = tk.Tk()
settings.title("Settings")
settings.geometry("800x500")
settings_frame = tk.Frame(settings)
settings_frame.pack(pady=2)
#find the user so settings can be changed on the json
users = security_access.Security.load_users()
for user in users:
if user["username"] == acct_name:
user_temp = user
break
#change time some ones locked out after reaching max attempts
#must be atleast 5 seconds at most 600 seconds
change_timeout_time = tk.Button(settings_frame, width=35, text = "Change Failed Login Timeout Time",command=lambda: change_timeout_timer())
change_timeout_time.pack(pady=2)
def change_timeout_timer():
new_timeout = simpledialog.askinteger("Change Timeout", "Enter new timeout time (in seconds):", parent=settings,minvalue=5, maxvalue=600)
if new_timeout:
user_temp["timeoutTime"] = new_timeout
security_access.Security.save_users(users)
messagebox.showinfo("Success", f"Timeout updated to {new_timeout} seconds.", parent=settings)
#change max amount of login attempts
#must be atleast 1 at most 10
change_login_tries = tk.Button(settings_frame, width=35, text = "Change Max Login Tries Before Lockout",command=lambda: change_login_attempts())
change_login_tries.pack(pady=2)
def change_login_attempts():
new_max_attempts = simpledialog.askinteger("Change Max Attempts", "Enter new Max login attempts:", parent=settings,minvalue=1, maxvalue=10)
if new_max_attempts:
user_temp["maxAttempts"] = new_max_attempts
security_access.Security.save_users(users)
messagebox.showinfo("Success", f"Max Attempts updated to {new_max_attempts}.", parent=settings)
#changeUserName
change_user_name = tk.Button(settings_frame, width=35, text = "Change Username",command=lambda: change_username_ui())
change_user_name.pack(pady=2)
def change_username_ui():
new_username = simpledialog.askstring("Change UserName", "Enter new Username:", parent=settings)
#if blank or already taken throw invalid
if not new_username:
messagebox.showerror("Error", "Username is Invalid.", parent=settings)
return
if new_username:
if any(u["username"] == new_username for u in users):
messagebox.showerror("Error", "Username is Invalid.", parent=settings)
return
#else actually change it
nonlocal acct_path
acct_path = security_access.Security.change_username(new_username, user_temp, current_user, self.accounts_path, users)
messagebox.showinfo("Success", f"Username changed to {new_username}.", parent=settings)
#change password
change_password = tk.Button(settings_frame, width=35, text = "Change Password",command=lambda: change_password_ui())
change_password.pack(pady=2)
def change_password_ui():
new_password = simpledialog.askstring("Change Password", "Enter new Password:", parent=settings,show="*")
if not new_password:
messagebox.showinfo("Error", "Password is Invalid.", parent=settings)
return
confirm_password = simpledialog.askstring("Confirm Password", "Confirm Password:", parent=settings,show="*")
if new_password != confirm_password:
messagebox.showerror("Error", "Passwords do not match.", parent=settings)
return
security_access.Security.change_password(new_password, users, user_temp)
messagebox.showinfo("Success", "Password updated successfully.", parent=settings)
#exit settings menu button same as closing window
exit_button = tk.Button(settings_frame, width=25, text = "EXIT",command=lambda: settings.destroy())
exit_button.pack(pady=2)
#logs user out
exit_to_login = tk.Button(notes_button_frame,width=25, text = "Logout",command=lambda: Exit_to_login(self))
exit_to_login.pack(pady=2)
def Exit_to_login(self):
nonlocal after_notes_app
if after_notes_app:
notes_app.after_cancel(after_notes_app)
after_notes_app = None
notes_app.destroy()
self.notes.deiconify()
#ui stuff for looks
separator_line = tk.Frame(notes_app, height=2, bd=1, relief="sunken")
separator_line.pack(fill="x", padx=5, pady=5)
#ui stuff for the notes frame
notes_frame = tk.Frame(notes_app)
notes_frame.pack(fill="both", expand=True)
canvas = tk.Canvas(notes_frame)
scrollbar = tk.Scrollbar(notes_frame, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
#allows notes to be scrollable
scrollable_frame = tk.Frame(canvas)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
def on_frame_configure(event):
canvas.configure(scrollregion=canvas.bbox("all"))
scrollable_frame.bind("<Configure>", on_frame_configure)
canvas.bind("<Enter>", bind_mousewheel)
canvas.bind("<Leave>", unbind_mousewheel)
check_files()
#Used to open an actual note
def open_note(self,account_path,fileName,account_password):
#finds the path of the file by hte name of it
path = os.path.join(account_path, fileName)
file_key_path = path + ".filekey"
cipher = None
file_password = None
if os.path.exists(file_key_path):
salt = open(file_key_path, "rb").read()
file_password = simpledialog.askstring("Input", "Enter file password:", show="*")
cipher = security_access.Security.load_file_encryption(file_key_path, account_path, account_password, file_password)
#needed for a potential error handling dont touch
save_after = None
#saves files data
def save_encrypted_file(data):
encrypted = security_access.Security.save_encrypted_file(data, cipher)
notes_access.NotesAccess.save_encrypted_file(path, encrypted)
#load file if it has a password
def load_encrypted_file():
encrypted = notes_access.NotesAccess.load_encrypted_file(path)
if not encrypted:
return None
try:
decrypted = security_access.Security.load_encrypted_file(encrypted, cipher)
return decrypted
except Exception:
messagebox.showerror("Error", "Incorrect file password")
return None
#deletes a file and closes the window for it
def delete_file():
notes_access.NotesAccess.delete_note_file(path)
note.destroy()
#used if file is password protected
content = load_encrypted_file()
if content is None:
messagebox.showerror("Error", "Incorrect file password")
return
#note ui stuff
note = tk.Toplevel()
note.title(fileName[:-4])
note.geometry("1000x600")
notes_frame = tk.Frame(note)
notes_frame.pack()
textbox = tk.Text(note, wrap="word")
textbox.pack(fill="both", expand=True)
textbox.insert("1.0", content)
#button for deleting file
tk.Button(notes_frame, text="Delete File", command=delete_file).pack(pady=2)
#save feature automatically done
def save():
nonlocal save_after
save_encrypted_file(textbox.get("1.0", tk.END))
save_after = note.after(1000, save)
#what to do when the windows closed
def on_close():
nonlocal save_after
if save_after:
note.after_cancel(save_after)
save_after = None
note.destroy()
note.protocol("WM_DELETE_WINDOW", on_close)
save()
app = NoteGui()