diff --git a/credsmanager.py b/credsmanager.py new file mode 100644 index 0000000..deccad5 --- /dev/null +++ b/credsmanager.py @@ -0,0 +1,202 @@ +from json import dumps as js_d, loads as js_l +from cryptography.fernet import Fernet +from subprocess import Popen, PIPE +from os.path import exists + + +class CredentialsManager(): + """Used to acces crypted json credentials path using fernet""" + + def __init__(self, credentials_file_path: str, reg_key_path: str = "CredentialsManager", key_name: str = "fernkey"): + """Create a CredentialsManager object used to acces crypted json credentials path using fernet\n + Parameters + ---------- + * credentials_file_path : str\n + The path of your credentials file, MUST be .json, if not or if file doesn't exist, create a new one + * reg_key_path : str\n + The path where the fernet key will be or is stored in the windows registery.\n + >>> "IN THE CURRENT USER REG PATH (HKEY_CURRENT_USER\\) ONLY. ENTER NAME OF THE 'FOLDER' YOU WANT THE KEY TO BE IN"\n + * key_name : str\n + The name of the registery key in which the encryption key will be stored""" + + + # Storing the reg key path in self to acces it later + self.reg_key_path = f"HKEY_CURRENT_USER\\{reg_key_path}" + + # Storing the reg key name in self to acces it later + self.reg_key_name = key_name + + # Get/Create a new key + self.key = self.get_key() + + # Initialise our Fernet object with the key, will raise a error if key wasn't create and returned None + self.Fernet = Fernet(key=self.key) + + + # Replace credential file extansion by .json + # Replace last element after a dot by json in the file path + # Store it in self to acces it later + self.creds_file_path = credentials_file_path.replace(credentials_file_path.split(".")[-1], "json") + + # Check if credentials file exist, if not, try creating new one and write empty line + if not exists(self.creds_file_path): + with open(self.creds_file_path, "w") as f: + f.write("") + + + def get_key(self) -> bytes | None: + """Get the Fernet encryption key in the windows registery, if not found, create a new one""" + # Get key using reg QUERY in subprocess + proces = Popen(f"reg QUERY {self.reg_key_path} /v {self.reg_key_name}", stdout=PIPE, stderr=PIPE) + + # Check if error output is not empty + if proces.stderr.read() !=b'': + # Create a new key and return it + return self.create_key() + + else: + # Return key as binary + return proces.stdout.read().decode("CP850").rsplit()[3].encode() + + + def create_key(self): + """Create a new key, store it in the windows registery and return it""" + # Generate a new key with Fernet + key = Fernet.generate_key() + + # Add it to registery using reg ADD in subprocess + proces = Popen(f"reg ADD {self.reg_key_path} /v {self.reg_key_name} /d {key.decode()}", stdout=PIPE, stderr=PIPE) + + # Check if process executed successfully, if yes return key, if not return None + if proces.stderr.read() == b'': + self.key = key + return key + return + + + def delete_key(self, delete_dir: bool = False): + """Delete the encryption key from the windows registery, return True if removed successfully, and False if not.\n + >>> "WARNING YOU CAN'T RETRIEVE ENCRYPTED DATA AFTER REMOVAL"\n + Parameters + ----------- + * delete_dir bool (Defaukt = False)\n + If set on True, also delete the directory of the stored key\n + >>> "WARNING ALL THE KEYS CONTAINED IN THE DIRECTORY WILL BE DELETED" """ + + if delete_dir: + # Delete directory and all the key from registery using reg DELETE in subprocess + command = f"reg DELETE {self.reg_key_path} /f" + else: + # Delete key from registery using reg DELETE in subprocess + command = f"reg DELETE {self.reg_key_path} /v {self.reg_key_name} /f" + + # Execute command + proces = Popen(command, stdout=PIPE, stderr=PIPE) + + # Check if process executed successfully, if yes return True, if not return False + if proces.stderr.read() == b'': + return True + return False + + + def decrypt(self, data: bytes) -> dict: + """Return the decrypted version of the encrypted bytes as a json dict""" + # Decrypt using Fernet, then decode into str, then load it with json + return js_l(self.Fernet.decrypt(data).decode()) + + + def encrypt(self, data: str) -> bytes: + """Return the encrypted version of the Serialize (means it's a dict encoded as a str by json) json dict as encrypted bytes""" + # Dump using json, then encode it into UTF-8, then encrypt it using Fernet + return self.Fernet.encrypt(js_d(data).encode()) + + + def get_credentials(self): + """Return the decrypted dict of username and passwords (format : {username: password, ...})""" + + # Open credentials file + with open(self.creds_file_path, "rb") as f: + # Read content of the file + data = f.read() + + # If file is empty, return an empty dict to avoid Fernet from crashing while trying to decrypt void + if data == b"": + return {} + + # Decrypt and return the dict + return self.decrypt(data) + + + def write_credentials(self, new_credentials_dict: dict[str: str]): + """Write the dict data in the credentials file\n + Parameters + ---------- + * new_credentials_dict : {str: str}\n + The dict to encrypt and write in the credentials file\n + >>> "WARNING. ERASE ALL EXISTING DATA IN THE FILE, IF YOU WANT TO APPEND DATA, SEE CredentialsManager.add_credentials()" """ + + # Open credentials file + with open(self.creds_file_path, "wb") as f: + # Encrypt, and write the encrypted data in the file + f.write(self.encrypt(new_credentials_dict)) + return + + + def add_credentials(self, new_credentials_data_dict: dict[str: str]): + """Append data to the encrypted json credentials file\n + Parameters + ---------- + * new_credentials_data_dict: {str: str}\n + The data to append to the encrypted credentials json dict""" + + # Use get_credentials and write_credentials + + # Get current data + old_dict = self.get_credentials() + + + # Set our new dict with the old value to keep + new_dict = old_dict + + + # Get all the keys of the new data + # For each key add them to the new dict + for key in new_credentials_data_dict.keys(): + new_dict[key] = new_credentials_data_dict[key] + + # Write our new dict to the encrypted json credentials file + self.write_credentials(new_dict) + return + + + def remove_credentials(self, credentials_to_remove: dict[str: str], match_password: bool = True): + """Remove the specified data from the encrypted json credentials file\n + Parameters + ---------- + * credentials_to_remove : {str: str}\n + The data to remove from the encrypted credentials json dict + * match_password : bool (Default = True)\n + If set on True, the username AND password must match to be removed, if set on False, ONLY username must match""" + + # Use get_credentials and write_credentials + + # Get current data + dict = self.get_credentials() + + # Get all the keys to remove + # For each key, if the key is in the dict, remove it + # Using a try except in case the key isn't in the dict + for key in credentials_to_remove.keys(): + try: + # If match_password is True, check if passwords are equals, if not, pass + if match_password: + if dict[key] == credentials_to_remove[key]: + dict.pop(key) + else: + dict.pop(key) + except: + pass + + # Write our dict to the encrypted json credentials file + self.write_credentials(dict) + return \ No newline at end of file