From 6c7883657d1a548a2049252f50ad93e7cd570977 Mon Sep 17 00:00:00 2001 From: Lucas Mathews Date: Wed, 29 May 2024 19:06:02 +0200 Subject: [PATCH] client side hashing, shared via JSON, password reset working --- api.yml | 105 ++++++++++------- .../__pycache__/connection.cpython-312.pyc | Bin 11341 -> 13774 bytes application/account.py | 11 +- application/app.ini | 2 +- application/connection.py | 63 +++++++++-- application/dashboard.py | 88 +++++++++++++- application/login.py | 17 ++- application/session_data.json | 2 +- application/transaction.py | 2 - manager.py | 107 +++++++++++------- test_database.db | Bin 700416 -> 700416 bytes 11 files changed, 290 insertions(+), 107 deletions(-) diff --git a/api.yml b/api.yml index 1fd6357..048b9b1 100644 --- a/api.yml +++ b/api.yml @@ -29,24 +29,46 @@ paths: summary: Log in to the system description: Log in to the system operationId: manager.login - parameters: - - name: client_id - in: query - description: client_id - required: true - schema: - type: string - - name: password - in: query - description: Password - required: true - schema: - type: string + requestBody: + description: Login credentials + required: true + content: + application/json: + schema: + type: object + properties: + client_id: + type: string + description: Client ID + client_hash: + type: string + description: Hashed password + required: + - client_id + - client_hash responses: '200': description: Successful operation + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string '401': description: Invalid Client ID/password supplied + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string /Client/Logout: post: tags: @@ -78,39 +100,40 @@ paths: summary: Change password description: Change password operationId: manager.change_password - parameters: - - name: client_id - in: query - description: ID of client to change password - required: true - schema: - type: string - - name: password - in: query - description: New password - required: true - schema: - type: string - - name: new_password - in: query - description: New password - required: true - schema: - type: string - - name: otp - in: query - description: OTP to verify - required: true - schema: - type: integer - format: int32 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + client_id: + type: string + description: ID of client to change password + hash_old_password: + type: string + description: Old password as a hash + hash_new_password: + type: string + description: New password as a hash + otp_code: + type: integer + format: int32 + description: OTP to verify + required: + - client_id + - hash_old_password + - hash_new_password + - otp_code responses: '200': description: Password changed successfully '400': - description: Old password incorrect + description: Validation error + '401': + description: Unauthorised '404': - description: client_id not found + description: Client not found /OTP/Generate: post: tags: diff --git a/application/__pycache__/connection.cpython-312.pyc b/application/__pycache__/connection.cpython-312.pyc index 60315baf0536a6733ae9b675abf51f44488184e9..c33f362e4ac442a5b7bc00fe9b83e5bc6cbb09d6 100644 GIT binary patch delta 4469 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 AM*si- delta 2405 zcma)8TWl0n7@jk;vpaj=?v_%DUAE=2OiS4+6rmtep|yZ62sM>8uo-8jWy^MFJu|gH zfHhbXo>X!k#1J2Os63hVfspvXgNc`D^vw+N<_ix-ZZW1${{QS{7PCYrnQ#7c{&V^M z%lXgzbmqsmB0q$~0S=yr{nIla_g#&2SC*jlDW`E7DRMdd@q*&r|fy`*J?$6PLK0qQXi9-HB{ z$dZ`r(4sgxuEpS!I7j5f)$ebS{pmi&KZs=~-g^RrAyTIptjebpnmegon;8p$vr}-}m zmxSZ>?;=^`&I%{EMIr~z9~fJ(PU|JBpemMbrj(RgWF?O*l@-%mG^oa|dj^M^Et%Nf zvO<-4b0vOEe|JGQt?{#}UbYHGX*6~J3GjT(6zODlR`%Nxt&K2kPuVan>H$)0pP1Zj zi)C6USyX@yJ7`*pwP5B|L(^>uJ|zq2$Q~L+ZB#85pqX#dnG>JyTnCyic+C$1-sc{8 zB(a+*=|uY0%fIv;`dOKKz;WluB>#}(rpQ753CDFF;;%@*Mps{n{{34p!M^g3C)dK< z%bN$eo4sSB!W}6+HX_`~5R^y6vC(SQ`wj7;vuT8_k+#m!ILZlxP5?P%dyKMPvOROA z0a!E?jjlp15_3$Yt5NDkSc8Dp(jEldTK3pv#txafX@c+anqn!m7nMo2&fg~vfY@Ih z_kYcg^kHv5K$h0{fVkqz>~|bP*P|9)wan8ED0d*l*=T5QWtq2q=7Ori9=2~DP?^=` z0NscqHX)=DkhLw$>a_sbF{%_yJwId6d@VBnvO^Y~?dWElDT^Tg%EKKk$DgYJ71|3b&tvCq-BZt~q)e?(sI1&S5`_pgw z;9BeWw;rjFI?^Jpv8Q^w#-2%*?&=rEVg6J#+chp!+Hqa!%uG?GWGX72=gefm zv< zSM`;(f02Q<+@yvBYyNZg8Dj7BPHUI|Ee>p-;k3npFuoS`rW2n*+*dam4rOn$Pdthf ztNW9?3E5Gttow{_@gk(OLkW||4r;oo(t=YKZLwrPWpy)xzK+W{=hlh;C^RSNZtTSP z(>)0Pk=eFTUV!uJtkg_x`V!7`DvX=cewP+KhWg_OZh%_T`S<>`_$H_?S7Ynnekw&A z)5172f-s5TTJZk@;1))gakDx?0FL0IEyVCzZLEm(7skprVk65mIQH5Y?Imf za0W_6=2g9_N+mBiuix5;p zm$JjP;3?M~c(Nk<_q&K62NZ>V30z421A&(`F#O`q$ l?s04Ga>09C@==H3-DBu8fQXANPdG1flpJ@(&bx>^lGe diff --git a/application/account.py b/application/account.py index a321b95..badde45 100644 --- a/application/account.py +++ b/application/account.py @@ -12,6 +12,7 @@ from connection import get_transactions, format_balance, get_account, generate_o description_entry = None notes_entry = None otp_entry = None +notes_text = None ################# ### Functions ### @@ -64,6 +65,8 @@ def display_account_info(account_id): label_value = customtkinter.CTkLabel(info_frame, text=value, font=("Helvetica", 14)) label_key.grid(row=0, column=i*2, sticky='w', padx=10) label_value.grid(row=0, column=i*2+1, sticky='w', padx=10) + global notes_text + notes_text.configure(text=account.get('notes', '')) # Use config instead of configuration def on_transaction_double_click(event): """Handles double-click event on a transaction in the table.""" @@ -157,7 +160,7 @@ def save_details(): root = customtkinter.CTk() root.title(f"Transactions for: {account_description}") root.iconbitmap("application/luxbank.ico") -root.geometry("800x400") +root.geometry("800x450") if CONFIG["preferences"]["dark_theme"] == "dark": # Check if dark mode is enabled customtkinter.set_appearance_mode("dark") # Set the style for dark mode @@ -168,6 +171,12 @@ else: welcome_label = customtkinter.CTkLabel(root, text=f"Transactions for: {account_description}", font=("Helvetica", 24)) welcome_label.pack(pady=10) +# Create the notes label and text box +notes_label = customtkinter.CTkLabel(root, text="Notes:", font=("Helvetica", 14)) +notes_label.pack(pady=10) +notes_text = customtkinter.CTkLabel(root, height=4, width=50, wraplength=400) # Use CTkLabel instead of CTkTextbox +notes_text.pack(pady=10, fill=tk.BOTH, expand=True) # Add fill and expand options + # Display account information info_frame = customtkinter.CTkFrame(root) info_frame.pack(fill=tk.X) diff --git a/application/app.ini b/application/app.ini index 00e8b3a..e0ea061 100644 --- a/application/app.ini +++ b/application/app.ini @@ -9,5 +9,5 @@ theme = dark-blue [client] default_id = d18e5ae0 -default_password = Happymeal1 +default_password = KFCKrusher1 diff --git a/application/connection.py b/application/connection.py index 60a41d7..982b451 100644 --- a/application/connection.py +++ b/application/connection.py @@ -1,10 +1,11 @@ # Lucas Mathews - Fontys Student ID: 5023572 # Banking System Connection Page +import json import requests +import hashlib from requests.models import Response from config import CONFIG -import json from tkinter import messagebox ############## @@ -15,22 +16,25 @@ def format_balance(balance): """Formats the balance as a currency string with comma separators.""" return f"€{balance:,.2f}" +def hash_password(password:str): + """Hashes a password using the SHA-512 algorithm and returns the hexadecimal representation of the hash.""" + return hashlib.sha512(password.encode()).hexdigest() + ##################### ### API Functions ### ##################### -def authenticate_client(client_id, client_password): - """Authenticates a client with the given client_id and client_password.""" +def authenticate_client(client_id, client_hash): + """Authenticates a client with the given client_id and client_hash.""" try: - response = requests.post(CONFIG["server"]["url"] + "/Client/Login", params={'client_id': client_id, 'password': client_password}) + response = requests.post(CONFIG["server"]["url"] + "/Client/Login", json={'client_id': client_id, 'client_hash': client_hash}) + response.raise_for_status() + if response.status_code == 401: + return {'success': False, 'message': "Incorrect password."} return response except requests.exceptions.RequestException as e: - print(f"RequestException: {e}") - response = Response() - response.status_code = 500 - response._content = b'{"success": false, "message": "Could not connect to the server. Please try again later."}' - return response - + raise e # Re-raise the exception to handle it in the login function + def logout_client(): """Logs out the current client.""" try: @@ -175,4 +179,41 @@ def generate_otp(): response.raise_for_status() except requests.exceptions.RequestException as e: print(f"RequestException: {e}") - messagebox.showerror("Error", f"Could not generate OTP: {e}") \ No newline at end of file + messagebox.showerror("Error", f"Could not generate OTP: {e}") + +def change_password(client_id, old_password, new_password, otp_code): + """Changes the password for the given client_id.""" + hash_old_password = hash_password(old_password) + hash_new_password = hash_password(new_password) + try: + otp_code = int(otp_code) # Ensure otp_code is an integer + except ValueError: + return {'success': False, 'message': "Invalid OTP code format: must be an integer."} + + try: + with open('application\\session_data.json', 'r') as f: + session_data = json.load(f) + payload = { # Prepare the payload to be sent in the request body + 'client_id': client_id, + 'hash_old_password': hash_old_password, + 'hash_new_password': hash_new_password, + 'otp_code': otp_code + } + response = requests.put( # Send the PUT request with the payload in the body + CONFIG["server"]["url"] + "/Client/Password", + cookies=session_data['session_cookie'], + json=payload # use json to send the data in the request body + ) + response.raise_for_status() + return response.json() + except requests.exceptions.HTTPError as e: + if response.status_code == 400: + return {'success': False, 'message': response.json().get('message', 'Invalid request.')} + elif response.status_code == 401: + return {'success': False, 'message': response.json().get('message', 'Unauthorised action.')} + elif response.status_code == 404: + return {'success': False, 'message': response.json().get('message', 'Client not found.')} + else: + return {'success': False, 'message': "An error occurred. Please try again later."} + except requests.exceptions.RequestException as e: + return {'success': False, 'message': "Could not connect to the server. Please try again later."} diff --git a/application/dashboard.py b/application/dashboard.py index bf44a45..2f008a9 100644 --- a/application/dashboard.py +++ b/application/dashboard.py @@ -6,7 +6,7 @@ import customtkinter import json import os from config import CONFIG -from connection import logout_client, get_client, update_client, get_accounts, format_balance, generate_otp +from connection import logout_client, get_client, update_client, get_accounts, format_balance, generate_otp, change_password # Global variables @@ -112,7 +112,7 @@ def edit_details(): otp_label.pack() otp_entry.pack() - save_button = customtkinter.CTkButton(edit_window, text="Verify OTP and Save", command=save_details) + save_button = customtkinter.CTkButton(edit_window, text="Verify OTP and Save", command=change_password_save) save_button.pack() edit_window.lift() @@ -186,6 +186,82 @@ def reload_info_and_accounts(): display_client_info() populate_table() +def change_password_box(): + """Opens a new window for changing the client's password.""" + global edit_window,password_entry, old_password_entry, confirm_password_entry, otp_entry + edit_window = customtkinter.CTkToplevel(root) + edit_window.title("Change Password") + edit_window.iconbitmap("application/luxbank.ico") + edit_window.geometry("300x350") + edit_window.attributes('-topmost', True) + + old_password_label = customtkinter.CTkLabel(edit_window, text="Old Password: ") + old_password_entry = customtkinter.CTkEntry(edit_window, show="*") + old_password_label.pack() + old_password_entry.pack() + + customtkinter.CTkLabel(edit_window, text=" ").pack() # Add space under the old password box + + password_label = customtkinter.CTkLabel(edit_window, text="New Password: ") + password_entry = customtkinter.CTkEntry(edit_window, show="*") + password_label.pack() + password_entry.pack() + + confirm_password_label = customtkinter.CTkLabel(edit_window, text="Confirm Password: ") + confirm_password_entry = customtkinter.CTkEntry(edit_window, show="*") + confirm_password_label.pack() + confirm_password_entry.pack() + + customtkinter.CTkLabel(edit_window, text=" ").pack() # Add space under the confirm password box + + otp_button = customtkinter.CTkButton(edit_window, text="Get OTP Code", command=generate_otp) + otp_button.pack() + + otp_label = customtkinter.CTkLabel(edit_window, text="OTP Code: ") + otp_entry = customtkinter.CTkEntry(edit_window) + otp_label.pack() + otp_entry.pack() + + save_button = customtkinter.CTkButton(edit_window, text="Verify OTP and Save", command=change_password_save) + save_button.pack() + edit_window.lift() + +def change_password_save(): + """Saves the updated client password.""" + global edit_window, otp_entry, password_entry, old_password_entry, confirm_password_entry + old_password = old_password_entry.get() + new_password = password_entry.get() + confirm_password = confirm_password_entry.get() + otp_code = otp_entry.get() + + if not otp_code: + messagebox.showerror("Error", "OTP code must be entered.") + return + + if not new_password or not confirm_password: + messagebox.showerror("Error", "New password and confirm password must be entered.") + return + + if new_password != confirm_password: + messagebox.showerror("Error", "New password and confirm password do not match.") + return + + with open('application\\session_data.json', 'r') as f: + session_data = json.load(f) + client_id = session_data['client_id'] + + if not messagebox.askyesno("Confirmation", "Are you sure you want to change the password?"): + return + + try: + response = change_password(client_id, old_password, new_password, otp_code) + if response['success']: + messagebox.showinfo("Success", "Password changed successfully.") + edit_window.destroy() + else: + messagebox.showerror("Error", f"Could not change password: {response['message']}") + except Exception as e: + messagebox.showerror("Error", f"Could not change password: {str(e)}") ############## ### Layout ### @@ -215,13 +291,17 @@ otp_button.grid(row=0, column=0, padx=5) reload_button = customtkinter.CTkButton(button_frame, text="Reload", command=reload_info_and_accounts) reload_button.grid(row=0, column=1, padx=5) +# Create reset password button +password_button = customtkinter.CTkButton(button_frame, text="Reset Password", command=change_password_box) +password_button.grid(row=0, column=2, padx=5) + # Create the logout button logout_button = customtkinter.CTkButton(button_frame, text="Logout", command=logout) -logout_button.grid(row=0, column=2, padx=5) +logout_button.grid(row=0, column=3, padx=5) # Create the exit button exit_button = customtkinter.CTkButton(button_frame, text="Exit", command=exit_application) -exit_button.grid(row=0, column=3, padx=5) +exit_button.grid(row=0, column=4, padx=5) # Display client info after creating the buttons frame = customtkinter.CTkFrame(root) diff --git a/application/login.py b/application/login.py index 4a02854..0fe2bf2 100644 --- a/application/login.py +++ b/application/login.py @@ -4,7 +4,7 @@ import customtkinter import os import json import requests -from connection import authenticate_client +from connection import authenticate_client, hash_password from config import CONFIG import configparser, sys @@ -17,10 +17,11 @@ def login(): """Authenticate the client and open the dashboard if successful.""" client_id = entry_username.get() if entry_username.get() else CONFIG["client"]["default_id"] client_password = entry_password.get() if entry_password.get() else CONFIG["client"]["default_password"] + client_hash = hash_password(client_password) # Hash the password on the client-side try: - response = authenticate_client(client_id, client_password) # Authenticate the client + response = authenticate_client(client_id, client_hash) # Authenticate the client json_response = response.json() # Convert the response content to JSON - if json_response["success"]: # If the authentication is successful, open the dashboard + if response.status_code == 200 and json_response.get("success"): # If the authentication is successful, open the dashboard session_data = { 'session_cookie': response.cookies.get_dict(), 'client_id': client_id @@ -29,10 +30,14 @@ def login(): json.dump(session_data, f) root.destroy() os.system("python application/dashboard.py") + elif response.status_code == 401: + messagebox.showerror("Login failed", "Invalid client ID or password.") else: - messagebox.showerror("Login failed", json_response["message"]) - except requests.exceptions.RequestException as e: - messagebox.showerror("Login failed", f"Could not connect to the server. Please try again later. Error: {str(e)}") + messagebox.showerror("Login failed", json_response.get("message", "Unknown error")) + except requests.exceptions.HTTPError: + messagebox.showerror("Login failed", "Invalid client ID or password.") + except requests.exceptions.ConnectionError: + messagebox.showerror("Connection Error", "Could not connect to the server.") def change_dark_theme(): """Change the theme between dark and light.""" diff --git a/application/session_data.json b/application/session_data.json index 7ff4dbb..0902470 100644 --- a/application/session_data.json +++ b/application/session_data.json @@ -1 +1 @@ -{"session_cookie": {"session": "nwHUYOr9vg2nOaZmrYNmWgjMgJ47QLIz71_dX_kFH_o"}, "client_id": "d18e5ae0"} \ No newline at end of file +{"session_cookie": {"session": "HHymBLOCpW9YTcajilxYA_B9aVPJ1FyS75TAz2995jk"}, "client_id": "d18e5ae0"} \ No newline at end of file diff --git a/application/transaction.py b/application/transaction.py index 0a549aa..7871030 100644 --- a/application/transaction.py +++ b/application/transaction.py @@ -14,8 +14,6 @@ import sys - - ############## ### Layout ### ############## diff --git a/manager.py b/manager.py index e75ca16..f823b47 100644 --- a/manager.py +++ b/manager.py @@ -6,16 +6,18 @@ from class_account import Account from class_transaction import Transaction from emailer import EmailSendingError # Import the EmailSendingError class to handle email sending errors from flask import jsonify, session as flask_session # Imports the Flask modules +from functools import wraps # For decorators / user login +from database import * # Importing the database connection +from emailer import send_email # Importing the emailer function +from flask import session as flask_session +from flask import request +from database import session import hashlib # For password hashing import datetime # For timestamps import uuid # For unique identifiers import random # For OTP generation import time # For OTP generation -from functools import wraps # For decorators / user login -from database import * # Importing the database connection -from emailer import send_email # Importing the emailer function -from flask import session as flask_session -from database import session +import re # For password hash validation otps = {} # Temporary dictionary to store OTPs and their creation time @@ -41,10 +43,8 @@ def generate_uuid_short(): def get_email(client_id:str): """Returns the email of a client given their client_id. If the client is not found, returns None.""" - for client in session.query(Client).all(): - if client.client_id == client_id: - return client.email - return None + client = session.query(Client).filter_by(client_id=client_id).one_or_none() + return client.email if client else None def format_response(success: bool, message: str = '', data: dict = None): """Formats the response for the API so that it is standardised across all functions.""" @@ -68,10 +68,12 @@ def get_current_client(): def verify_otp(client_id:str, otp:int): """Verifies a one time password for a client. Returns True if the OTP is correct and False otherwise.""" - if client_id in otps and otps[client_id][0] == otp: - return True + if client_id in otps: + stored_otp, creation_time = otps[client_id] + if stored_otp == otp and time.time() - creation_time <= 300: # Check if OTP is within 5 minutes + return True return False - + def delete_otp(client_id:str): """Deletes the OTP for a client.""" if client_id in otps: @@ -88,14 +90,18 @@ def check_expired_otps(): ### Authentication ### ###################### -def login(client_id:str, password:str): - """Logs in a client using their client_id and password. Returns a success message if the login is successful and an error message otherwise.""" - password_hash = hash_password(password) - for client in session.query(Client).all(): - if client.client_id == client_id and client.hash == password_hash: - flask_session['client_id'] = client_id - return format_response(True, f"{flask_session['client_id']} logged in succsessfully."), 200 - return format_response(False, "Invalid client_id or password."), 400 +def login(): + """Logs in a client using their client_id and password hash. Returns a success message if the login is successful and an error message otherwise.""" + data = request.get_json() + client_id = data.get('client_id') + client_hash = data.get('client_hash') + + client = session.query(Client).filter_by(client_id=client_id).first() + if client and client.hash == client_hash: + flask_session['client_id'] = client_id + return format_response(True, f"{flask_session['client_id']} logged in successfully."), 200 + return format_response(False, "Invalid client_id or password."), 401 + def logout(): """Logs out a client. Returns a success message if the logout is successful and an error message otherwise.""" @@ -162,14 +168,16 @@ def generate_otp(client_id: str): ############## @login_required -def get_client(client_id:str): +def get_client(client_id: str): """Returns a specific client in the database. If the client is not found, returns an error message.""" current_client_id, is_admin = get_current_client() if not is_admin and client_id != current_client_id: return format_response(False, "You can only view your own client information."), 403 - for client in session.query(Client).all(): - if client.client_id == client_id: - return format_response(True, "", client.to_dict()), 200 + + client = session.query(Client).filter_by(client_id=client_id).one_or_none() + if client: + return format_response(True, "", client.to_dict()), 200 + return format_response(False, "Client not found."), 404 @login_required @@ -192,25 +200,44 @@ def update_client(client_id:str, otp_code:int, **kwargs): return format_response(False, "Client not found."), 404 -@login_required -def change_password(client_id:str, password:str, new_password:str, otp:int): +def change_password(): """Changes the password for a client in the database. If the client is not found, returns an error message.""" + data = request.get_json() + client_id = data.get('client_id') + hash_old_password = data.get('hash_old_password') + hash_new_password = data.get('hash_new_password') + otp_code = data.get('otp_code') current_client_id, is_admin = get_current_client() + + # Verify if the OTP is correct + otp_verified = verify_otp(client_id, otp_code) + + # Check if the client is authorized to change the password if not is_admin and client_id != current_client_id: - return format_response(False, "You can only update your own client information."), 403 - if not verify_otp(client_id, otp): - return format_response(False, "Invalid OTP."), 400 - old_hash = hash_password(password) - new_hash = hash_password(new_password) - for client in session.query(Client).all(): - if client.client_id == client_id: - if client.hash == old_hash: - client.hash = new_hash - session.commit() - delete_otp(client_id) - return format_response(True, f"Password for client_id: {client_id} has been updated."), 200 - return format_response(False, "Invalid password."), 400 - return format_response(False, "Client not found."), 404 + return format_response(False, "You can only update your own client information."), 401 + + # Recheck OTP verification after authorisation check + if not otp_verified: + return format_response(False, "Invalid OTP."), 400 + + # Validate new password format + hash_format = r'^[0-9a-f]{128}$' + if not re.match(hash_format, hash_new_password): + return format_response(False, "Invalid new password format (must be provided as a hash)."), 400 + + # Check if the old password hash matches and update to the new password hash + client = session.query(Client).filter_by(client_id=client_id).first() + if client: + if client.hash == hash_old_password: + client.hash = hash_new_password + session.commit() + delete_otp(client_id) + return format_response(True, f"Password for client_id: {client_id} has been updated."), 200 + else: + return format_response(False, "Invalid old password."), 400 + else: + return format_response(False, "Client not found."), 404 + @login_required def get_accounts(client_id: str): diff --git a/test_database.db b/test_database.db index 7fe9cd29ad9f15aaaeb189ad9b1dd071a88663c8..83e5c1f0fe9d68250a69f268b73e51e46ecc252c 100644 GIT binary patch delta 324 zcmWN_Jxjwt007V?Y15=>Z9#CU#fUgqm)u=?xkCiOj^ZS^n){+%T4<_nZ4u0@bZ{zk z5O=MMi}(WsZ*WyIIypHx=^Ni9YfiG}Roy6GukIV=`;(v1SRfe*g?L0Tfr_yfkc49Z z0){BWD5Q*q5Hn5*3ONn~E-=O|KxqJCsgO`HiwK1Os~8l5N<<}V#U!LqVG;liC5TuY za)238j0@1BJV01U7zx4*v3MWDy@Nq6_mLY^=hfY%&!tY~w=$~)<)?Dn%e-^1R+^Wv z`|KV$U(ThoQk)gH>{nYAz6+zma{e*jwzL&nws~W&84pH#x?c0WuIV-!+o*23$J5)l zoz27kP$quA=MOvG#E&`epZ11+Jdl2z_(>lFjA^tud)cU7=u{i2HdAe-I-lx7s_j%4 KQ|;VoH~Ryotz;Yk delta 169 zcmV;a09OBi;3|ONDv%oi3XvQ`0Sd8T^nR06ADFZ3e;Pncb^ri&519`+4%QBL4gn35 z4K@tP3{nf-3wa9=3aJV?3C{^v2<-@g2p$Ko2Q~)624w~R1&0L}1gr#21Kk5@0{Q}< z0x$u%0ZRbglOYfv1!@5a9UZgXz#|U^WdaEu9V?SCKNXXWz$=q-KOut9hR^|q&;o|g X1BTE9hR_9u&<2Lk2ZqoHrqBuCzm7OP