diff --git a/AppConfig/Consts.py b/AppConfig/Consts.py index 595fe00..9fd3741 100644 --- a/AppConfig/Consts.py +++ b/AppConfig/Consts.py @@ -1,5 +1,3 @@ -import os - class Consts: APP_NAME = "PassManager" encoding = 'utf-8' @@ -10,3 +8,4 @@ class Consts: _DebugMode = 'Debug_Mode' Gibberish_Len = 1024 Gibberish_Template = '{G1}{data}{G2}' + UserMetaDataFolderName = 'UsersMetaData' diff --git a/Authentication/AuthenticationService.py b/Authentication/AuthenticationService.py index 62cd916..9195983 100644 --- a/Authentication/AuthenticationService.py +++ b/Authentication/AuthenticationService.py @@ -21,6 +21,7 @@ def __init__(self, self._fileEncryptionService = fileEncryptionService self.mfaManager = None self.gpasswordKey: bytes = bytearray() + self.userEmail = None def isAuthnticated(self) -> bool: return self._mfaManagerService.IsMfaActive() and self.__isPasswordActive() @@ -38,6 +39,7 @@ def login(self, email, password, mfa) -> LogInReturnStatus: if mfaLoginResult.IsSucceded(): hashedMfaKey = PasswordService.HashifyPassword(mfaKeyDecrebtedKey) self.__setPassword(hashedMfaKey) + self.__setEmai(email) mfaKeyDecrebtedKey = None password = None @@ -45,6 +47,7 @@ def login(self, email, password, mfa) -> LogInReturnStatus: return mfaLoginResult def logout(self): + self.userEmail = None self.gpasswordKey = bytearray() self._mfaManagerService.logOut() @@ -75,4 +78,7 @@ def getPassowrdKey(self) -> bytes: return self.gpasswordKey def __setPassword(self, password:bytes): - self.gpasswordKey = password \ No newline at end of file + self.gpasswordKey = password + + def __setEmai(self, email:str): + self.userEmail = email \ No newline at end of file diff --git a/Model/IAmUnique.py b/Model/IAmUnique.py new file mode 100644 index 0000000..3ddc702 --- /dev/null +++ b/Model/IAmUnique.py @@ -0,0 +1,4 @@ +class IAmUnique: + + def getUniqueId(self): + pass \ No newline at end of file diff --git a/Model/ISerilizable.py b/Model/ISerilizable.py new file mode 100644 index 0000000..9a9c017 --- /dev/null +++ b/Model/ISerilizable.py @@ -0,0 +1,16 @@ +import pickle + +from AppConfig.Consts import Consts + + +class ISerilizable: + + def serializeJson(self)->str: + return str(pickle.dumps(self), encoding=Consts.encoding_byte_array) + + @staticmethod + def deserilizeJson(str): + try: + return pickle.loads(bytes(str, encoding=Consts.encoding_byte_array)) + except: + return None \ No newline at end of file diff --git a/Model/SavedPasswordData.py b/Model/SavedPasswordData.py new file mode 100644 index 0000000..ec369d6 --- /dev/null +++ b/Model/SavedPasswordData.py @@ -0,0 +1,25 @@ +from Model.IAmUnique import IAmUnique +from Model.ISerilizable import ISerilizable + + +class SavedPasswordData(ISerilizable, IAmUnique): + + def __init__(self, serviceName, password, email, website=None, metaData={}): + self.serviceName = serviceName + self.email = email + self.password = password + self.website = website + self.metaData = metaData + + def setEmail(self, email): + self.email = email + + def getUniqueId(self): + return self.email + self.serviceName + + def __str__(self): + return f'Email = {self.email}\n' \ + f'Service Name = {self.serviceName}\n' \ + f'Password = {self.password}\n' \ + f'websiteUrl = {self.website}\n' \ + f'more Info = {self.metaData}.' \ No newline at end of file diff --git a/PasswordManager/MainPasswordManager.py b/PasswordManager/MainPasswordManager.py index 0e88d99..ea224cc 100644 --- a/PasswordManager/MainPasswordManager.py +++ b/PasswordManager/MainPasswordManager.py @@ -1,6 +1,7 @@ import gc from AppConfig.IConfiguration import IConfiguration +from Model.SavedPasswordData import SavedPasswordData from PasswordManager.FileEncryptionManager import FileEncryptionManager from Service.FileManagement import FileManagement @@ -13,14 +14,17 @@ def __init__(self, self._fileEncryptionManager = fileEncryptionManager self._defaultDir = configuration.SavedPasswordDirPath - def addPassword(self, serviceName, password): + def addPassword(self, dataToSave:SavedPasswordData): 'Throw exception' self.initSavedPasswordDir() fileFullPath = '' try: - fileFullPath = FileManagement.createFilePath(serviceName, self._defaultDir) - FileManagement.WriteInFile(fileFullPath, password) + fileFullPath = FileManagement.createFilePath( + dataToSave.getUniqueId(), + self._defaultDir + ) + FileManagement.WriteInFile(fileFullPath, dataToSave.serializeJson()) except FileExistsError as ex: raise except Exception: @@ -39,20 +43,22 @@ def addPassword(self, serviceName, password): key = None collected = gc.collect() - def getPassword(self, serviceName): + def getPassword(self, dataToSave:SavedPasswordData) -> SavedPasswordData: 'Throw exception' self.initSavedPasswordDir() - fileFullPath = FileManagement.createFilePath(serviceName, self._defaultDir, FileManagement.Encryption) + # need to add the email to the path + fileFullPath = FileManagement.createFilePath(dataToSave.getUniqueId(), self._defaultDir, FileManagement.Encryption) if not FileManagement.DoesPathExist(fileFullPath): - raise Exception(f'No matching passwrod for this service found :{serviceName}.') + raise Exception(f'No matching passwrod for this service found :{dataToSave.serviceName}.') - decPassword = self._fileEncryptionManager.read_encrypted_file(fileFullPath) + defFile = self._fileEncryptionManager.read_encrypted_file(fileFullPath) + savedData = SavedPasswordData.deserilizeJson(defFile) # Clean-up key = None collected = gc.collect() - return decPassword + return savedData def initSavedPasswordDir(self): FileManagement.CreateDir(self._defaultDir) \ No newline at end of file diff --git a/Service/FileEncryptionService.py b/Service/FileEncryptionService.py index 2ea02df..01cccb3 100644 --- a/Service/FileEncryptionService.py +++ b/Service/FileEncryptionService.py @@ -39,11 +39,7 @@ def create_decrypted_file(self, filePath, key:bytes, delete=True) -> str: dec = self.decryptFileContent(filePath, key) decFilePath = FileManagement.ChangeFileType(filePath, FileManagement.Txt) - try: - FileManagement.WriteInFile(decFilePath, dec) - except FileExistsError as ex: - os.remove(decFilePath) - raise ex + FileManagement.WriteInFileWithErrorHandling(decFilePath, dec) if delete: os.remove(filePath) diff --git a/Service/FileManagement.py b/Service/FileManagement.py index 29271b6..e34343a 100644 --- a/Service/FileManagement.py +++ b/Service/FileManagement.py @@ -41,16 +41,24 @@ def WriteJsonObject(filePath, data, newFile=True): @staticmethod - def WriteInFile(filePath, data, inBytes=False,newFile=True): + def WriteInFile(filePath, data, inBytes=False, newFile=True, overiteFile=False): FileManagement.__CheckFileCreation(filePath, newFile) - if not newFile: + if not newFile and not overiteFile: FileManagement.AppendToFile(filePath, data) writeMode = 'wb' if inBytes else 'w' with open(filePath, writeMode) as file: file.write(data) + @staticmethod + def WriteInFileWithErrorHandling(filePath, data, inBytes=False, newFile=True, overiteFile=False): + try: + FileManagement.WriteInFile(filePath, data, inBytes, newFile, overiteFile) + except FileExistsError as ex: + os.remove(filePath) + raise ex + @staticmethod def __CheckFileCreation(filePath:str, newFile:bool): if newFile and FileManagement.DoesPathExist(filePath): diff --git a/Service/Model/UserMetaData.py b/Service/Model/UserMetaData.py new file mode 100644 index 0000000..48894e2 --- /dev/null +++ b/Service/Model/UserMetaData.py @@ -0,0 +1,17 @@ +from Model.IAmUnique import IAmUnique +from Model.ISerilizable import ISerilizable + + +class UserMetaData(ISerilizable, IAmUnique): + + def __init__(self, userEmail, servicesNamesList, lastLoginAt, accountCreatedAt): + self.userEmail = userEmail + self.servicesNamesList = servicesNamesList + self.lastLoginAt = lastLoginAt + self.accountCreatedAt = accountCreatedAt + + def getUniqueId(self): + return self.userEmail + + def addService(self, serviceName): + self.servicesNamesList.append(serviceName) \ No newline at end of file diff --git a/Service/Model/__init__.py b/Service/Model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Service/UserMetaDataService.py b/Service/UserMetaDataService.py new file mode 100644 index 0000000..a678ad3 --- /dev/null +++ b/Service/UserMetaDataService.py @@ -0,0 +1,45 @@ +from AppConfig.Consts import Consts +from AppConfig.IConfiguration import IConfiguration +from PasswordManager.FileEncryptionManager import FileEncryptionManager +from Service.FileManagement import FileManagement +from Service.Model.UserMetaData import UserMetaData + +import os + +class UserMetaDataService: + + def __init__(self, + fileEncryptionManager:FileEncryptionManager, + configuration:IConfiguration): + self._fileEncryptionManager = fileEncryptionManager + self._configuration = configuration + + def ReadUserMetaData(self, userMetaData:UserMetaData) -> UserMetaData: + filePath = self.GetFilePath(userMetaData) + FileManagement.DoesPathExist(filePath, raiseException=True) + fileContent = self._fileEncryptionManager.read_encrypted_file(filePath) + return UserMetaData.deserilizeJson(fileContent) + + def WriteUserMetaData(self, userMetaData:UserMetaData): + # write assumes that the enc file doesn't exist + filePath = self.GetFilePath(userMetaData) + FileManagement.WriteInFileWithErrorHandling(filePath, userMetaData.serializeJson(), newFile=False, overiteFile=True) + self._fileEncryptionManager.encryptFile(filePath) + + def UpdateUserMetaData(self, userMetaData:UserMetaData): + # change fileName then create new file then delte it + pass + + def CreateUserMetaDataFolder(self): + dirPath = self.GetMetaDataDirPath() + FileManagement.CreateDir(dirPath) + + def GetFilePath(self, userMetaData:UserMetaData) -> str: + dirPath = self.GetMetaDataDirPath() + filePath = FileManagement.createFilePath(userMetaData.getUniqueId(), dirPath) + return filePath + + def GetMetaDataDirPath(self) -> str: + folderName = Consts.UserMetaDataFolderName + savedPasswordsDirPath = self._configuration.GetValueOrDefault(Consts._Saved_Password_Dir_Path) + return os.path.join(savedPasswordsDirPath, folderName) \ No newline at end of file diff --git a/UserInput/PromptUserInputHandler.py b/UserInput/PromptUserInputHandler.py index 552d2d2..9e9a260 100644 --- a/UserInput/PromptUserInputHandler.py +++ b/UserInput/PromptUserInputHandler.py @@ -2,6 +2,7 @@ from Service.FileManagement import FileManagement from UserInput.BasePromptUserInputHandler import BasePromptUserInputHandler import re +import validators class PromptUserInputHandler(BasePromptUserInputHandler): @@ -73,9 +74,32 @@ def getValidEmail(self) -> str: # return email - def getWebSiteServiceName(self) -> str: - WebSiteServiceName = self.getUserInput('Please Enter Website/Service Name:') - return WebSiteServiceName + def getServiceName(self) -> str: + serviceName = self.getUserInput('Please Enter Service Name:') + return serviceName + + def getValidWebsiteUrl(self) -> str: + while True: + websiteUrl = self.getUserInput('Please Enter website url (To skip press enter):') + if websiteUrl == '\n': + return None + if validators.url(websiteUrl): + break + return websiteUrl + + def getAddetionalInfo(self) -> {}: + keyValuesMetaData = {} + while True: + key = self.getUserInput('Please Enter name to store a value (To finish press enter):') + if key == '\n': + return keyValuesMetaData + if key in keyValuesMetaData: + print("name already exists.") + continue + + value = self.getUserInput('Please Enter the name value:') + keyValuesMetaData[key] = value + return keyValuesMetaData def getFilePath(self, toMsg='') -> str: to = '' if len(toMsg) == 0 else f' to {toMsg}' diff --git a/UserPrompt/UserPrompt.py b/UserPrompt/UserPrompt.py index 6fa318d..f4b0372 100644 --- a/UserPrompt/UserPrompt.py +++ b/UserPrompt/UserPrompt.py @@ -9,12 +9,14 @@ from Exceptions.InvalidFileTypeException import InvalidFileTypeException from Exceptions.PathAccessException import PathAccessException from Model.LogInReturnStatus import LogInReturnStatus +from Model.SavedPasswordData import SavedPasswordData from Model.Status import Status from PasswordManager.FileEncryptionManager import FileEncryptionManager from PasswordManager.MainPasswordManager import MainPasswordManager from Service.AccessService import AccessService from Service.FileEncryptionService import FileEncryptionService from Service.PasswordService import PasswordService +from Service.UserMetaDataService import UserMetaDataService from UserInput.BasePromptUserInputHandler import BasePromptUserInputHandler from UserInput.PromptUserInputHandler import PromptUserInputHandler @@ -35,7 +37,8 @@ def __init__(self, registrationService:RegistrationService, mainPasswordManager:MainPasswordManager, fileEncryptionManager:FileEncryptionManager, - configuration:IConfiguration): + configuration:IConfiguration, + userMetaDataService:UserMetaDataService): super().__init__() self._userPromptHandler = userPromptHandler self._authenticationService = authenticationService @@ -44,6 +47,7 @@ def __init__(self, self._mainPasswordManager = mainPasswordManager self._fileEncryptionManager = fileEncryptionManager self._configuration = configuration + self._userMetaDataService = userMetaDataService self.__setUpEnv(False, True) @@ -135,10 +139,21 @@ def do_register(self, arg): def do_addPassword(self, arg): 'Add New Password, it will be saved in a file where you had set the SavedPasswordDirPath' try: - accountName = self._userPromptHandler.getWebSiteServiceName() + accountName = self._userPromptHandler.getServiceName() password = self._userPromptHandler.getInputPassword() - - self._mainPasswordManager.addPassword(accountName, password) + websiteUrl = self._userPromptHandler.getValidWebsiteUrl() + metaData = self._userPromptHandler.getAddetionalInfo() + + dataToSave = SavedPasswordData( + accountName, + password, + self._authenticationService.userEmail, + websiteUrl, + metaData, + ) + + self._mainPasswordManager.addPassword(dataToSave) + self._userMetaDataService.ReadUserMetaData() # Clean-up password = None except Exception as ex: @@ -148,10 +163,15 @@ def do_addPassword(self, arg): def do_getPassword(self, arg): 'Get Saved Password' try: - accountName = self._userPromptHandler.getWebSiteServiceName() - - savedPass = self._mainPasswordManager.getPassword(accountName) - print(f'Password of {accountName} = {savedPass}.') + serviceName = self._userPromptHandler.getServiceName() + dataFetchRequest = SavedPasswordData( + serviceName, + None, + self._authenticationService.userEmail, + ) + + savedData = self._mainPasswordManager.getPassword(dataFetchRequest) + print(f'Password of {savedData}.') except Exception as ex: handeled = self.__handleMainExcptions(ex, "while trying to get password.") if not handeled: raise @@ -219,6 +239,8 @@ def InitAll(): authenticationService = AuthenticationService(mfaManagerService, registrationService, fileEncryptionService) fileEncryptionManager = FileEncryptionManager(authenticationService, fileEncryptionService) mainPasswordManager = MainPasswordManager(fileEncryptionManager, configuration) + userMetaDataService = UserMetaDataService(fileEncryptionManager, configuration) + userPrompt = UserPrompt( promptUserInputHandler, @@ -227,7 +249,8 @@ def InitAll(): registrationService, mainPasswordManager, fileEncryptionManager, - configuration + configuration, + userMetaDataService ) userPrompt.cmdloop() diff --git a/requirements.txt b/requirements.txt index b8986eb..adb4fd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ pyotp==2.7.0 qrcode==7.3.1 Pillow==8.3.1 simplejson==3.17.6 -pycryptodome==3.10.1 \ No newline at end of file +pycryptodome==3.10.1 +validators==0.20.0 \ No newline at end of file