diff --git a/backend/data_handler.py b/backend/data_handler.py index 0095475..180fe5d 100644 --- a/backend/data_handler.py +++ b/backend/data_handler.py @@ -1,163 +1,276 @@ import json +from os import path + +import appdirs from encryption_handler import EncryptionHandler -file_path: str = "/home/kosh/.local/share/kosh_pass/data.json" -password_check_path: str = "/home/kosh/.local/share/kosh_pass/password_check" - class DataHandler: - def __get_file_data(self) -> dict[str, dict[str, str]]: - with open(file_path, "r", encoding="utf8") as file: - return json.load(file) + """ + WARNING:- + THere is no fail-safe for if the app is opened for the first time. - def __write_file_data(self, data: dict[str, dict[str, str]]) -> None: - with open(file_path, "w", encoding="utf8") as file: - json.dump(data, file) + A class that handles the stored data. + This class does NOT handle encryption directly. Look at ./encryption_handler.py + When a instance is created, the instance should be used for one user ONLY. - def set_password(self, password: str) -> None: - with open(password_check_path, "wb") as password_check_file: - password_check_file.write( - EncryptionHandler.encrypt("Correct".encode(), password.encode()) - ) + In this class "data" is used. THe following is the format of data:- + data:- { + entry_name: { + field_name: field_value + } + } + Example for "data": + { + "Amazon": { + "Username": "Kosh", + "Password": "Pass1234" + }, + "Matrix": { + "Username": "Kosh", + "Email": "kosh@fake.com", + "id": "@kosh:matrix.com" + } + } - def verify_password(self, password: str) -> None: - with open(password_check_path, "rb") as password_check_file: - EncryptionHandler.decrypt(password_check_file.read(), password.encode()) - - def change_password(self, old_password: str, new_password: str) -> None: - self.verify_password(old_password) - data: dict[str, dict[str, str]] = {} + Attributes:- + __file_path (private):- The path to the file where the users encrypted data is stored. + __password (private):- The users password. + """ - for entry_name in self.get_entry_names(): - data[entry_name] = {} - for field_name in self.get_field_names(entry_name): - data[entry_name][field_name] = \ - self.get_field_data(entry_name, field_name, old_password) + __file_path: str + __user_name: str + __password: str - self.set_password(new_password) - self.__write_file_data({}) + def __init__(self, user_name: str, password: str) -> None: + """ + Logs in the user + Parameters:- + user_name + password + Raises:- + If user_name does not exist, ValueError will be thrown + If password is wrong, ValueError will be thrown + WARNING:- + If this is the first time the app is opened, the user_name will be shown as wrong + """ + self.__file_path = appdirs.user_data_dir(appname="Unnamed_Password_Manager") + self.__user_name = user_name + self.__file_path = path.join(self.__file_path, self.__user_name) + self.__password = password - for entry_name in data: - self.add_entry(entry_name) - for field_name in data[entry_name]: - self.add_field( - entry_name, field_name, data[entry_name][field_name], new_password - ) + if not path.exists(self.__file_path): + raise ValueError(f"User {user_name} does not exist.") - def add_entry(self, entry_name: str) -> None: - data = self.__get_file_data() + # This will call encryption handler, + # which will throw an error if the password is wrong + self.get_data() + + @staticmethod + def create_user(user_name: str, password: str) -> None: + """ + Creates a new user. + Parameters:- + user_name + password + Raises:- + If user_name exists, ValueError is thrown + """ + file_path = path.join( + appdirs.user_data_dir(appname="Unnamed_Password_Manager"), + user_name + ) + if path.exists(file_path): + raise ValueError(f"User {user_name} already exists") + with open(file_path, "wb") as file: + file.write(EncryptionHandler.encrypt(b"{}", password.encode())) + + def get_data(self) -> dict[str, dict[str, str]]: + """ + Get the data of the user. + Return:- + data:- Read the class documentation for more information. + """ + with open(self.__file_path, "rb") as file: + data: str = \ + EncryptionHandler.decrypt(file.read(), self.__password.encode())\ + .decode() + return json.loads(data) + + def write_data(self, data: dict[str, dict[str, str]]) -> None: + """ + Write the data of the user. + Parameters:- + data:- Read the class documentation for more information + """ + json_data: str = json.dumps(data) + encrypted_data: bytes = EncryptionHandler.encrypt( + json_data.encode(), + self.__password.encode() + ) + with open(self.__file_path, "wb") as file: + file.write(encrypted_data) + + def change_password(self, password: str) -> None: + """ + Changes the password of the user. + Parameters:- + password + """ + data: dict[str, dict[str, str]] = self.get_data() + self.__password = password + self.write_data(data) + + def add_entry(self, entry_name: str, fields: dict[str, str]) -> None: + """ + Add a new entry:- + Parameters:- + entry_name + fields:- { + field_name_1: field_value_1, + field_name_2: field_value_2 + } + Raises:- + A ValueError is raised if entry_name already exists. + """ + data: dict[str, dict[str, str]] = self.get_data() if entry_name in data: - raise ValueError(f"An entry with the name {entry_name} already exists.") + raise ValueError(f"Entry {entry_name} already exists.") - data[entry_name] = {} - self.__write_file_data(data) + data[entry_name] = fields + self.write_data(data) def delete_entry(self, entry_name: str) -> None: - data = self.__get_file_data() - try: - data.pop(entry_name) - except KeyError: - raise ValueError(f"No entry with name \"{entry_name}\"") from KeyError - self.__write_file_data(data) + """ + Deletes the given entry. + Parameters:- + entry_name + Errors:- + If the requested entry does not exist, an error will be thrown + """ + data: dict[str, dict[str, str]] = self.get_data() + if entry_name not in data: + raise ValueError(f"{entry_name} does not exist.") + data.pop(entry_name) + self.write_data(data) - def edit_entry_name(self, entry_name: str, new_entry_name: str) -> None: - if new_entry_name in self.get_entry_names(): + def edit_entry_name(self, old_entry_name: str, new_entry_name: str) -> None: + """ + Changes the name of the entry. + Parameters:- + old_entry_name + new_entry_name + Errors:- + If the old_entry_name does not exist, a ValueError will be thrown. + If new_entry_name already exists, a ValueError will be raised. + """ + data: dict[str, dict[str, str]] = self.get_data() + if new_entry_name in data: raise ValueError(f"\"{new_entry_name}\" already exists") - if entry_name not in self.get_entry_names(): - raise ValueError(f"No entry with name \"{entry_name}\"") + if old_entry_name not in data: + raise ValueError(f"No entry with name \"{old_entry_name}\"") - fields: dict[str, str] = self.__get_file_data()[entry_name] - self.delete_entry(entry_name) - data = self.__get_file_data() - data[new_entry_name] = fields - self.__write_file_data(data) + self.delete_entry(old_entry_name) + self.add_entry(new_entry_name, data[old_entry_name]) - def get_entry_names(self, ) -> list[str]: - return list(self.__get_file_data().keys()) - - def add_field(self, + def add_field(self, entry_name: str, field_name: str, field_value: str, - password: str ) -> None: - self.verify_password(password) - if entry_name not in self.get_entry_names(): - raise ValueError(f"No entry with name {entry_name}") - if field_name in self.get_field_names(entry_name): + """ + Adds a new field to a given entry. + Parameters:- + entry_name + field_name + field_value + Raises:- + If entry_name does not exist, a ValueError will be raised. + If field_name already exists, a ValueError will be raised. + """ + data: dict[str, dict[str, str]] = self.get_data() + if entry_name not in data: + raise ValueError(f"No entry with name \"{entry_name}\"") + if field_name in data[entry_name]: raise ValueError(f"Field {field_name} already exists under entry {entry_name}") - - data: dict[str, dict[str, str]] = self.__get_file_data() - data[entry_name][field_name] = EncryptionHandler.encrypt( - field_value.encode(), - password.encode() - ).decode("unicode-escape") - self.__write_file_data(data) + data[entry_name][field_name] = field_value + self.write_data(data) def delete_field(self, entry_name: str, field_name: str) -> None: - if entry_name not in self.get_entry_names(): + """ + Adds a new field to a given entry. + Parameters:- + entry_name + field_name + Raises:- + If entry_name does not exist, a ValueError will be raised. + If field_name does not exists, a ValueError will be raised. + """ + data: dict[str, dict[str, str]] = self.get_data() + if entry_name not in data: raise ValueError(f"No entry with name \"{entry_name}\"") - if field_name not in self.get_field_names(entry_name): - raise ValueError(f"{entry_name} does not have field {field_name}") - data = self.__get_file_data() + if field_name not in data[entry_name]: + raise ValueError(f"No field {field_name} under entry {entry_name}") data[entry_name].pop(field_name) - self.__write_file_data(data) + self.write_data(data) - def edit_field_name(self, entry_name: str, field_name: str, new_field_name: str) -> None: - if entry_name not in self.get_entry_names(): + def edit_field_name( + self, + entry_name: str, + old_field_name: str, + new_field_name: str + ) -> None: + """ + Change the name of a field under the given entry. + Parameters:- + entry_name + old_field_name + new_field_name + Raises:- + If entry_name does not exist, a ValueError will be raised. + If old_field_name does not exists, a ValueError will be raised. + If field_name already exists, a ValueError will be raised. + """ + data: dict[str, dict[str, str]] = self.get_data() + if entry_name not in data: raise ValueError(f"No entry with name \"{entry_name}\"") - if field_name not in self.get_field_names(entry_name): - raise ValueError(f"{entry_name} does not have field {field_name}") - if new_field_name in self.get_field_names(entry_name): - raise ValueError(f"{entry_name} does has a field {new_field_name}") - field_data = self.__get_file_data()[entry_name][field_name] - self.delete_field(entry_name, field_name) - data = self.__get_file_data() - data[entry_name][new_field_name] = field_data - self.__write_file_data(data) + if old_field_name not in data[entry_name]: + raise ValueError(f"No field {old_field_name} under entry {entry_name}") + if new_field_name in data[entry_name]: + raise ValueError(f"Field {new_field_name} already exists under {entry_name}") + data[entry_name][new_field_name] = data[entry_name][old_field_name] + data[entry_name].pop(old_field_name) + self.write_data(data) - def edit_field_value(self, + def edit_field_value(self, entry_name: str, field_name: str, new_value: str, - password: str ) -> None: - self.verify_password(password) - if entry_name not in self.get_entry_names(): - raise ValueError(f"No entry called {entry_name}") - if field_name not in self.get_field_names(entry_name): - raise ValueError(f"No field called {field_name} under {entry_name}") - self.delete_field(entry_name, field_name) - self.add_field(entry_name, field_name, new_value, password) - - def get_field_data(self, entry_name: str, field_name: str, password: str) -> str: - self.verify_password(password) - if field_name not in self.get_field_names(entry_name): - raise ValueError(f"No field with name {field_name} under {entry_name}.") - encrypted_data = self.__get_file_data()[entry_name][field_name].\ - encode("ISO-8859-1") - plain_data = EncryptionHandler.decrypt( - encrypted_data, - password.encode("ISO-8859-1") - ).decode("utf-8") - return plain_data - - def get_field_names(self, entry_name: str) -> list[str]: - data = self.__get_file_data() + """ + Change the value of the given field under the given entry. + Parameters:- + entry_name + field_name + new_value + Raises:- + If entry_name does not exist, a ValueError will be raised. + If field_name does not exists, a ValueError will be raised. + """ + data: dict[str, dict[str, str]] = self.get_data() if entry_name not in data: - raise ValueError(f"No entry with name {entry_name}.") - return list(data[entry_name].keys()) + raise ValueError(f"No entry with name \"{entry_name}\"") + if field_name not in data[entry_name]: + raise ValueError(f"No field {field_name} under entry {entry_name}") + data[entry_name][field_name] = new_value + self.write_data(data) - def add_entry_and_fields( - self, - entry_name: str, - fields: dict[str, str], - password: str - ) -> None: - self.verify_password(password) - if entry_name in self.get_entry_names(): - raise ValueError(f"Entry {entry_name} already exists.") - self.add_entry(entry_name) - for field_name, field_value in fields.items(): - print(field_name, field_value) - self.add_field(entry_name, field_name, field_value, password) + +def test(): + #DataHandler.create_user("Kosh", "p") + d = DataHandler("Kosh", "p") + d.edit_field_value("Not Amazon", "p", "v") + + +if __name__ == "__main__": + test() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..c013c3c --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,2 @@ +appdirs==1.4.4 +pycryptodome==3.18.0