diff --git a/.gitignore b/.gitignore index a9867ef..39f5b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /Pods /**/.DS_Store /**/*.xcuserdatad + +/LibrePass/Configs diff --git a/COPYING.IOS b/COPYING.IOS new file mode 100644 index 0000000..66bd62a --- /dev/null +++ b/COPYING.IOS @@ -0,0 +1,13 @@ +The LibrePass iOS developers are aware that the terms of service that +apply to apps distributed via Apple's App Store services may conflict +with rights granted under the LibrePass iOS license, the GNU General +Public License, version 3 or (at your option) any later version. The +copyright holders of the LibrePass iOS app do not wish this conflict +to prevent the otherwise-compliant distribution of derived apps via +the App Store. Therefore, we have committed not to pursue any license +violation that results solely from the conflict between the GNU GPLv3 +or any later version and the Apple App Store terms of service. In +other words, as long as you comply with the GPL in all other respects, +including its requirements to provide users with source code and the +text of the license, we will not object to your distribution of the +LibrePass iOS app through the App Store. diff --git a/LibrePass.xcodeproj/project.pbxproj b/LibrePass.xcodeproj/project.pbxproj index 0348a81..ff82f31 100644 --- a/LibrePass.xcodeproj/project.pbxproj +++ b/LibrePass.xcodeproj/project.pbxproj @@ -7,62 +7,47 @@ objects = { /* Begin PBXBuildFile section */ - 3975184DC26DCD2D852A7C08 /* Pods_LibrePass.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A927B022513678BA42B54FCF /* Pods_LibrePass.framework */; }; - B23D44F82BA62C6B00097960 /* OATH.swift in Sources */ = {isa = PBXBuildFile; fileRef = B23D44F72BA62C6B00097960 /* OATH.swift */; }; - B25257BB2B91149D0028ABF3 /* LibrePassAccountSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25257BA2B91149D0028ABF3 /* LibrePassAccountSettings.swift */; }; - B25257BF2B91341D0028ABF3 /* vaultScreenshot.png in Resources */ = {isa = PBXBuildFile; fileRef = B25257BE2B91341D0028ABF3 /* vaultScreenshot.png */; }; - B253F1D72B83942B00D048F9 /* LibrePassCipherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1CC2B83942B00D048F9 /* LibrePassCipherView.swift */; }; - B253F1D82B83942B00D048F9 /* LibrePassLocalLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1CD2B83942B00D048F9 /* LibrePassLocalLogin.swift */; }; - B253F1D92B83942B00D048F9 /* LibrePassLoginWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1CE2B83942B00D048F9 /* LibrePassLoginWindow.swift */; }; - B253F1DA2B83942B00D048F9 /* LibrePassManagerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1CF2B83942B00D048F9 /* LibrePassManagerWindow.swift */; }; - B253F1DB2B83942B00D048F9 /* CredentialsDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1D02B83942B00D048F9 /* CredentialsDatabase.swift */; }; - B253F1DC2B83942B00D048F9 /* LibrePassAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1D12B83942B00D048F9 /* LibrePassAPI.swift */; }; + 17349C0D2BC1E19D00452814 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17349C0C2BC1E19D00452814 /* ContentView.swift */; }; + 17349C112BC1E4B300452814 /* SignupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17349C102BC1E4B300452814 /* SignupView.swift */; }; + 174EA0982BC3609E00084D2C /* Argon2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 174EA0972BC3609E00084D2C /* Argon2Swift */; }; + 175190532BCA3EEB0076460A /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175190522BCA3EEB0076460A /* HomeView.swift */; }; + 176A30622BC1C103005C143E /* CustomEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176A30612BC1C103005C143E /* CustomEnvironment.swift */; }; + 177B62CC2BCB14DF003EBC65 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 177B62CB2BCB14DF003EBC65 /* Alamofire */; }; + 177B62CE2BCB14DF003EBC65 /* AlamofireDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 177B62CD2BCB14DF003EBC65 /* AlamofireDynamic */; }; + 177B62D02BCB78B1003EBC65 /* PreLoginResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177B62CF2BCB78B1003EBC65 /* PreLoginResponse.swift */; }; + 179E981C2BC1DDBB00D71ACA /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179E981B2BC1DDBB00D71ACA /* AuthViewModel.swift */; }; + 17AABE232BD3913500B0AA8C /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17349C0E2BC1E4A100452814 /* LoginView.swift */; }; + 17B4516B2BCC6E1C00C5E1AE /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 17B4516A2BCC6E1C00C5E1AE /* KeychainSwift */; }; + 17B4516D2BCC832900C5E1AE /* UserCredentialResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B4516C2BCC832900C5E1AE /* UserCredentialResponse.swift */; }; + 17B4516F2BCC8CE600C5E1AE /* LoginRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B4516E2BCC8CE600C5E1AE /* LoginRequest.swift */; }; + 17B451722BCD866900C5E1AE /* RegisterRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B451712BCD866900C5E1AE /* RegisterRequest.swift */; }; + 17B451742BCDB90B00C5E1AE /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B451732BCDB90B00C5E1AE /* Credentials.swift */; }; B253F1DD2B83942B00D048F9 /* LibrePassApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1D22B83942B00D048F9 /* LibrePassApp.swift */; }; - B253F1DE2B83942B00D048F9 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1D32B83942B00D048F9 /* ApiClient.swift */; }; - B253F1DF2B83942B00D048F9 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1D42B83942B00D048F9 /* Crypto.swift */; }; - B253F1E02B83942B00D048F9 /* LibrePassCipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1D52B83942B00D048F9 /* LibrePassCipher.swift */; }; - B253F1E12B83942B00D048F9 /* LibrePassEncryptedCipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253F1D62B83942B00D048F9 /* LibrePassEncryptedCipher.swift */; }; B253F1E52B83976900D048F9 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = B253F1E32B83976900D048F9 /* LICENSE */; }; - B253F1E62B83976900D048F9 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = B253F1E42B83976900D048F9 /* README.md */; }; - B2780F062B8A43C200835EE3 /* LibrePassRegistrationWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2780F052B8A43C200835EE3 /* LibrePassRegistrationWindow.swift */; }; - B28808912B9254E300A526C6 /* PasswordGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28808902B9254E300A526C6 /* PasswordGenerator.swift */; }; - B28E06202B8BBD1900F48661 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28E061F2B8BBD1900F48661 /* Utils.swift */; }; - B2D4447B2B87D0DF0031A685 /* Vault.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D4447A2B87D0DF0031A685 /* Vault.swift */; }; B2D91A1D2B8393C70004561A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B2D91A1C2B8393C70004561A /* Assets.xcassets */; }; B2D91A202B8393C70004561A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B2D91A1F2B8393C70004561A /* Preview Assets.xcassets */; }; - B2DD90712B8D10D4003D84B7 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2DD90702B8D10D4003D84B7 /* NetworkMonitor.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 2816A09CADE280F72E52A29A /* Pods-LibrePass.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LibrePass.release.xcconfig"; path = "Target Support Files/Pods-LibrePass/Pods-LibrePass.release.xcconfig"; sourceTree = ""; }; - A927B022513678BA42B54FCF /* Pods_LibrePass.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LibrePass.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B23D44F72BA62C6B00097960 /* OATH.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATH.swift; sourceTree = ""; }; - B25257BA2B91149D0028ABF3 /* LibrePassAccountSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrePassAccountSettings.swift; sourceTree = ""; }; - B25257BE2B91341D0028ABF3 /* vaultScreenshot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vaultScreenshot.png; sourceTree = ""; }; - B253F1CC2B83942B00D048F9 /* LibrePassCipherView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibrePassCipherView.swift; sourceTree = ""; }; - B253F1CD2B83942B00D048F9 /* LibrePassLocalLogin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibrePassLocalLogin.swift; sourceTree = ""; }; - B253F1CE2B83942B00D048F9 /* LibrePassLoginWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibrePassLoginWindow.swift; sourceTree = ""; }; - B253F1CF2B83942B00D048F9 /* LibrePassManagerWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibrePassManagerWindow.swift; sourceTree = ""; }; - B253F1D02B83942B00D048F9 /* CredentialsDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsDatabase.swift; sourceTree = ""; }; - B253F1D12B83942B00D048F9 /* LibrePassAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibrePassAPI.swift; sourceTree = ""; }; + 17349C0C2BC1E19D00452814 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 17349C0E2BC1E4A100452814 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; + 17349C102BC1E4B300452814 /* SignupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignupView.swift; sourceTree = ""; }; + 1742993E2BC9D09B008B639D /* Dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Dev.xcconfig; sourceTree = ""; }; + 1742993F2BC9D0B6008B639D /* Prod.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Prod.xcconfig; sourceTree = ""; }; + 175190522BCA3EEB0076460A /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 176A30612BC1C103005C143E /* CustomEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEnvironment.swift; sourceTree = ""; }; + 176A306A2BC1C8D8005C143E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 177B62CF2BCB78B1003EBC65 /* PreLoginResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreLoginResponse.swift; sourceTree = ""; }; + 179E981B2BC1DDBB00D71ACA /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = ""; }; + 17B4516C2BCC832900C5E1AE /* UserCredentialResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCredentialResponse.swift; sourceTree = ""; }; + 17B4516E2BCC8CE600C5E1AE /* LoginRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRequest.swift; sourceTree = ""; }; + 17B451712BCD866900C5E1AE /* RegisterRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterRequest.swift; sourceTree = ""; }; + 17B451732BCDB90B00C5E1AE /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; B253F1D22B83942B00D048F9 /* LibrePassApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibrePassApp.swift; sourceTree = ""; }; - B253F1D32B83942B00D048F9 /* ApiClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiClient.swift; sourceTree = ""; }; - B253F1D42B83942B00D048F9 /* Crypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; - B253F1D52B83942B00D048F9 /* LibrePassCipher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibrePassCipher.swift; sourceTree = ""; }; - B253F1D62B83942B00D048F9 /* LibrePassEncryptedCipher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibrePassEncryptedCipher.swift; sourceTree = ""; }; - B253F1E22B8394EE00D048F9 /* LibrePass-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LibrePass-Bridging-Header.h"; sourceTree = ""; }; B253F1E32B83976900D048F9 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; - B253F1E42B83976900D048F9 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - B2780F052B8A43C200835EE3 /* LibrePassRegistrationWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrePassRegistrationWindow.swift; sourceTree = ""; }; - B28808902B9254E300A526C6 /* PasswordGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordGenerator.swift; sourceTree = ""; }; - B28DFDBD2B8B6DC1007D5837 /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; - B28E061F2B8BBD1900F48661 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - B2D4447A2B87D0DF0031A685 /* Vault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vault.swift; sourceTree = ""; }; B2D91A152B8393C60004561A /* LibrePass.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LibrePass.app; sourceTree = BUILT_PRODUCTS_DIR; }; B2D91A1C2B8393C70004561A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B2D91A1F2B8393C70004561A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - B2DD90702B8D10D4003D84B7 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; - F596AB355AB5D89D17A58912 /* Pods-LibrePass.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LibrePass.debug.xcconfig"; path = "Target Support Files/Pods-LibrePass/Pods-LibrePass.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -70,48 +55,104 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3975184DC26DCD2D852A7C08 /* Pods_LibrePass.framework in Frameworks */, + 174EA0982BC3609E00084D2C /* Argon2Swift in Frameworks */, + 177B62CE2BCB14DF003EBC65 /* AlamofireDynamic in Frameworks */, + 17B4516B2BCC6E1C00C5E1AE /* KeychainSwift in Frameworks */, + 177B62CC2BCB14DF003EBC65 /* Alamofire in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 1CCFA11845A6091810429252 /* Frameworks */ = { + 1742993D2BC9D070008B639D /* Configs */ = { isa = PBXGroup; children = ( - A927B022513678BA42B54FCF /* Pods_LibrePass.framework */, + 1742993E2BC9D09B008B639D /* Dev.xcconfig */, + 1742993F2BC9D0B6008B639D /* Prod.xcconfig */, ); - name = Frameworks; + path = Configs; sourceTree = ""; }; - B2D444772B87CFF00031A685 /* API */ = { + 175190512BCA3EE00076460A /* Home */ = { isa = PBXGroup; children = ( - B253F1D12B83942B00D048F9 /* LibrePassAPI.swift */, - B253F1D52B83942B00D048F9 /* LibrePassCipher.swift */, - B253F1D62B83942B00D048F9 /* LibrePassEncryptedCipher.swift */, - B253F1D02B83942B00D048F9 /* CredentialsDatabase.swift */, - B2D4447A2B87D0DF0031A685 /* Vault.swift */, - B253F1D42B83942B00D048F9 /* Crypto.swift */, - B253F1D32B83942B00D048F9 /* ApiClient.swift */, - B28808902B9254E300A526C6 /* PasswordGenerator.swift */, - B23D44F72BA62C6B00097960 /* OATH.swift */, + 175190522BCA3EEB0076460A /* HomeView.swift */, ); - path = API; + path = Home; + sourceTree = ""; + }; + 179E98132BC1DAB900D71ACA /* App */ = { + isa = PBXGroup; + children = ( + B253F1D22B83942B00D048F9 /* LibrePassApp.swift */, + ); + path = App; + sourceTree = ""; + }; + 179E98142BC1DAC200D71ACA /* Core */ = { + isa = PBXGroup; + children = ( + 175190512BCA3EE00076460A /* Home */, + 179E98182BC1DCEC00D71ACA /* Auth */, + 179E98152BC1DB1B00D71ACA /* Root */, + ); + path = Core; + sourceTree = ""; + }; + 179E98152BC1DB1B00D71ACA /* Root */ = { + isa = PBXGroup; + children = ( + 17349C0C2BC1E19D00452814 /* ContentView.swift */, + ); + path = Root; + sourceTree = ""; + }; + 179E98182BC1DCEC00D71ACA /* Auth */ = { + isa = PBXGroup; + children = ( + 17B451702BCD847D00C5E1AE /* Model */, + 179E981A2BC1DD2000D71ACA /* ViewModel */, + 179E98192BC1DD1A00D71ACA /* Views */, + ); + path = Auth; + sourceTree = ""; + }; + 179E98192BC1DD1A00D71ACA /* Views */ = { + isa = PBXGroup; + children = ( + 17349C0E2BC1E4A100452814 /* LoginView.swift */, + 17349C102BC1E4B300452814 /* SignupView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 179E981A2BC1DD2000D71ACA /* ViewModel */ = { + isa = PBXGroup; + children = ( + 179E981B2BC1DDBB00D71ACA /* AuthViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 17B451702BCD847D00C5E1AE /* Model */ = { + isa = PBXGroup; + children = ( + 177B62CF2BCB78B1003EBC65 /* PreLoginResponse.swift */, + 17B4516C2BCC832900C5E1AE /* UserCredentialResponse.swift */, + 17B4516E2BCC8CE600C5E1AE /* LoginRequest.swift */, + 17B451712BCD866900C5E1AE /* RegisterRequest.swift */, + 17B451732BCDB90B00C5E1AE /* Credentials.swift */, + ); + path = Model; sourceTree = ""; }; B2D91A0C2B8393C60004561A = { isa = PBXGroup; children = ( B2D91A172B8393C60004561A /* LibrePass */, - B2DD90722B8D3F0E003D84B7 /* readme */, - B28DFDBD2B8B6DC1007D5837 /* Makefile */, B253F1E32B83976900D048F9 /* LICENSE */, - B253F1E42B83976900D048F9 /* README.md */, B2D91A162B8393C60004561A /* Products */, - C4BF8AE2A13C542780EBE854 /* Pods */, - 1CCFA11845A6091810429252 /* Frameworks */, ); sourceTree = ""; }; @@ -126,18 +167,12 @@ B2D91A172B8393C60004561A /* LibrePass */ = { isa = PBXGroup; children = ( - B2D444772B87CFF00031A685 /* API */, - B253F1D22B83942B00D048F9 /* LibrePassApp.swift */, - B2780F052B8A43C200835EE3 /* LibrePassRegistrationWindow.swift */, - B253F1CE2B83942B00D048F9 /* LibrePassLoginWindow.swift */, - B253F1CD2B83942B00D048F9 /* LibrePassLocalLogin.swift */, - B253F1CF2B83942B00D048F9 /* LibrePassManagerWindow.swift */, - B253F1CC2B83942B00D048F9 /* LibrePassCipherView.swift */, - B25257BA2B91149D0028ABF3 /* LibrePassAccountSettings.swift */, - B2DD90702B8D10D4003D84B7 /* NetworkMonitor.swift */, - B28E061F2B8BBD1900F48661 /* Utils.swift */, - B253F1E22B8394EE00D048F9 /* LibrePass-Bridging-Header.h */, + 179E98132BC1DAB900D71ACA /* App */, B2D91A1C2B8393C70004561A /* Assets.xcassets */, + 1742993D2BC9D070008B639D /* Configs */, + 179E98142BC1DAC200D71ACA /* Core */, + 176A30612BC1C103005C143E /* CustomEnvironment.swift */, + 176A306A2BC1C8D8005C143E /* Info.plist */, B2D91A1E2B8393C70004561A /* Preview Content */, ); path = LibrePass; @@ -151,23 +186,6 @@ path = "Preview Content"; sourceTree = ""; }; - B2DD90722B8D3F0E003D84B7 /* readme */ = { - isa = PBXGroup; - children = ( - B25257BE2B91341D0028ABF3 /* vaultScreenshot.png */, - ); - path = readme; - sourceTree = ""; - }; - C4BF8AE2A13C542780EBE854 /* Pods */ = { - isa = PBXGroup; - children = ( - F596AB355AB5D89D17A58912 /* Pods-LibrePass.debug.xcconfig */, - 2816A09CADE280F72E52A29A /* Pods-LibrePass.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -175,17 +193,21 @@ isa = PBXNativeTarget; buildConfigurationList = B2D91A232B8393C70004561A /* Build configuration list for PBXNativeTarget "LibrePass" */; buildPhases = ( - F7C85135F291197A1D751897 /* [CP] Check Pods Manifest.lock */, B2D91A112B8393C60004561A /* Sources */, B2D91A122B8393C60004561A /* Frameworks */, B2D91A132B8393C60004561A /* Resources */, - E2BF409140E7F75969234855 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = LibrePass; + packageProductDependencies = ( + 174EA0972BC3609E00084D2C /* Argon2Swift */, + 177B62CB2BCB14DF003EBC65 /* Alamofire */, + 177B62CD2BCB14DF003EBC65 /* AlamofireDynamic */, + 17B4516A2BCC6E1C00C5E1AE /* KeychainSwift */, + ); productName = LibrePass; productReference = B2D91A152B8393C60004561A /* LibrePass.app */; productType = "com.apple.product-type.application"; @@ -215,6 +237,11 @@ Base, ); mainGroup = B2D91A0C2B8393C60004561A; + packageReferences = ( + 174EA0962BC3609E00084D2C /* XCRemoteSwiftPackageReference "Argon2Swift" */, + 177B62CA2BCB14DF003EBC65 /* XCRemoteSwiftPackageReference "Alamofire" */, + 17B451692BCC6E1C00C5E1AE /* XCRemoteSwiftPackageReference "keychain-swift" */, + ); productRefGroup = B2D91A162B8393C60004561A /* Products */; projectDirPath = ""; projectRoot = ""; @@ -231,87 +258,244 @@ files = ( B253F1E52B83976900D048F9 /* LICENSE in Resources */, B2D91A202B8393C70004561A /* Preview Assets.xcassets in Resources */, - B253F1E62B83976900D048F9 /* README.md in Resources */, B2D91A1D2B8393C70004561A /* Assets.xcassets in Resources */, - B25257BF2B91341D0028ABF3 /* vaultScreenshot.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - E2BF409140E7F75969234855 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-LibrePass/Pods-LibrePass-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-LibrePass/Pods-LibrePass-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LibrePass/Pods-LibrePass-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - F7C85135F291197A1D751897 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-LibrePass-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ B2D91A112B8393C60004561A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B253F1E02B83942B00D048F9 /* LibrePassCipher.swift in Sources */, - B28808912B9254E300A526C6 /* PasswordGenerator.swift in Sources */, - B2DD90712B8D10D4003D84B7 /* NetworkMonitor.swift in Sources */, - B253F1DC2B83942B00D048F9 /* LibrePassAPI.swift in Sources */, - B253F1D92B83942B00D048F9 /* LibrePassLoginWindow.swift in Sources */, - B253F1D72B83942B00D048F9 /* LibrePassCipherView.swift in Sources */, - B253F1DF2B83942B00D048F9 /* Crypto.swift in Sources */, - B253F1DE2B83942B00D048F9 /* ApiClient.swift in Sources */, - B2D4447B2B87D0DF0031A685 /* Vault.swift in Sources */, - B2780F062B8A43C200835EE3 /* LibrePassRegistrationWindow.swift in Sources */, + 17B4516F2BCC8CE600C5E1AE /* LoginRequest.swift in Sources */, + 17B451742BCDB90B00C5E1AE /* Credentials.swift in Sources */, + 177B62D02BCB78B1003EBC65 /* PreLoginResponse.swift in Sources */, + 17B4516D2BCC832900C5E1AE /* UserCredentialResponse.swift in Sources */, B253F1DD2B83942B00D048F9 /* LibrePassApp.swift in Sources */, - B28E06202B8BBD1900F48661 /* Utils.swift in Sources */, - B253F1D82B83942B00D048F9 /* LibrePassLocalLogin.swift in Sources */, - B23D44F82BA62C6B00097960 /* OATH.swift in Sources */, - B25257BB2B91149D0028ABF3 /* LibrePassAccountSettings.swift in Sources */, - B253F1E12B83942B00D048F9 /* LibrePassEncryptedCipher.swift in Sources */, - B253F1DB2B83942B00D048F9 /* CredentialsDatabase.swift in Sources */, - B253F1DA2B83942B00D048F9 /* LibrePassManagerWindow.swift in Sources */, + 17349C0D2BC1E19D00452814 /* ContentView.swift in Sources */, + 17AABE232BD3913500B0AA8C /* LoginView.swift in Sources */, + 17B451722BCD866900C5E1AE /* RegisterRequest.swift in Sources */, + 176A30622BC1C103005C143E /* CustomEnvironment.swift in Sources */, + 179E981C2BC1DDBB00D71ACA /* AuthViewModel.swift in Sources */, + 175190532BCA3EEB0076460A /* HomeView.swift in Sources */, + 17349C112BC1E4B300452814 /* SignupView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ - B2D91A212B8393C70004561A /* Debug */ = { + 176A30632BC1C4D6005C143E /* Debug(Prod) */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1742993F2BC9D0B6008B639D /* Prod.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = "Debug(Prod)"; + }; + 176A30642BC1C4D6005C143E /* Debug(Prod) */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1742993F2BC9D0B6008B639D /* Prod.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"LibrePass/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LibrePass/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSFaceIDUsageDescription = "We need you unlock your data."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.2.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.zapomnij.LibrePass; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "LibrePass/LibrePass-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug(Prod)"; + }; + 176A30652BC1C4D9005C143E /* Release(Prod) */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 1742993F2BC9D0B6008B639D /* Prod.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = "Release(Prod)"; + }; + 176A30662BC1C4D9005C143E /* Release(Prod) */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1742993F2BC9D0B6008B639D /* Prod.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"LibrePass/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LibrePass/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSFaceIDUsageDescription = "We need you unlock your data."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.2.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.zapomnij.LibrePass; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "LibrePass/LibrePass-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Release(Prod)"; + }; + B2D91A212B8393C70004561A /* Debug(Dev) */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1742993E2BC9D09B008B639D /* Dev.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -362,8 +546,9 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -371,10 +556,11 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; - name = Debug; + name = "Debug(Dev)"; }; - B2D91A222B8393C70004561A /* Release */ = { + B2D91A222B8393C70004561A /* Release(Dev) */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 1742993E2BC9D09B008B639D /* Dev.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -419,36 +605,39 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; }; - name = Release; + name = "Release(Dev)"; }; - B2D91A242B8393C70004561A /* Debug */ = { + B2D91A242B8393C70004561A /* Debug(Dev) */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F596AB355AB5D89D17A58912 /* Pods-LibrePass.debug.xcconfig */; + baseConfigurationReference = 1742993E2BC9D09B008B639D /* Dev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"LibrePass/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LibrePass/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSFaceIDUsageDescription = "We need you unlock your data."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -456,7 +645,8 @@ MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.github.zapomnij.LibrePass; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -464,30 +654,32 @@ SWIFT_OBJC_BRIDGING_HEADER = "LibrePass/LibrePass-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; - name = Debug; + name = "Debug(Dev)"; }; - B2D91A252B8393C70004561A /* Release */ = { + B2D91A252B8393C70004561A /* Release(Dev) */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2816A09CADE280F72E52A29A /* Pods-LibrePass.release.xcconfig */; + baseConfigurationReference = 1742993E2BC9D09B008B639D /* Dev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"LibrePass/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LibrePass/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSFaceIDUsageDescription = "We need you unlock your data."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -495,16 +687,17 @@ MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.github.zapomnij.LibrePass; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "LibrePass/LibrePass-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; - name = Release; + name = "Release(Dev)"; }; /* End XCBuildConfiguration section */ @@ -512,22 +705,76 @@ B2D91A102B8393C60004561A /* Build configuration list for PBXProject "LibrePass" */ = { isa = XCConfigurationList; buildConfigurations = ( - B2D91A212B8393C70004561A /* Debug */, - B2D91A222B8393C70004561A /* Release */, + B2D91A212B8393C70004561A /* Debug(Dev) */, + 176A30632BC1C4D6005C143E /* Debug(Prod) */, + B2D91A222B8393C70004561A /* Release(Dev) */, + 176A30652BC1C4D9005C143E /* Release(Prod) */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = "Release(Dev)"; }; B2D91A232B8393C70004561A /* Build configuration list for PBXNativeTarget "LibrePass" */ = { isa = XCConfigurationList; buildConfigurations = ( - B2D91A242B8393C70004561A /* Debug */, - B2D91A252B8393C70004561A /* Release */, + B2D91A242B8393C70004561A /* Debug(Dev) */, + 176A30642BC1C4D6005C143E /* Debug(Prod) */, + B2D91A252B8393C70004561A /* Release(Dev) */, + 176A30662BC1C4D9005C143E /* Release(Prod) */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = "Release(Dev)"; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 174EA0962BC3609E00084D2C /* XCRemoteSwiftPackageReference "Argon2Swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/tmthecoder/Argon2Swift"; + requirement = { + branch = main; + kind = branch; + }; + }; + 177B62CA2BCB14DF003EBC65 /* XCRemoteSwiftPackageReference "Alamofire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Alamofire/Alamofire"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.9.1; + }; + }; + 17B451692BCC6E1C00C5E1AE /* XCRemoteSwiftPackageReference "keychain-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/evgenyneu/keychain-swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 22.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 174EA0972BC3609E00084D2C /* Argon2Swift */ = { + isa = XCSwiftPackageProductDependency; + package = 174EA0962BC3609E00084D2C /* XCRemoteSwiftPackageReference "Argon2Swift" */; + productName = Argon2Swift; + }; + 177B62CB2BCB14DF003EBC65 /* Alamofire */ = { + isa = XCSwiftPackageProductDependency; + package = 177B62CA2BCB14DF003EBC65 /* XCRemoteSwiftPackageReference "Alamofire" */; + productName = Alamofire; + }; + 177B62CD2BCB14DF003EBC65 /* AlamofireDynamic */ = { + isa = XCSwiftPackageProductDependency; + package = 177B62CA2BCB14DF003EBC65 /* XCRemoteSwiftPackageReference "Alamofire" */; + productName = AlamofireDynamic; + }; + 17B4516A2BCC6E1C00C5E1AE /* KeychainSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 17B451692BCC6E1C00C5E1AE /* XCRemoteSwiftPackageReference "keychain-swift" */; + productName = KeychainSwift; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = B2D91A0D2B8393C60004561A /* Project object */; } diff --git a/LibrePass.xcodeproj/xcshareddata/xcschemes/LibrePass(Dev).xcscheme b/LibrePass.xcodeproj/xcshareddata/xcschemes/LibrePass(Dev).xcscheme new file mode 100644 index 0000000..b8bd2eb --- /dev/null +++ b/LibrePass.xcodeproj/xcshareddata/xcschemes/LibrePass(Dev).xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LibrePass.xcodeproj/xcshareddata/xcschemes/LibrePass(Prod).xcscheme b/LibrePass.xcodeproj/xcshareddata/xcschemes/LibrePass(Prod).xcscheme new file mode 100644 index 0000000..28421f9 --- /dev/null +++ b/LibrePass.xcodeproj/xcshareddata/xcschemes/LibrePass(Prod).xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LibrePass.xcworkspace/contents.xcworkspacedata b/LibrePass.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7f906f9..0000000 --- a/LibrePass.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/LibrePass.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/LibrePass.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/LibrePass.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/LibrePass.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/LibrePass.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index 0c67376..0000000 --- a/LibrePass.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/LibrePass/API/ApiClient.swift b/LibrePass/API/ApiClient.swift deleted file mode 100644 index adabbea..0000000 --- a/LibrePass/API/ApiClient.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation - -class ApiClient { - var accessToken: String? - var apiUrl: String - - init(apiUrl: String) { - self.apiUrl = apiUrl - } - - func request(path: String, body: Data?, method: String) throws -> Data { - let url = URL(string: self.apiUrl + path)! - var request = URLRequest(url: url) - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - if let accessToken = self.accessToken { - request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization") - } - - request.httpMethod = method - if let body = body { - request.httpBody = body - } - - var responseBody = Data() - var errorToReturn: Error? - let group = DispatchGroup() - let task = URLSession.shared.dataTask(with: request) {(data, response, error) -> Void in - defer {group.leave()} - - if let error = error { - errorToReturn = error - } else if let response = response as? HTTPURLResponse { - if response.statusCode == 200 { - responseBody = data ?? Data() - } else if response.statusCode < 200 || response.statusCode >= 300 { - if let apiErrorData = try? JSONDecoder().decode(ApiErrorData.self, from: data ?? Data()) { - errorToReturn = ApiClientErrors.StatusCodeNot200(statusCode: response.statusCode, body: apiErrorData) - } else { - errorToReturn = ApiClientErrors.UnknownResponse - } - } - } else { - errorToReturn = ApiClientErrors.UnknownResponse - } - } - - group.enter() - task.resume() - group.wait() - - if let errorToReturn = errorToReturn { - throw errorToReturn - } - - return responseBody - } - - struct ApiErrorData: Codable { - var error: String - var status: Int - } -} - -enum ApiClientErrors: Error { - case StatusCodeNot200(statusCode: Int, body: ApiClient.ApiErrorData) - case UnknownResponse -} diff --git a/LibrePass/API/CredentialsDatabase.swift b/LibrePass/API/CredentialsDatabase.swift deleted file mode 100644 index da898ed..0000000 --- a/LibrePass/API/CredentialsDatabase.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// CredentialsDatabase.swift -// LibrePass -// -// Created by Zapomnij on 18/02/2024. -// - -import Foundation -import SwiftUI - -struct LibrePassCredentialsDatabase: Codable { - var userId: String - var email: String - var apiUrl: String - var accessToken: String - var publicKey: String - - var argon2idParams: Argon2IdOptions - - func save() throws { - let encoded = try? JSONEncoder().encode(self) - UserDefaults.standard.setValue(encoded, forKey: "credentialsDatabase") - } - - static func load() throws -> Self { - let data = UserDefaults.standard.data(forKey: "credentialsDatabase")! - return try JSONDecoder().decode(Self.self, from: data) - } - - static func isLocallyLoggedIn() -> Bool { - return UserDefaults.standard.data(forKey: "credentialsDatabase") != nil - } -} diff --git a/LibrePass/API/CredentialsUpdate.swift b/LibrePass/API/CredentialsUpdate.swift deleted file mode 100644 index 7dadbbe..0000000 --- a/LibrePass/API/CredentialsUpdate.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CredentialsUpdate.swift -// LibrePass -// -// Created by Zapomnij on 28/02/2024. -// - -import Foundation - -extension LibrePassClient { - struct LibrePassChangePasswordRequest: Codable { - var oldSharedKey: String - var newPublicKey: String - var newSharedKey: String - var newPasswordHint: String - var parallelism: Int - var memory: Int - var iterations: Int - } - - mutating func changePassword(currentPassword: String, newPassword: String, newPasswordHint: String) throws { - - } -} diff --git a/LibrePass/API/Crypto.swift b/LibrePass/API/Crypto.swift deleted file mode 100644 index 9ff24f0..0000000 --- a/LibrePass/API/Crypto.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// crypto.swift -// LibrePass -// -// Created by Zapomnij on 17/02/2024. -// - -import Foundation -import SignalArgon2 -import CryptoKit - -struct Argon2IdOptions: Codable { - var parallelism: Int - var memory: Int - var iterations: Int - var serverPublicKey: String -} - -func argon2Hash(email: String, password: String, hashOptions: Argon2IdOptions) throws -> Data { - let (rawHash, _) = try Argon2.hash(iterations: UInt32(hashOptions.iterations), memoryInKiB: UInt32(hashOptions.memory), threads: UInt32(hashOptions.parallelism), password: password.data(using: .utf8)!, salt: email.data(using: .utf8)!, desiredLength: 32, variant: .id, version: .v13) - - return rawHash -} - -func dataToHexString(data: Data) -> String { - return data.map { String(format: "%02X", $0) }.joined() -} - -func hexStringToData(string: String) -> Data? { - guard string.count.isMultiple(of: 2) else { - return nil - } - let chars = string.map { $0 } - let bytes = stride(from: 0, to: chars.count, by: 2) - .map { String(chars[$0]) + String(chars[$0 + 1]) } - .compactMap { UInt8($0, radix: 16) } - - guard string.count / bytes.count == 2 else { return nil } - - return Data(bytes) -} - -func aesGcmDecrypt(data: Data, key: SymmetricKey) throws -> Data { - let sealedBoxRestored = try AES.GCM.SealedBox(combined: data) - return try AES.GCM.open(sealedBoxRestored, using: key) -} - -func aesGcmEncrypt(data: Data, key: SymmetricKey) throws -> Data { - let sealed = try AES.GCM.seal(data, using: key) - return sealed.combined! -} diff --git a/LibrePass/API/LibrePassAPI.swift b/LibrePass/API/LibrePassAPI.swift deleted file mode 100644 index a8e3e2f..0000000 --- a/LibrePass/API/LibrePassAPI.swift +++ /dev/null @@ -1,354 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book - -import Foundation -import CryptoKit -import SwiftUI - -struct LibrePassClient { - var client: ApiClient - var loginData: LoginData? - var sharedKey: SymmetricKey? - var credentialsDatabase: LibrePassCredentialsDatabase? - var vault: LibrePassDecryptedVault - - init(apiUrl: String) { - self.client = ApiClient(apiUrl: apiUrl) - self.vault = LibrePassDecryptedVault(lastSync: 0) - - if !LibrePassEncryptedVault.isVaultSavedLocally() { - _ = try? LibrePassEncryptedVault(lastSync: 0).saveVault() - } - } - - init(credentials: LibrePassCredentialsDatabase, password: String) throws { - self.init(apiUrl: credentials.apiUrl) - self.client.accessToken = credentials.accessToken - self.loginData = LoginData(userId: credentials.userId, apiKey: credentials.accessToken, verified: true) - - let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: hexStringToData(string: credentials.publicKey)!) - let privateKeyData = try argon2Hash(email: credentials.email, password: password, hashOptions: credentials.argon2idParams) - let privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: privateKeyData) - if publicKey.rawRepresentation != privateKey.publicKey.rawRepresentation { - throw LibrePassApiErrors.WithMessage(message: "Invalid credentials") - } - - let serializedSharedKey = try privateKey.sharedSecretFromKeyAgreement(with: publicKey).withUnsafeBytes { - return Data(Array($0)) - } - - self.sharedKey = SymmetricKey(data: serializedSharedKey) - self.credentialsDatabase = credentials - self.vault.key = self.sharedKey! - } - - func getKeys(email: String, password: String, argon2options: Argon2IdOptions) throws -> (Data, Data, Data) { - let privateKeyData = try argon2Hash(email: email, password: password, hashOptions: argon2options) - - let privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: privateKeyData) - - let sharedKey = try privateKey.sharedSecretFromKeyAgreement(with: try Curve25519.KeyAgreement.PublicKey(rawRepresentation: hexStringToData(string: argon2options.serverPublicKey)!)) - - let sharedKeyData = sharedKey.withUnsafeBytes { - return Data(Array($0)) - } - - let publicKeyData = privateKey.publicKey.rawRepresentation.withUnsafeBytes { - return Data(Array($0)) - } - - return (privateKeyData, publicKeyData, sharedKeyData) - } - - func preLogin(email: String) throws -> Argon2IdOptions { - let body = try self.client.request(path: "/api/auth/preLogin?email=" + email, body: nil, method: "GET") - - let decoder = JSONDecoder() - do { - return try decoder.decode(Argon2IdOptions.self, from: body) - } catch { - throw error - } - } - - func register(email: String, password: String, passwordHint: String) throws { - struct RegisterRequestBody: Encodable { - var email: String - var passwordHint: String - var sharedKey: String - var publicKey: String - var parallelism: Int - var memory: Int - var iterations: Int - } - - let preLogin = try self.preLogin(email: "") - let (_, publicKeyData, sharedKeyData) = try self.getKeys(email: email, password: password, argon2options: preLogin) - - let request = RegisterRequestBody(email: email, passwordHint: passwordHint, sharedKey: dataToHexString(data: sharedKeyData), publicKey: dataToHexString(data: publicKeyData), parallelism: preLogin.parallelism, memory: preLogin.memory, iterations: preLogin.iterations) - - let requestData = try JSONEncoder().encode(request) - - _ = try self.client.request(path: "/api/auth/register", body: requestData, method: "POST") - } - - mutating func login(email: String, password: String) throws { - struct LoginRequestBody: Encodable { - var email: String - var sharedKey: String - } - - let preLogin = try self.preLogin(email: email) - - let (privateKeyData, _, sharedKeyData) = try self.getKeys(email: email, password: password, argon2options: preLogin) - - let encoder = JSONEncoder() - let loginRequestData = LoginRequestBody(email: email, sharedKey: dataToHexString(data: sharedKeyData)) - let loginRequestBody = try encoder.encode(loginRequestData) - - let loginResponse = try self.client.request(path: "/api/auth/oauth?grantType=login", body: loginRequestBody, method: "POST") - - let decoder = JSONDecoder() - self.loginData = try decoder.decode(LoginData.self, from: loginResponse) - self.client.accessToken = self.loginData!.apiKey - - let privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: privateKeyData) - let sharedKeySerialized = try privateKey.sharedSecretFromKeyAgreement(with: privateKey.publicKey).withUnsafeBytes { - return Data(Array($0)) - } - self.sharedKey = SymmetricKey(data: sharedKeySerialized) - - let privateKeySerialized = privateKey.publicKey.rawRepresentation.withUnsafeBytes { - return Data(Array($0)) - } - - self.credentialsDatabase = LibrePassCredentialsDatabase(userId: self.loginData!.userId, email: email, apiUrl: self.client.apiUrl, accessToken: self.client.accessToken!, publicKey: dataToHexString(data: privateKeySerialized), argon2idParams: preLogin) - try self.credentialsDatabase!.save() - } - - mutating func replaceApiClient(apiUrl: String) { - self.client = ApiClient(apiUrl: apiUrl) - } - - mutating func logOut() { - UserDefaults.standard.removeObject(forKey: "credentialsDatabase") - UserDefaults.standard.removeObject(forKey: "vault") - UserDefaults.standard.removeObject(forKey: "queue") - - self.unAuth() - } - - mutating func unAuth() { - self.client = ApiClient(apiUrl: self.client.apiUrl) - self.loginData = nil - self.sharedKey = nil - self.credentialsDatabase = nil - self.vault = LibrePassDecryptedVault(lastSync: 0) - } - - mutating func fetchCiphers() throws { - self.vault.key = self.sharedKey! - - let resp = try self.client.request(path: "/api/cipher", body: nil, method: "GET") - - var cryptedVault = LibrePassEncryptedVault(lastSync: Int64(Date().timeIntervalSince1970)) - cryptedVault.vault = try JSONDecoder().decode([LibrePassEncryptedCipher].self, from: resp) - for _ in cryptedVault.vault { - cryptedVault.toSync.append(false) - } - - try cryptedVault.saveVault() - - self.vault = try cryptedVault.decryptVault(key: self.sharedKey!) - } - - mutating func syncVault() throws { - struct SyncResponse: Codable { - var ids: [String] - var ciphers: [LibrePassEncryptedCipher] - } - - let encryptedVault = try LibrePassEncryptedVault.loadVault() - self.vault = try encryptedVault.decryptVault(key: self.sharedKey!) - - if networkMonitor.isConnected { - let resp = try self.client.request(path: "/api/cipher/sync?lastSync=" + String(self.vault.lastSync), body: nil, method: "GET") - - let updated = try JSONDecoder().decode(SyncResponse.self, from: resp) - - var newVaultToSync: [Bool] = [] - var newVault: [LibrePassCipher] = [] - for (i, j) in self.vault.vault.enumerated() { - if updated.ids.firstIndex(where: { id in j.id == id }) == nil && !self.vault.toSync[i] { - continue - } - - if let i2 = updated.ciphers.firstIndex(where: { cipher in cipher.id == j.id }) { - if let last1 = updated.ciphers[i2].lastModified, let last2 = j.lastModified { - if last1 > last2 || !self.vault.toSync[i] { - self.vault.vault[i] = try LibrePassCipher(encCipher: updated.ciphers[i2], key: self.sharedKey!) - self.vault.toSync[i] = false - } - } else if !self.vault.toSync[i] { - self.vault.vault[i] = try LibrePassCipher(encCipher: updated.ciphers[i2], key: self.sharedKey!) - self.vault.toSync[i] = false - } - } - - if self.vault.toSync[i] { - if let _ = try? self.put(cipher: self.vault.vault[i]) { - self.vault.toSync[i] = false - } - } - - newVault.append(self.vault.vault[i]) - newVaultToSync.append(false) - } - - for index in stride(from: self.vault.idstoDelete.count - 1, through: 0, by: -1) { - if let _ = try? self.delete(id: self.vault.idstoDelete[index]) { - if let _ = try? self.vault.remove(id: self.vault.idstoDelete[index], save: true) { - self.vault.idstoDelete.remove(at: index) - } - } - } - - for encCipher in updated.ciphers { - if self.vault.vault.firstIndex(where: { cipher in encCipher.id == cipher.id }) == nil { - newVault.append(try LibrePassCipher(encCipher: encCipher, key: self.sharedKey!)) - newVaultToSync.append(false) - } - } - - self.vault.vault = newVault - self.vault.toSync = newVaultToSync - self.vault.lastSync = Int64(Date().timeIntervalSince1970) - try self.vault.encryptVault().saveVault() - } - } - - mutating func put(encryptedCipher: LibrePassEncryptedCipher) throws { - try self.put(cipher: LibrePassCipher(encCipher: encryptedCipher, key: self.sharedKey!)) - } - - mutating func put(cipher: LibrePassCipher) throws { - let encrypted = try LibrePassEncryptedCipher(cipher: cipher, key: self.sharedKey!) - - var toSync = false - if networkMonitor.isConnected { - let req = try JSONEncoder().encode(encrypted) - - _ = try self.client.request(path: "/api/cipher", body: req, method: "PUT") - } else { - toSync = true - } - - try self.vault.addOrReplace(cipher: cipher, toSync: toSync, save: true) - } - - mutating func delete(id: String) throws { - if networkMonitor.isConnected { - _ = try self.client.request(path: "/api/cipher/" + id, body: nil, method: "DELETE") - } else { - self.vault.idstoDelete.append(id) - } - - try self.vault.remove(id: id, save: true) - } - - mutating func updateCredentials(email: String, password: String, passwordHint: String, oldSharedKey: String) throws { - struct CompactCipher: Codable { - var id: String - var data: String - } - - struct LibrePassChangeEmailRequest: Codable { - var newEmail: String - var oldSharedKey: String - var newPublicKey: String - var newSharedKey: String - var ciphers: [CompactCipher] - } - - struct LibrePassChangePasswordRequest: Codable { - var oldSharedKey: String - var newPublicKey: String - var newSharedKey: String - var passwordHint: String - var parallelism: Int - var memory: Int - var iterations: Int - var ciphers: [CompactCipher] - } - - let (newPrivateData, newPublicData, newSharedData) = try self.getKeys(email: email, password: password, argon2options: self.credentialsDatabase!.argon2idParams) - - let vaultEncryptionKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: newPrivateData).sharedSecretFromKeyAgreement(with: try Curve25519.KeyAgreement.PublicKey(rawRepresentation: newPublicData)).withUnsafeBytes { - return Data(Array($0)) - } - - var newVault = self.vault - newVault.key = SymmetricKey(data: vaultEncryptionKey) - - var encryptedVault = try newVault.encryptVault() - var compactCiphers: [CompactCipher] = [] - for j in encryptedVault.vault { - compactCiphers.append(CompactCipher(id: j.id, data: j.protectedData)) - } - - var requestBody: Data - var path: String - if email != self.credentialsDatabase!.email { - let requestStruct = LibrePassChangeEmailRequest(newEmail: email, oldSharedKey: oldSharedKey, newPublicKey: dataToHexString(data: newPublicData), newSharedKey: dataToHexString(data: newSharedData), ciphers: compactCiphers) - - requestBody = try JSONEncoder().encode(requestStruct) - path = "/api/user/email" - } else if dataToHexString(data: newSharedData) != oldSharedKey { - let requestStruct = LibrePassChangePasswordRequest(oldSharedKey: oldSharedKey, newPublicKey: dataToHexString(data: newPublicData), newSharedKey: dataToHexString(data: newSharedData), passwordHint: passwordHint, parallelism: self.credentialsDatabase!.argon2idParams.parallelism, memory: self.credentialsDatabase!.argon2idParams.memory, iterations: self.credentialsDatabase!.argon2idParams.iterations, ciphers: compactCiphers) - - requestBody = try JSONEncoder().encode(requestStruct) - path = "/api/user/password" - } else { - throw LibrePassApiErrors.WithMessage(message: "Nothing is to be changed") - } - - _ = try self.client.request(path: path, body: requestBody, method: "PATCH") - } - - func deleteAccount(password: String) throws { - struct LibrePassDeleteAccountRequest: Codable { - var sharedKey: String - var code: String - } - - let (oldPrivateData, oldPublicData, oldSharedData) = try self.getKeys(email: self.credentialsDatabase!.email, password: password, argon2options: self.credentialsDatabase!.argon2idParams) - - if dataToHexString(data: oldPublicData) != self.credentialsDatabase!.publicKey { - throw LibrePassApiErrors.WithMessage(message: "Invalid credentials") - } - - let request = LibrePassDeleteAccountRequest(sharedKey: dataToHexString(data: oldSharedData), code: "") - let requestBody = try JSONEncoder().encode(request) - - _ = try self.client.request(path: "/api/user/delete", body: requestBody, method: "DELETE") - } - - func generateId() -> String { - var uuid = UUID().uuidString.lowercased() - while self.vault.vault.firstIndex(where: { cipher in cipher.id == uuid }) != nil { - uuid = UUID().uuidString.lowercased() - } - - return uuid - } -} - -struct LoginData: Codable { - var userId: String - var apiKey: String - var verified: Bool -} - -enum LibrePassApiErrors: Error { - case WithMessage(message: String) -} diff --git a/LibrePass/API/LibrePassCipher.swift b/LibrePass/API/LibrePassCipher.swift deleted file mode 100644 index d949060..0000000 --- a/LibrePass/API/LibrePassCipher.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// LibrePassCipher.swift -// LibrePass -// -// Created by Zapomnij on 18/02/2024. -// - -import Foundation -import CryptoKit -import SwiftUI - -class LibrePassCipher: Codable { - var id: String - var owner: String - var type: CipherType - var loginData: CipherLoginData? - var secureNoteData: CipherSecureNoteData? - var cardData: CipherCardData? - var collection: String? - var favorite: Bool = false - var rePrompt: Bool = false - var version: Int = 1 - var created: Int64? - var lastModified: Int64? - - init(id: String, owner: String, type: CipherType) { - self.id = id - self.owner = owner - self.type = type - self.created = Int64(Date().timeIntervalSince1970) - self.lastModified = self.created - - switch self.type { - case .Login: - self.loginData = CipherLoginData(name: "New") - break - case .SecureNote: - self.secureNoteData = CipherSecureNoteData(title: "New", note: "") - break - case .Card: - self.cardData = CipherCardData(name: "New", cardholderName: "", number: "") - break - } - } - - convenience init(encCipher: LibrePassEncryptedCipher, key: SymmetricKey) throws { - self.init(id: encCipher.id, owner: encCipher.owner, type: CipherType.Card) - self.collection = encCipher.collection - self.favorite = encCipher.favorite - self.rePrompt = encCipher.rePrompt - self.version = encCipher.version - self.created = encCipher.created - self.lastModified = encCipher.lastModified - - let decoder = JSONDecoder() - switch encCipher.type { - case 0: - self.loginData = try decoder.decode(CipherLoginData.self, from: aesGcmDecrypt(data: hexStringToData(string: encCipher.protectedData)!, key: key)) - self.type = CipherType.Login - break - case 1: - self.secureNoteData = try decoder.decode(CipherSecureNoteData.self, from: aesGcmDecrypt(data: hexStringToData(string: encCipher.protectedData)!, key: key)) - self.type = CipherType.SecureNote - break - case 2: - self.cardData = try decoder.decode(CipherCardData.self, from: aesGcmDecrypt(data: hexStringToData(string: encCipher.protectedData)!, key: key)) - self.type = CipherType.Card - break - default: - break - } - } - - enum CipherType: Codable { - case Login - case SecureNote - case Card - } - - struct CipherLoginData: Codable { - var name: String - var username: String? - var password: String? - var passwordHistory: [PasswordHistory]? - var uris: [String]? - var twoFactor: String? - var notes: String? - var fields: [CustomField]? - } - - struct CipherSecureNoteData: Codable { - var title: String - var note: String - var fields: [CustomField]? - } - - struct CipherCardData: Codable { - var name: String - var cardholderName: String - var number: String - var expMonth: Int? - var expYear: Int? - var code: String? - var notes: String? - var fields: [CustomField]? - } - - struct CustomField: Codable { - var name: String - var type: CipherFieldType - var value: String - } - - enum CipherFieldType: Codable { - case Text - case Hidden - } - - struct PasswordHistory: Codable { - var password: String - var lastUsed: Int - } -} diff --git a/LibrePass/API/LibrePassEncryptedCipher.swift b/LibrePass/API/LibrePassEncryptedCipher.swift deleted file mode 100644 index 478f12a..0000000 --- a/LibrePass/API/LibrePassEncryptedCipher.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// LibrePassEncryptedCipher.swift -// LibrePass -// -// Created by Zapomnij on 18/02/2024. -// - -import Foundation -import CryptoKit - -class LibrePassEncryptedCipher: Codable { - var id: String - var owner: String - var type: Int - var protectedData: String - var collection: String? - var favorite: Bool - var rePrompt: Bool - var version: Int - var created: Int64? - var lastModified: Int64? - - init(cipher: LibrePassCipher, key: SymmetricKey) throws { - self.id = cipher.id - self.owner = cipher.owner - - switch cipher.type { - case .Login: - self.type = 0 - self.protectedData = dataToHexString(data: try aesGcmEncrypt(data: JSONEncoder().encode(cipher.loginData!), key: key)) - break - case .SecureNote: - self.type = 1 - self.protectedData = dataToHexString(data: try aesGcmEncrypt(data: JSONEncoder().encode(cipher.secureNoteData!), key: key)) - break - case .Card: - self.type = 2 - self.protectedData = dataToHexString(data: try aesGcmEncrypt(data: JSONEncoder().encode(cipher.cardData!), key: key)) - break - } - - self.collection = cipher.collection - self.favorite = cipher.favorite - self.rePrompt = cipher.rePrompt - self.version = cipher.version - self.created = cipher.created - self.lastModified = cipher.lastModified - } -} diff --git a/LibrePass/API/OATH.swift b/LibrePass/API/OATH.swift deleted file mode 100644 index f951908..0000000 --- a/LibrePass/API/OATH.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// OATH.swift -// LibrePass -// -// Created by Zapomnij on 16/03/2024. -// - -import Foundation -import SwiftOTP -import SwiftUI - -extension String { - func toOTPAlgorithm() -> SwiftOTP.OTPAlgorithm { - switch self { - case "SHA1": - return .sha1 - case "SHA256": - return .sha256 - case "SHA512": - return .sha512 - default: - return .sha1 - } - } -} - -extension SwiftOTP.OTPAlgorithm { - func toString() -> String { - switch self { - case .sha1: - return "SHA1" - case .sha256: - return "SHA256" - case .sha512: - return "SHA512" - } - } -} - -var stop = false -var running = false -struct OATHParams { - enum OATHType { - case TOTP - case HOTP - - func toString() -> String { - switch self { - case .TOTP: - return "totp" - case .HOTP: - return "htop" - } - } - } - - var uri: String - - var type: OATHType = .TOTP - var algorithm: SwiftOTP.OTPAlgorithm = .sha1 - var secret: Data = Data() - var digits: Int = 6 - var period: Int = 30 - var counter: Int = 0 - - init(uri: String) throws { - self.uri = uri - - if uri.starts(with: "otpauth://totp/") { - self.type = .TOTP - } else { - self.type = .HOTP - } - - let uriSplit = uri.components(separatedBy: "?")[1].components(separatedBy: "&") - - for keyVal in uriSplit { - let split = keyVal.components(separatedBy: "=") - let key = split[0], val = split[1] - - switch key { - case "secret": - guard let secret = base32Decode(val) else { - throw LibrePassApiErrors.WithMessage(message: "Bad 2FA secret") - } - self.secret = Data(secret) - break - case "algorithm": - self.algorithm = val.toOTPAlgorithm() - break - case "digits": - self.digits = Int(val) ?? 6 - break - case "counter": - self.counter = Int(val) ?? 0 - break - case "period": - self.period = Int(val) ?? 30 - break - default: - break - } - } - } - - func runTOTPCounter(callback: (_ oneTimePassword: String, _ timeLeft: Int) -> ()) async { - running = true - if let totp = TOTP(secret: self.secret, digits: self.digits, timeInterval: self.period, algorithm: self.algorithm) { - var counter = Int(Date().timeIntervalSince1970) % self.period - var password = totp.generate(time: Date()) ?? "" - while !stop { - if counter == 0 { - if let newPassword = totp.generate(time: Date()) { - password = newPassword - } - } - - callback(password, self.period - counter) - try? await Task.sleep(nanoseconds: UInt64(0.5 * 1000000000)) - counter = Int(Date().timeIntervalSince1970) % self.period - } - - stop = false - running = false - } - } -} diff --git a/LibrePass/API/PasswordGenerator.swift b/LibrePass/API/PasswordGenerator.swift deleted file mode 100644 index 8dfad2b..0000000 --- a/LibrePass/API/PasswordGenerator.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// PasswordGenerator.swift -// LibrePass -// -// Created by Zapomnij on 01/03/2024. -// - -import Foundation - -func generatePassword(length: Int) throws -> String { - if length < 8 { - throw LibrePassApiErrors.WithMessage(message: "Password length must be longer or equal to 8") - } - - let nOfNumbers = length / 4 - let nOfSpecial = length / 4 - let nOfLowercased = length / 4 - let nOfUppercased = length - nOfSpecial - nOfLowercased - nOfNumbers - - var password = [Character?](repeating: nil, count: length) - - func fill(from: Int, to: Int, times: Int) { - var filled = 0 - while filled < times { - let char = Character(UnicodeScalar(Int.random(in: from...to))!) - - if password.first(where: { ch in ch == char }) == nil { - while true { - let index = Int.random(in: 0...length - 1) - if password[index] == nil { - password[index] = char - filled += 1 - break - } - } - } - } - } - - for _ in stride(from: 0, to: nOfSpecial, by: 1) { - switch Int.random(in: 0...3) { - case 0: - fill(from: 32, to: 47, times: 1) - break - case 1: - fill(from: 58, to: 64, times: 1) - break - case 2: - fill(from: 91, to: 96, times: 1) - break - case 3: - fill(from: 123, to: 126, times: 1) - break - default: - print("This won't ever happen") - } - } - - fill(from: 48, to: 57, times: nOfNumbers) - fill(from: 97, to: 122, times: nOfLowercased) - fill(from: 65, to: 90, times: nOfUppercased) - - var string = "" - password.forEach { - if let char = $0 { - string += String(char) - } - } - - return string -} diff --git a/LibrePass/API/Vault.swift b/LibrePass/API/Vault.swift deleted file mode 100644 index d89d8f0..0000000 --- a/LibrePass/API/Vault.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// Vault.swift -// LibrePass -// -// Created by Zapomnij on 22/02/2024. -// - -import Foundation -import CryptoKit - -struct LibrePassEncryptedVault: Codable { - var vault: [LibrePassEncryptedCipher] = [] - var toSync: [Bool] = [] - var idsToDelete: [String] = [] - var lastSync: Int64 - - static func loadVault() throws -> Self { - let vaultJson = UserDefaults.standard.data(forKey: "vault")! - - return try JSONDecoder().decode(Self.self, from: vaultJson) - } - - func saveVault() throws { - let data = try JSONEncoder().encode(self) - - UserDefaults.standard.setValue(data, forKey: "vault") - } - - func decryptVault(key: SymmetricKey) throws -> LibrePassDecryptedVault { - var ciphers: LibrePassDecryptedVault = LibrePassDecryptedVault(toSync: self.toSync, idstoDelete: self.idsToDelete, lastSync: self.lastSync, key: key) - for (i, encCipher) in self.vault.enumerated() { - try ciphers.addOrReplace(cipher: try LibrePassCipher(encCipher: encCipher, key: key), toSync: self.toSync[i], save: false) - } - - return ciphers - } - - static func isVaultSavedLocally() -> Bool { - return UserDefaults.standard.data(forKey: "vault") != nil - } -} - -struct LibrePassDecryptedVault { - var vault: [LibrePassCipher] = [] - var toSync: [Bool] = [] - var idstoDelete: [String] = [] - var lastSync: Int64 - var key: SymmetricKey? - - mutating func addOrReplace(cipher: LibrePassCipher, toSync: Bool, save: Bool) throws { - if let idx = self.vault.firstIndex(where: { ciph in ciph.id == cipher.id }) { - self.vault[idx] = cipher - self.toSync[idx] = toSync - } else { - self.vault.append(cipher) - self.toSync.append(toSync) - } - - if save { - try self.encryptVault().saveVault() - } - } - - mutating func remove(id: String, save: Bool) throws { - if let cipher = self.vault.first(where: { cipher in cipher.id == id }) { - try self.remove(cipher: cipher, save: save) - } - } - - mutating func remove(cipher: LibrePassCipher, save: Bool) throws { - if let idx = self.vault.firstIndex(where: { ciph in ciph.id == cipher.id }) { - self.vault.remove(at: idx) - self.toSync.remove(at: idx) - - if save { - try self.encryptVault().saveVault() - } - } - } - - func encryptVault() throws -> LibrePassEncryptedVault { - var encCiphers: LibrePassEncryptedVault = LibrePassEncryptedVault(toSync: self.toSync, idsToDelete: self.idstoDelete, lastSync: self.lastSync) - for cipher in self.vault { - encCiphers.vault.append(try LibrePassEncryptedCipher(cipher: cipher, key: self.key!)) - } - - return encCiphers - } -} diff --git a/LibrePass/App/LibrePassApp.swift b/LibrePass/App/LibrePassApp.swift new file mode 100644 index 0000000..f77745a --- /dev/null +++ b/LibrePass/App/LibrePassApp.swift @@ -0,0 +1,30 @@ +// +// LibrePassApp.swift +// LibrePass +// +// Created by Zapomnij on 17/02/2024. +// + +import SwiftUI +import SwiftData +import KeychainSwift + +@main +struct LibrePassApp: App { + let keychain = KeychainSwift() + @StateObject var authViewModel = AuthViewModel() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(authViewModel) + .modelContainer(for: Credentials.self) + } + } + + +} + +class KeychainManager { + static let shared = KeychainSwift() +} diff --git a/LibrePass/Configs/Dev.xcconfig b/LibrePass/Configs/Dev.xcconfig new file mode 100644 index 0000000..26c1be0 --- /dev/null +++ b/LibrePass/Configs/Dev.xcconfig @@ -0,0 +1,11 @@ +// +// Dev.xcconfig +// LibrePass +// +// Created by Nish on 2024-04-12. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +ROOT_URL=http:/$()/localhost:8080 diff --git a/LibrePass/Configs/Prod.xcconfig b/LibrePass/Configs/Prod.xcconfig new file mode 100644 index 0000000..f192e89 --- /dev/null +++ b/LibrePass/Configs/Prod.xcconfig @@ -0,0 +1,11 @@ +// +// Prod.xcconfig +// LibrePass +// +// Created by Nish on 2024-04-12. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +ROOT_URL=https:/$()/api.librepass.org diff --git a/LibrePass/Core/Auth/Model/Credentials.swift b/LibrePass/Core/Auth/Model/Credentials.swift new file mode 100644 index 0000000..02b6916 --- /dev/null +++ b/LibrePass/Core/Auth/Model/Credentials.swift @@ -0,0 +1,46 @@ +// +// Credentials.swift +// LibrePass +// +// Created by Nish on 2024-04-15. +// + +import Foundation +import SwiftData + +@Model +class Credentials { + @Attribute(.unique) var userId: UUID + var email: String + var apiUrl: String? + var apiKey: String + var publicKey: String + var lastSync: Int64? + var memory: Int + var iterations: Int + var parallelism: Int + + init( + userId: UUID, + email: String, + apiUrl: String? = nil, + apiKey: String, + publicKey: String, + lastSync: Int64? = nil, + memory: Int, + iterations: Int, + parallelism: Int + ) { + self.userId = userId + self.email = email + self.apiUrl = apiUrl + self.apiKey = apiKey + self.publicKey = publicKey + self.lastSync = lastSync + self.memory = memory + self.iterations = iterations + self.parallelism = parallelism + } + +} + diff --git a/LibrePass/Core/Auth/Model/LoginRequest.swift b/LibrePass/Core/Auth/Model/LoginRequest.swift new file mode 100644 index 0000000..e4899fe --- /dev/null +++ b/LibrePass/Core/Auth/Model/LoginRequest.swift @@ -0,0 +1,13 @@ +// +// LoginRequest.swift +// LibrePass +// +// Created by Nish on 2024-04-14. +// + +import Foundation + +struct LoginRequest: Codable { + let email: String + let sharedKey: String +} diff --git a/LibrePass/Core/Auth/Model/PreLoginResponse.swift b/LibrePass/Core/Auth/Model/PreLoginResponse.swift new file mode 100644 index 0000000..1b42906 --- /dev/null +++ b/LibrePass/Core/Auth/Model/PreLoginResponse.swift @@ -0,0 +1,15 @@ +// +// PreLoginResponse.swift +// LibrePass +// +// Created by Nish on 2024-04-13. +// + +import Argon2Swift + +struct PreLoginResponse: Decodable { + let parallelism: Int + let memory: Int + let iterations: Int + let serverPublicKey: String +} diff --git a/LibrePass/Core/Auth/Model/RegisterRequest.swift b/LibrePass/Core/Auth/Model/RegisterRequest.swift new file mode 100644 index 0000000..c6f4d4d --- /dev/null +++ b/LibrePass/Core/Auth/Model/RegisterRequest.swift @@ -0,0 +1,18 @@ +// +// RegisterRequest.swift +// LibrePass +// +// Created by Nish on 2024-04-15. +// + +import Foundation + +struct RegisterRequest: Codable { + let email: String + let passwordHint: String? + let sharedKey: String + let parallelism: Int + let memory: Int + let iterations: Int + let publicKey: String +} diff --git a/LibrePass/Core/Auth/Model/UserCredentialResponse.swift b/LibrePass/Core/Auth/Model/UserCredentialResponse.swift new file mode 100644 index 0000000..e3cb892 --- /dev/null +++ b/LibrePass/Core/Auth/Model/UserCredentialResponse.swift @@ -0,0 +1,14 @@ +// +// UserCredentialResponse.swift +// LibrePass +// +// Created by Nish on 2024-04-14. +// + +import Foundation + +class UserCredentialResponse: Decodable { + let userId: UUID + let apiKey: String + let verified: Bool +} diff --git a/LibrePass/Core/Auth/ViewModel/AuthViewModel.swift b/LibrePass/Core/Auth/ViewModel/AuthViewModel.swift new file mode 100644 index 0000000..9f21f48 --- /dev/null +++ b/LibrePass/Core/Auth/ViewModel/AuthViewModel.swift @@ -0,0 +1,318 @@ +// +// AuthViewModel.swift +// LibrePass +// +// Created by Nish on 2024-04-06. +// + +import Foundation +import CryptoKit +import Argon2Swift +import Alamofire +import KeychainSwift +import LocalAuthentication + +protocol AuthenticationFormProtocol { + var formIsValid: Bool { get } +} + +class AuthViewModel: ObservableObject { + @Published var isLoggedIn = false + @Published var isValidated = false + @Published var error: AuthenticationError? + + let endpoint = "/api/auth" + + + func signIn(withEmail email: String, password: String) async throws { + do { + _ = try await login(withEmail: email, password: password) + + } catch { + print("Debug: Failed to signIn with error: \(error.localizedDescription)") + } + } + + func signUp(withEmail email: String, password: String) async throws { + do { + let preLoginData = try await preLogin(email: "") + + let passwordHash = try Argon2Swift.hashPasswordBytes( + password: password.data(using: .utf8)!, + salt: Salt(bytes: email.data(using: .utf8)!), + iterations: preLoginData.iterations, + memory: preLoginData.memory, + parallelism: preLoginData.parallelism, + type: .id, + version: .V13 + ) + + let serverPublicKeyHex = Data(preLoginData.serverPublicKey.hexToBytes()!) + let serverPublicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: serverPublicKeyHex) + KeychainManager.shared.set(serverPublicKey.rawRepresentation, forKey: "serverPublicKey") + + let privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: passwordHash.hashData()) + KeychainManager.shared.set(privateKey.rawRepresentation, forKey: "userPrivateKey") + + let sharedSecretKey = try privateKey.sharedSecretFromKeyAgreement(with: serverPublicKey) + let sharedSecretKeyHex = sharedSecretKey.withUnsafeBytes { bytes in + return Data(bytes).map { String(format: "%02hhx", $0) }.joined() + } + + let body = try JSONEncoder().encode(RegisterRequest( + email: email, + passwordHint: "", + sharedKey: sharedSecretKeyHex, + parallelism: preLoginData.parallelism, + memory: preLoginData.memory, + iterations: preLoginData.iterations, + publicKey: privateKey.publicKey.rawRepresentation.map { String(format: "%02hhx", $0) }.joined() + )) + + let url = URL(string: CustomEnvironment.rootURL + endpoint + "/register")! + var urlRequest = URLRequest(url: url) + urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") + urlRequest.httpMethod = "POST" + urlRequest.httpBody = body + + let (data, response) = try await URLSession.shared.data(for: urlRequest) + + guard let response = response as? HTTPURLResponse else { + throw NetworkError.invalidResponse + } + + guard (200...299).contains(response.statusCode) else { + throw NetworkError.statusCode(response.statusCode) + } + + do { + let resp = try JSONDecoder().decode(UserCredentialResponse.self, from: data) + } catch { + throw NetworkError.decodingError(error) + } + + + + + + } catch { + print("Debug: Failed to sigup with error: \(error.localizedDescription)") + } + } + + private func preLogin(email: String) async throws -> PreLoginResponse { + let parameters: [String: Any] = [ + "email" : email + ] + + let headers: HTTPHeaders = [ + "Accept": "application/json" + ] + + return try await withCheckedThrowingContinuation { continuation in + AF.request(CustomEnvironment.rootURL + endpoint + "/preLogin", method: .get, parameters: parameters, headers: headers) + .responseDecodable(of: PreLoginResponse.self) { response in + switch response.result { + case let .success(data): + continuation.resume(returning: data) + case let .failure(error): + continuation.resume(throwing: error) + } + } + } + } + + private func login(withEmail email: String, password: String) async throws { + let preLoginData = try await preLogin(email: email) + + let passwordHash = try Argon2Swift.hashPasswordBytes( + password: password.data(using: .utf8)!, + salt: Salt(bytes: email.data(using: .utf8)!), + iterations: preLoginData.iterations, + memory: preLoginData.memory, + parallelism: preLoginData.parallelism, + type: .id, + version: .V13 + ) + + let serverPublicKeyHex = Data(preLoginData.serverPublicKey.hexToBytes()!) + let serverPublicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: serverPublicKeyHex) + //check if there's an existing key in keychain + KeychainManager.shared.set(serverPublicKey.rawRepresentation, forKey: "serverPublicKey") + + let privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: passwordHash.hashData()) + KeychainManager.shared.set(privateKey.rawRepresentation, forKey: "userPrivateKey") + + let sharedSecretKey = try privateKey.sharedSecretFromKeyAgreement(with: serverPublicKey) + let sharedSecretKeyHex = sharedSecretKey.withUnsafeBytes { bytes in + return Data(bytes).map { String(format: "%02hhx", $0) }.joined() + } + + let body = try JSONEncoder().encode(LoginRequest(email: email, sharedKey: sharedSecretKeyHex)) + + let url = URL(string: CustomEnvironment.rootURL + endpoint + "/oauth?grantType=login")! + var urlRequest = URLRequest(url: url) + urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") + urlRequest.httpMethod = "POST" + urlRequest.httpBody = body + + let (data, response) = try await URLSession.shared.data(for: urlRequest) + + guard let response = response as? HTTPURLResponse else { + throw NetworkError.invalidResponse + } + + guard (200...299).contains(response.statusCode) else { + throw NetworkError.statusCode(response.statusCode) + } + + do { + let resp = try JSONDecoder().decode(UserCredentialResponse.self, from: data) + } catch { + throw NetworkError.decodingError(error) + } + + self.isLoggedIn = true + + // let aesKey = try privateKey.sharedSecretFromKeyAgreement(with: privateKey.publicKey) + + + } + + func biometricType() -> BiometricType { + let authContext = LAContext() + let _ = authContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) + switch authContext.biometryType { + case .none: + return .none + case .touchID: + return .touch + case .faceID: + return .face + case .opticID: + return .optic + @unknown default: + return .none + } + } + + enum BiometricType { + case none + case face + case touch + case optic + } + + func requestBiometricUnlock(completion: @escaping (Result) -> Void) { + let credentials: Credentials? = nil +// let credentials: Credentials? = Credentials( +// userId: UUID(), +// email: "test", +// apiKey: "apikey", +// publicKey: "publickey", +// memory: 64950, +// iterations: 3, +// parallelism: 3 +// ) + + guard let credentials = credentials else { + completion(.failure(.credentialsNotSaved)) + return + } + + let context = LAContext() + var error: NSError? + let canEvalute = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + if let error = error { + switch error.code { + case -6: + completion(.failure(.deniedAccess)) + case -7: + if context.biometryType == .faceID { + completion(.failure(.noFaceIdEnrolled)) + } else { + completion(.failure(.noFingerprintEnrolled)) + } + default: + completion(.failure(.biometricError)) + } + return + } + + if canEvalute { + if context.biometryType != .none { + context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Need access to credentials.") { + success, error in + DispatchQueue.main.async { + if error != nil { + completion(.failure(.biometricError)) + } else { + completion(.success(credentials)) + } + } + } + } + } + } + + enum NetworkError: Error { + case invalidResponse + case statusCode(Int) + case decodingError(Error) + + var id: String { + self.localizedDescription + } + + // var errorDescription: String? { + // + // } + } + + enum AuthenticationError: Error, LocalizedError, Identifiable { + case invalidCredentials + case deniedAccess + case noFaceIdEnrolled + case noFingerprintEnrolled + case biometricError + case credentialsNotSaved + + var id: String { + self.localizedDescription + } + + var errorDescription: String? { + switch self { + case .invalidCredentials: + return NSLocalizedString("Either your email or password are incorrect. Please try again.", comment: "") + case .deniedAccess: + return NSLocalizedString("You have denied access. Please go to settings app and located this application and turn on Face ID.", comment: "") + case .noFaceIdEnrolled: + return NSLocalizedString("You have not registered any Face ID yet.", comment: "") + case .noFingerprintEnrolled: + return NSLocalizedString("You have not registered any fingerprint yet.", comment: "") + case .biometricError: + return NSLocalizedString("Your face or fingerprint were not recognized.", comment: "") + case .credentialsNotSaved: + return NSLocalizedString("You credentials have not been saved. Do you want to save them after the next successful login?", comment: "") + } + } + } + + func updateValidation(success: Bool) { + isValidated = success + } +} + +extension String { + func hexToBytes() -> [UInt8]? { + var startIndex = self.startIndex + return (0..) in + switch result { + case .success(let credentials): + authViewModel.isLoggedIn = true + case .failure(let error): + authViewModel.error = error + } + } + } label: { + Image(systemName: authViewModel.biometricType() == .face ? "faceid" : "touchid") + .resizable() + .frame(width: 50, height: 50) + .padding() + } + } + + Spacer() + + NavigationLink { + SignupView() + } label: { + HStack(spacing: 2) { + Text("Don't have an account?") + Text("Sign up") + .fontWeight(.bold) + } + .font(.system(size: 16)) + + } + .padding() + } + .alert(item: $authViewModel.error) { error in + if error == .credentialsNotSaved { + return Alert( + title: Text("Credentials Not Saved"), + message: Text(error.localizedDescription), + primaryButton: .default(Text("OK"), action: {}), + secondaryButton: .cancel() + ) + } else { + return Alert(title: Text("Invalid Login"), message: Text(error.localizedDescription)) + } + } + .sheet(isPresented: $isShowingCustomURLSheet) { + NavigationStack { + ServerURLView(serverURL: $customServerURL, isPresented: $isShowingCustomURLSheet) + } + } + } +} + +extension LoginView: AuthenticationFormProtocol { + var formIsValid: Bool { + return !email.isEmpty + && email.contains("@") + && !password.isEmpty + && password.count > 7 + } +} + +#Preview { + LoginView() + .environmentObject(AuthViewModel()) +} diff --git a/LibrePass/Core/Auth/Views/SignupView.swift b/LibrePass/Core/Auth/Views/SignupView.swift new file mode 100644 index 0000000..9ac8d51 --- /dev/null +++ b/LibrePass/Core/Auth/Views/SignupView.swift @@ -0,0 +1,194 @@ +// +// SignupView.swift +// LibrePass +// +// Created by Nish on 2024-04-06. +// + +import SwiftUI + +struct SignupView: View { + + @State private var email = "" + @State private var fullname = "" + @State private var password = "" + @State private var confirmPassword = "" + @State private var customServerURL = "" + @State private var isShowingCustomURLSheet = false + @State private var selectedServerType: ServerType = .official + + @Environment(\.dismiss) var dismiss + @EnvironmentObject var authViewModel: AuthViewModel + + + var body: some View { + VStack { + Image("Icon") + .resizable() + .frame(width: 170, height: 170) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .padding() + + + VStack(spacing: 24) { + InputView(text: $email, title: "Email Address", placeholder: "name@example.com") + .autocapitalization(.none) + + InputView(text: $password, title: "Password", placeholder: "Enter your password", isSecureField: true) + + ZStack(alignment: .trailing) { + InputView(text: $confirmPassword, title: "Confirm Password", placeholder: "Confirm your password", isSecureField: true) + + if !password.isEmpty && !confirmPassword.isEmpty { + if password == confirmPassword { + Image(systemName: "checkmark.circle.fill") + .imageScale(.large) + .fontWeight(.bold) + .foregroundColor(Color(.systemGreen)) + + } else { + Image(systemName: "xmark.circle.fill") + .imageScale(.large) + .fontWeight(.bold) + .foregroundColor(Color(.systemRed)) + } + } + } + + HStack { + Text ("Server Address") + .font(.footnote) + + Spacer() + + Picker("Server Type", selection: $selectedServerType) { + Text("Official").tag(ServerType.official) + Text("Self-Hosted").tag(ServerType.selfHosted) + } + .padding() + .onChange(of: selectedServerType) { oldValue, newValue in + if newValue == .selfHosted { + isShowingCustomURLSheet = true + } else { + isShowingCustomURLSheet = false + customServerURL = CustomEnvironment.rootURL + } + } + } + + } + .padding(.horizontal) + .padding(.top, 12) + + + Button { + Task { + try await authViewModel.signUp(withEmail: email, password: password) + } + } label: { + HStack { + Text ("SIGN UP") + .fontWeight(.semibold) + Image(systemName: "arrow.right") + } + .foregroundColor(.white) + .frame(width: UIScreen.main.bounds.width - 32, height: 48) + } + .background(Color(.systemBlue)) + .disabled(!formIsValid) + .opacity(formIsValid ? 1.0 : 0.5 ) + .cornerRadius(10) + .padding(.top, 24) + + Spacer() + + Button { + dismiss() + + } label: { + HStack(spacing: 2) { + Text("Already have an account?") + Text("Sign in") + .fontWeight(.bold) + } + .font(.system(size: 16)) + + } + .padding() + } + .navigationTitle("Register") + .sheet(isPresented: $isShowingCustomURLSheet) { + NavigationStack { + ServerURLView(serverURL: $customServerURL, isPresented: $isShowingCustomURLSheet) + } + } + } +} + +extension SignupView: AuthenticationFormProtocol { + var formIsValid: Bool { + return !email.isEmpty + && email.contains("@") + && !password.isEmpty + && confirmPassword == password + && password.count > 7 + } +} + +struct InputView: View { + @Binding var text: String + let title: String + let placeholder: String + var isSecureField = false + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .foregroundColor(Color(.darkGray)) + .fontWeight(.semibold) + .font(.footnote) + + if isSecureField { + SecureField(placeholder, text: $text) + .font(.system(size: 16)) + } else { + TextField(placeholder, text: $text) + .font(.system(size: 16)) + } + + Divider() + + } + } +} + +struct ServerURLView: View { + @Binding var serverURL: String + @Binding var isPresented: Bool + + var body: some View { + NavigationStack { + VStack { + List { + TextField("Enter Server URL", text: $serverURL) + } + .background(Color(.systemGray6)) + } + .navigationTitle("Self-Hosted") + .navigationBarItems(trailing: Button("Done") { + UserDefaults.standard.set(serverURL, forKey: "SelfHostedURL") + isPresented = false + }) + } + .background(Color(.systemGray6).ignoresSafeArea()) + } +} + +enum ServerType { + case official + case selfHosted +} + +#Preview { + SignupView() +} diff --git a/LibrePass/Core/Home/HomeView.swift b/LibrePass/Core/Home/HomeView.swift new file mode 100644 index 0000000..b64bad4 --- /dev/null +++ b/LibrePass/Core/Home/HomeView.swift @@ -0,0 +1,18 @@ +// +// HomeView.swift +// LibrePass +// +// Created by Nish on 2024-04-12. +// + +import SwiftUI + +struct HomeView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + HomeView() +} diff --git a/LibrePass/Core/Root/ContentView.swift b/LibrePass/Core/Root/ContentView.swift new file mode 100644 index 0000000..2f5950e --- /dev/null +++ b/LibrePass/Core/Root/ContentView.swift @@ -0,0 +1,27 @@ +// +// ContentView.swift +// LibrePass +// +// Created by Nish on 2024-04-06. +// + +import SwiftUI + +struct ContentView: View { + @EnvironmentObject var authViewModel: AuthViewModel + + var body: some View { + Group { + if authViewModel.isLoggedIn { + HomeView() + } else { + LoginView() + } + } + } +} + +#Preview { + ContentView() + .environmentObject(AuthViewModel()) +} diff --git a/LibrePass/CustomEnvironment.swift b/LibrePass/CustomEnvironment.swift new file mode 100644 index 0000000..bd06658 --- /dev/null +++ b/LibrePass/CustomEnvironment.swift @@ -0,0 +1,26 @@ +// +// Environment.swift +// LibrePass +// +// Created by Nish on 2024-04-06. +// + +import Foundation + +public enum CustomEnvironment { + private static let infoDictionary: [String: Any] = { + guard let dict = Bundle.main.infoDictionary else { + fatalError("Plist file not found") + } + return dict + }() + + static let rootURL: String = { + guard let rootURLstring = CustomEnvironment.infoDictionary["ROOT_URL"] as? String else { + fatalError("Root URL not set in plist for this environment") + } + + return rootURLstring + }() +} + diff --git a/LibrePass/Info.plist b/LibrePass/Info.plist index 0c67376..ae847ca 100644 --- a/LibrePass/Info.plist +++ b/LibrePass/Info.plist @@ -1,5 +1,8 @@ - + + ROOT_URL + ${ROOT_URL} + diff --git a/LibrePass/LibrePassAccountSettings.swift b/LibrePass/LibrePassAccountSettings.swift deleted file mode 100644 index 4ee66a1..0000000 --- a/LibrePass/LibrePassAccountSettings.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// LibrePassAccountSettings.swift -// LibrePass -// -// Created by Zapomnij on 29/02/2024. -// - -import SwiftUI -import CryptoKit - -struct LibrePassAccountSettings: View { - @Binding var lClient: LibrePassClient - @Binding var locallyLoggedIn: Bool - @Binding var loggedIn: Bool - - @State var password = String() - @State var email = String() - @State var newPassword = String() - @State var newPasswordConfirm = String() - @State var newPasswordHint = String() - - @State var done = false - - var body: some View { - List { - Section(header: Text("New password. Leave empty if you don't want to change")) { - SecureField("New password", text: self.$newPassword) - .autocapitalization(.none) - SecureField("Confirm password", text: self.$newPasswordConfirm) - .autocapitalization(.none) - TextField("Password hint", text: self.$newPasswordHint) - } - - Section(header: Text("Email")) { - TextField("Email", text: self.$email) - .autocapitalization(.none) - } - - Section(header: Text("Confirm action")) { - SecureField("Current password", text: self.$password) - .autocapitalization(.none) - ButtonWithSpinningWheel(text: "Update credentials", task: self.updateCredentials) - ButtonWithSpinningWheel(text: "Delete account", task: self.deleteAccount, color: Color.red) - } - - Section { - Button("Log out", role: .destructive) { - self.logOut() - } - } - } - - .alert("Operation is finished. You'll be logged out. If you've changed email address, check your mailbox and verify email address", isPresented: self.$done) { - Button("OK") { - self.locallyLoggedIn = false - self.loggedIn = false - } - } - - .onAppear { - self.email = self.lClient.credentialsDatabase!.email - } - } - - func deleteAccount() throws { - try self.lClient.deleteAccount(password: self.password) - self.logOut() - } - - func logOut() { - self.locallyLoggedIn = false - self.loggedIn = false - self.lClient.logOut() - } - - func updateCredentials() throws { - let (oldPrivateData, oldPublicData, oldSharedData) = try self.lClient.getKeys(email: self.lClient.credentialsDatabase!.email, password: self.password, argon2options: self.lClient.credentialsDatabase!.argon2idParams) - - if dataToHexString(data: oldPublicData) != self.lClient.credentialsDatabase!.publicKey { - throw LibrePassApiErrors.WithMessage(message: "Invalid credentials") - } - - if email != self.lClient.credentialsDatabase!.email && !self.newPassword.isEmpty { - throw LibrePassApiErrors.WithMessage(message: "Both settings (email and password) cannot be changed at the same time.") - } - - _ = try? self.lClient.syncVault() - - if self.newPassword != "" { - if self.newPassword != self.newPasswordConfirm { - throw LibrePassApiErrors.WithMessage(message: "Passwords doesn't match") - } - - self.password = self.newPassword - } - - try self.lClient.updateCredentials(email: self.email, password: self.password, passwordHint: self.newPasswordHint, oldSharedKey: dataToHexString(data: oldSharedData)) - - self.done = true - } -} diff --git a/LibrePass/LibrePassApp.swift b/LibrePass/LibrePassApp.swift deleted file mode 100644 index b90c3ff..0000000 --- a/LibrePass/LibrePassApp.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// LibrePassApp.swift -// LibrePass -// -// Created by Zapomnij on 17/02/2024. -// - -import SwiftUI - -@main -struct LibrePassApp: App { - - var body: some Scene { - WindowGroup { - MainWindow() - } - } -} - -var networkMonitor = NetworkMonitor() - -struct MainWindow: View { - @State var lClient: LibrePassClient = LibrePassClient(apiUrl: "") - - @State var localLogIn = false - @State var loggedIn = false - - @State var showAbout = false - - var body: some View { - HStack { - if self.loggedIn { - LibrePassManagerWindow(lClient: $lClient, loggedIn: $loggedIn, locallyLoggedIn: $localLogIn) - } else if self.localLogIn { - LibrePassLocalLogin(lClient: $lClient, loggedIn: $loggedIn, localLogIn: $localLogIn) - } else { - NavigationView { - List { - NavigationLink(destination: LibrePassLoginWindow(lClient: $lClient, loggedIn: $loggedIn, localLogIn: $localLogIn)) { - Text("Log in") - } - NavigationLink(destination: LibrePassRegistrationWindow(lClient: $lClient)) { - Text("Register") - } - } - - .navigationTitle("Welcome to LibrePass!") - .toolbar { - Button(action: { self.showAbout = true }) { - Image(systemName: "info.circle") - } - } - } - } - } - - .onAppear { - self.localLogIn = LibrePassCredentialsDatabase.isLocallyLoggedIn() - } - - .sheet(isPresented: self.$showAbout) { - VStack { - Image("Icon") - .resizable() - .cornerRadius(5.0) - .frame(width: 100, height: 100) - .padding() - - Text("Copyright © 2024 LibrePass Team") - Text("LibrePass server: Medzik (Oskar) and contributors") - Text("LibrePass app for iOS: aeoliux (Jacek)") - Text("App is licensed under GPL v3 license") - - Link("See on Github", destination: URL(string: "https://github.com/LibrePass")!) - .padding() - - Link("See website", destination: URL(string: "https://librepass.org")!) - .padding() - } - .padding() - } - } -} - - -//#Preview { -// MainWindow() -//} diff --git a/LibrePass/LibrePassCipherView.swift b/LibrePass/LibrePassCipherView.swift deleted file mode 100644 index 40ef717..0000000 --- a/LibrePass/LibrePassCipherView.swift +++ /dev/null @@ -1,388 +0,0 @@ -// -// LibrePassCipherView.swift -// Librepass -// -// Created by Zapomnij on 19/02/2024. -// - -import SwiftUI -import SwiftOTP - -struct CipherView: View { - @Binding var lClient: LibrePassClient - var cipher: LibrePassCipher - var index: Int - - func save(cipher: LibrePassCipher) { - do { - cipher.lastModified = Int64(Date().timeIntervalSince1970) - try lClient.put(cipher: cipher) - try lClient.syncVault() - } catch { - - } - } - - var body: some View { - switch self.cipher.type { - case LibrePassCipher.CipherType.Login: - CipherLoginDataView(cipher: self.cipher, index: index, save: save) - case LibrePassCipher.CipherType.SecureNote: - CipherSecureNoteView(cipher: self.cipher, index: index, save: save) - case LibrePassCipher.CipherType.Card: - CipherCardDataView(cipher: self.cipher, index: index, save: save) - } - } -} - -struct CipherLoginDataView: View { - var cipher: LibrePassCipher - var index: Int - var save: (_ save: LibrePassCipher) -> () - - @State var showPassword: Bool = false - @State var name = String() - @State var username = String() - @State var password = String() - @State var uris: [String] = [] - @State var notes = String() - @State var twoFactorUri: String? - - @State var passwordLength = 0 - @State var generatePasswordAlert = false - - @State var oneTimePassword = "" - @State var timeLeft = 0 - @State var editTwoFactor = false - - var body: some View { - List { - Section(header: Text("Login data")) { - TextField("Name", text: $name) - TextFieldWithCopyButton(text: "Username", textBind: self.$username) - SecureFieldWithCopyAndShowButton(text: "Password", textBind: self.$password) - Button("Generate random password") { self.generatePasswordAlert = true } - } - - Section(header: Text("URIs")) { - ForEach(self.uris.indices, id: \.self) { index in - HStack { - TextFieldWithCopyButton(text: "URI " + String(index + 1), textBind: self.$uris[index]) - } - } - .onDelete { index in - self.uris.remove(atOffsets: index) - } - - Button("Add") { - self.uris.append("") - } - } - - Section(header: Text("Two factor")) { - if let _ = self.twoFactorUri { - HStack { - Button(action: { - UIPasteboard.general.string = self.oneTimePassword - }, label: { - Image(systemName: "doc.on.doc") - }) - .buttonStyle(.plain) - Text(self.oneTimePassword) - Spacer() - Text(String(self.timeLeft)) - } - .onAppear { - runAuthenticatorJob() - } - .onDisappear { - stop = running - } - - Button("Edit 2FA") { - stop = running - self.editTwoFactor = true - } - - Button("Delete 2FA") { - stop = running - self.twoFactorUri = nil - } - - .alert("Incorrect 2FA configuration", isPresented: self.$twoFactorError) { - Button("OK", role: .cancel) { - self.twoFactorUri = nil - } - } - } else { - Button("Set up 2FA") { - self.editTwoFactor = true - } - } - } - - Section(header: Text("Notes")) { - TextField("Notes", text: self.$notes, axis: .vertical) - } - - Section { - ButtonWithSpinningWheel(text: "Save", task: self.saveCipher) - } - } - - .alert("Length of password (must be longer or equal to 8)", isPresented: self.$generatePasswordAlert) { - TextField("Length", value: self.$passwordLength, formatter: NumberFormatter()) - Button("Generate") { - if let password = try? generatePassword(length: self.passwordLength) { - self.password = password - } - } - - Button("Cancel", role: .cancel) {} - } - - .onAppear { - self.name = self.cipher.loginData!.name - self.username = self.cipher.loginData!.username ?? "" - self.password = self.cipher.loginData!.password ?? "" - self.uris = self.cipher.loginData!.uris ?? [] - self.notes = self.cipher.loginData!.notes ?? "" - self.twoFactorUri = self.cipher.loginData!.twoFactor - } - - .sheet(isPresented: self.$editTwoFactor, onDismiss: { - runAuthenticatorJob() - }) { - List { - Section(header: Text("Manual configuration")) { - TextField("Secret", text: self.$twoFactorSecret) - Picker("Type", selection: self.$twoFactorType) { - Text("TOTP").tag(OATHParams.OATHType.TOTP) - } - TextField("Digits", value: self.$twoFactorDigits, formatter: NumberFormatter()) - if self.twoFactorType == .TOTP { - TextField("Period", value: self.$twoFactorPeriod, formatter: NumberFormatter()) - } else { - TextField("Counter", value: self.$twoFactorCounter, formatter: NumberFormatter()) - } - - Button("Apply") { - let split = self.twoFactorSecret.components(separatedBy: " ") - if split.count > 0 { - self.twoFactorSecret = "" - split.forEach { - if $0 != "" { - self.twoFactorSecret += $0 - } - } - } - - var str = "otpauth://" + self.twoFactorType.toString() - str += "/randomlabel?secret=" + self.twoFactorSecret - str += "&algorithm=" + self.twoFactorAlgorithm.toString() - str += "&digits=" + String(self.twoFactorDigits) - - if self.twoFactorType == .TOTP { - str += "&period=" + String(self.twoFactorPeriod) - } else { - str += "&counter=" + String(self.twoFactorCounter) - } - - self.twoFactorUri = str - - self.editTwoFactor = false - } - } - } - .onAppear { - do { - if let twoFactorUri = self.twoFactorUri { - let params = try OATHParams(uri: twoFactorUri) - self.twoFactorType = params.type - self.twoFactorAlgorithm = params.algorithm - self.twoFactorSecret = base32Encode(params.secret) - self.twoFactorDigits = params.digits - self.twoFactorPeriod = params.period - self.twoFactorCounter = params.counter - } - } catch { - self.twoFactorError = true - } - } - } - } - - @State var twoFactorType: OATHParams.OATHType = .TOTP - @State var twoFactorAlgorithm: SwiftOTP.OTPAlgorithm = .sha1 - @State var twoFactorSecret = String() - @State var twoFactorDigits = 6 - @State var twoFactorPeriod = 30 - @State var twoFactorCounter = 0 - @State var twoFactorError = false - - func runAuthenticatorJob() { - Task { - do { - if let twoFactorUri = self.twoFactorUri { - let engine = try OATHParams(uri: twoFactorUri) - switch engine.type { - case .TOTP: - await engine.runTOTPCounter { oneTimePassword, timeLeft in - self.oneTimePassword = oneTimePassword - self.timeLeft = timeLeft - } - break - case .HOTP: - break - } - } - } catch { - self.twoFactorError = true - } - } - } - - func saveCipher() throws { - self.cipher.loginData!.name = self.name - self.cipher.loginData!.username = self.username.emptyStringToNil() - self.cipher.loginData!.password = self.password.emptyStringToNil() - self.cipher.loginData!.uris = self.uris - self.cipher.loginData!.notes = self.notes.emptyStringToNil() - self.cipher.loginData!.twoFactor = self.twoFactorUri - - self.save(self.cipher) - } -} - -struct CipherSecureNoteView: View { - var cipher: LibrePassCipher - var index: Int - var save: (_ cipher: LibrePassCipher) -> () - @State var title: String = String() - @State var note: String = String() - - var body: some View { - List { - TextField("Title", text: self.$title) - TextField("Note", text: self.$note, axis: .vertical) - - ButtonWithSpinningWheel(text: "Save", task: self.saveCipher) - } - - .onAppear { - self.title = self.cipher.secureNoteData!.title - self.note = self.cipher.secureNoteData!.note - } - } - - func saveCipher() throws { - self.cipher.secureNoteData!.title = title - self.cipher.secureNoteData!.note = note - self.save(self.cipher) - } -} - -struct CipherCardDataView: View { - var cipher: LibrePassCipher - var index: Int - var save: (_ cipher: LibrePassCipher) -> () - - @State var name = String() - @State var cardholderName = String() - @State var number = String() - @State var expMonth = String() - @State var expYear = String() - @State var code = String() - @State var notes = String() - - var body: some View { - List { - Section(header: Text("Card data")) { - TextField("Name", text: self.$name) - TextFieldWithCopyButton(text: "Cardholder name", textBind: self.$cardholderName) - SecureFieldWithCopyAndShowButton(text: "Card number", textBind: self.$number) - TextFieldWithCopyButton(text: "Expires in month", textBind: self.$expMonth) - TextFieldWithCopyButton(text: "Expires in year", textBind: self.$expYear) - SecureFieldWithCopyAndShowButton(text: "Security code", textBind: self.$code) - } - - Section(header: Text("Notes")) { - TextField("Note", text: self.$notes, axis: .vertical) - } - - Section { - ButtonWithSpinningWheel(text: "Save", task: self.saveCipher) - } - } - - .onAppear { - self.name = self.cipher.cardData!.name - self.cardholderName = self.cipher.cardData!.cardholderName - self.number = self.cipher.cardData!.number - - if self.cipher.cardData!.expMonth == nil { - self.expMonth = "" - } else { - self.expMonth = String(self.cipher.cardData!.expMonth!) - } - - if self.cipher.cardData!.expYear == nil { - self.expYear = "" - } else { - self.expYear = String(self.cipher.cardData!.expYear!) - } - - self.code = self.cipher.cardData!.code ?? "" - self.notes = self.cipher.cardData!.notes ?? "" - } - } - - func saveCipher() throws { - self.cipher.cardData!.name = self.name - self.cipher.cardData!.cardholderName = self.cardholderName - self.cipher.cardData!.number = self.number - self.cipher.cardData!.expMonth = Int(self.expMonth) ?? nil - self.cipher.cardData!.expYear = Int(self.expYear) ?? nil - self.cipher.cardData!.code = self.code.emptyStringToNil() - self.cipher.cardData!.notes = self.notes.emptyStringToNil() - - self.save(self.cipher) - } -} - -struct CipherButton: View { - var cipher: LibrePassCipher - - var body: some View { - switch self.cipher.type { - case .Login: - HStack { - Image(systemName: "person.crop.circle.fill") - VStack { - HStack{ - Text(self.cipher.loginData!.name) - Spacer() - } - if let username = self.cipher.loginData!.username { - HStack { - Text(username) - Spacer() - } - } - } - } - case .SecureNote: - HStack { - Image(systemName: "note.text") - Text(self.cipher.secureNoteData!.title) - Spacer() - } - case .Card: - HStack { - Image(systemName: "creditcard") - Text(self.cipher.cardData!.name) - Spacer() - } - } - } -} diff --git a/LibrePass/LibrePassLocalLogin.swift b/LibrePass/LibrePassLocalLogin.swift deleted file mode 100644 index 15c0e44..0000000 --- a/LibrePass/LibrePassLocalLogin.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// LibrePassLocalLogin.swift -// LibrePass -// -// Created by Zapomnij on 18/02/2024. -// - -import SwiftUI - -struct LibrePassLocalLogin: View { - @Binding var lClient: LibrePassClient - @Binding var loggedIn: Bool - @Binding var localLogIn: Bool - - @State private var password = String() - - @State private var showAlert = false - @State private var errorString = " " - @State private var tokenExpired = false - - var body: some View { - List { - Section(header: Text("Login")) { - SecureField("Password", text: $password) - .autocapitalization(.none) - ButtonWithSpinningWheel(text: "Unlock vault", task: self.login) - } - - Section(header: Text("WARNING! THIS WILL DELETE VAULT SAVED ON THE DISK, but can fix crashes")) { - ButtonWithSpinningWheel(text: "Clear vault", task: self.clearVault, color: Color.red) - } - } - - .alert("Token has expired. You must relogin to use LibrePass", isPresented: self.$tokenExpired) { - Button("OK", role: .cancel) { - self.localLogIn = false - } - } - } - - func clearVault() throws { - if networkMonitor.isConnected { - let credentials = try LibrePassCredentialsDatabase.load() - self.lClient = try LibrePassClient(credentials: credentials, password: self.password) - - try self.lClient.fetchCiphers() - - self.lClient.unAuth() - } else { - throw LibrePassApiErrors.WithMessage(message: "Offline clearing vault can't be done") - } - } - - func login() throws { - let credentials = try LibrePassCredentialsDatabase.load() - self.lClient = try LibrePassClient(credentials: credentials, password: self.password) - do { - try self.lClient.syncVault() - } catch ApiClientErrors.StatusCodeNot200(let statusCode, let body) { - self.lClient.unAuth() - if statusCode == 401 && body.error == "InvalidToken" { - self.lClient.logOut() - self.tokenExpired = true - return - } else { - throw ApiClientErrors.StatusCodeNot200(statusCode: statusCode, body: body) - } - } catch { - self.lClient.unAuth() - throw error - } - - self.loggedIn = true - } -} diff --git a/LibrePass/LibrePassLoginWindow.swift b/LibrePass/LibrePassLoginWindow.swift deleted file mode 100644 index 07e1433..0000000 --- a/LibrePass/LibrePassLoginWindow.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// LibrePassLoginWindow.swift -// LibrePass -// -// Created by Zapomnij on 17/02/2024. -// - -import SwiftUI - -struct LibrePassLoginWindow: View { - @Binding var lClient: LibrePassClient - - @State private var email = String() - @State private var password = String() - @State private var apiServer = "https://api.librepass.org" - - @Binding var loggedIn: Bool - @Binding var localLogIn: Bool - - var body: some View { - List { - TextField("Email", text: $email) - .autocapitalization(.none) - SecureField("Password", text: $password) - .autocapitalization(.none) - TextField("API Server", text: $apiServer) - .autocapitalization(.none) - - ButtonWithSpinningWheel(text: "Log in", task: self.login) - } - - .navigationTitle("Log in to LibrePass") - } - - func login() throws { - if self.email.isEmpty || self.password.isEmpty || self.apiServer.isEmpty { - throw LibrePassApiErrors.WithMessage(message: "Empty fields") - } - - lClient.unAuth() - lClient.replaceApiClient(apiUrl: self.apiServer) - - try lClient.login(email: self.email, password: self.password) - try self.lClient.fetchCiphers() - self.loggedIn = true - self.localLogIn = true - } -} diff --git a/LibrePass/LibrePassManagerWindow.swift b/LibrePass/LibrePassManagerWindow.swift deleted file mode 100644 index aa87adb..0000000 --- a/LibrePass/LibrePassManagerWindow.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// LibrePassManagerWindow.swift -// LibrePass -// -// Created by Zapomnij on 18/02/2024. -// - -import SwiftUI - -struct LibrePassManagerWindow: View { - @State private var errorString: String = " " - @State private var showAlert = false - @State private var new = false - - @Binding var lClient: LibrePassClient - - @Binding var loggedIn: Bool - @Binding var locallyLoggedIn: Bool - - @State var refreshIndicator: Bool = false - @State var deletionIndicator: Bool = false - - @State var toDelete: IndexSet = [] - - @State var accountSettings: Bool = false - - var body: some View { - NavigationView { - List { - ForEach(self.lClient.vault.vault, id: \.self.id) { cipher in - let index = lClient.vault.vault.firstIndex(where: { vaultCipher in vaultCipher.id == cipher.id })! - - NavigationLink(destination: CipherView(lClient: $lClient, cipher: cipher, index: index)) { - HStack { - CipherButton(cipher: cipher) - - Spacer() - } - } - } - .onDelete { indexSet in - self.toDelete = indexSet - self.deletionIndicator = true - } - } - - .navigationTitle("Vault") - .toolbar { - HStack { - SpinningWheel(isPresented: self.$deletionIndicator, task: self.deleteCiphers) - SpinningWheel(isPresented: self.$refreshIndicator, task: self.syncVault) - - if !self.refreshIndicator && !self.deletionIndicator { - Button(action: { - self.refreshIndicator = true - }) { - Image(systemName: "arrow.clockwise") - } - - Menu { - Button(action: { - self.accountSettings = true - }) { - Image(systemName: "gearshape") - Text("Account settings") - } - - Button(action: { - self.lClient.unAuth() - self.loggedIn = false - }) { - Image(systemName: "lock") - Text("Lock vault") - } - - Button(action: { - self.new.toggle() - }) { - Image(systemName: "plus") - Text("New cipher") - } - } label: { - Image(systemName: ("ellipsis.circle")) - } - } - } - } - } - - .alert(self.errorString, isPresented: self.$showAlert) { - Button("OK", role: .cancel) { - lClient.unAuth() - self.loggedIn = false - } - } - - .alert("Select cipher type", isPresented: self.$new) { - Button("Login data") { - self.newCipher(type: .Login) - } - - Button("Secure note") { - self.newCipher(type: .SecureNote) - } - - Button("Card data") { - self.newCipher(type: .Card) - } - - Button("Cancel", role: .cancel) {} - } - - .sheet(isPresented: self.$accountSettings) { - LibrePassAccountSettings(lClient: self.$lClient, locallyLoggedIn: self.$locallyLoggedIn, loggedIn: self.$loggedIn) - } - } - - func syncVault() throws { - if networkMonitor.isConnected { - try self.lClient.syncVault() - } - } - - func deleteCiphers() throws { - Task { - for index in self.toDelete { - try self.lClient.delete(id: self.lClient.vault.vault[index].id) - } - } - } - - func newCipher(type: LibrePassCipher.CipherType) { - Task { - let cipher = LibrePassCipher(id: lClient.generateId(), owner: lClient.credentialsDatabase!.userId, type: type) - - do { - try self.lClient.put(cipher: cipher) - try self.syncVault() - } catch ApiClientErrors.StatusCodeNot200(let statusCode, let body){ - self.errorString = String(statusCode) + ": " + body.error - self.showAlert = true - } catch { - self.errorString = error.localizedDescription - self.showAlert = true - } - } - } -} diff --git a/LibrePass/LibrePassRegistrationWindow.swift b/LibrePass/LibrePassRegistrationWindow.swift deleted file mode 100644 index 68be982..0000000 --- a/LibrePass/LibrePassRegistrationWindow.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// LibrePassRegistrationWindow.swift -// LibrePass -// -// Created by Zapomnij on 24/02/2024. -// - -import SwiftUI - -struct LibrePassRegistrationWindow: View { - @Environment(\.presentationMode) var presentationMode - - @Binding var lClient: LibrePassClient - - @State var apiServer = "https://api.librepass.org" - @State var email = String() - @State var password = String() - @State var confirmPassword = String() - @State var passwordHint = String() - @State var showAlert = false - @State var registered = false - - @State var errorString = " " - - var body: some View { - List { - TextField("Email", text: $email) - .autocapitalization(.none) - SecureField("Password", text: $password) - .autocapitalization(.none) - SecureField("Confirm password", text: $confirmPassword) - .autocapitalization(.none) - TextField("Password hint", text: $passwordHint) - TextField("API server", text: $apiServer) - .autocapitalization(.none) - - Button("Register") { - if self.confirmPassword != self.password { - self.confirmPassword = "" - - self.errorString = "Password doesn't match" - return - } - - do { - self.lClient.replaceApiClient(apiUrl: apiServer) - try self.lClient.register(email: self.email, password: self.password, passwordHint: self.passwordHint) - - self.errorString = "Check your mailbox, verify email and log in" - self.registered = true - self.showAlert = true - } catch { - self.errorString = error.localizedDescription - self.showAlert = true - } - } - } - - .navigationTitle("Registration") - - .alert(self.errorString, isPresented: self.$showAlert) { - Button("OK", role: .cancel) { - if self.registered { - self.registered = false - self.presentationMode.wrappedValue.dismiss() - } - } - } - } -} - diff --git a/LibrePass/NetworkMonitor.swift b/LibrePass/NetworkMonitor.swift deleted file mode 100644 index 7dc37da..0000000 --- a/LibrePass/NetworkMonitor.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// NetworkMonitor.swift -// LibrePass -// -// Created by Zapomnij on 26/02/2024. -// - -import Foundation -import Network - -class NetworkMonitor: ObservableObject { - private let monitor = NWPathMonitor() - private let dispatchQueue = DispatchQueue(label: "Monitor") - - var isConnected = true - - init() { - self.monitor.pathUpdateHandler = { path in - self.isConnected = path.status != .unsatisfied - } - - self.monitor.start(queue: self.dispatchQueue) - } -} diff --git a/LibrePass/Utils.swift b/LibrePass/Utils.swift deleted file mode 100644 index a76a25b..0000000 --- a/LibrePass/Utils.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// Utils.swift -// LibrePass -// -// Created by Zapomnij on 25/02/2024. -// - -import SwiftUI - -struct SpinningWheel: View { - @Binding var isPresented: Bool - var task: () throws -> () - - @State var showAlert: Bool = false - @State var errorString = String() - - var body: some View { - if self.isPresented { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: Color.secondary)) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now()) { - do { - try self.task() - self.isPresented = false - } catch LibrePassApiErrors.WithMessage(let message) { - self.errorString = message - self.showAlert = true - } catch ApiClientErrors.StatusCodeNot200(let statusCode, let body) { - self.errorString = String(statusCode) + ": " + body.error - self.showAlert = true - } catch { - self.errorString = error.localizedDescription - self.showAlert = true - } - } - } - .alert(self.errorString, isPresented: self.$showAlert) { - Button("OK", role: .cancel) { - self.isPresented = false - } - } - } - } -} - -struct ButtonWithSpinningWheel: View { - var text: String - var task: () throws -> () - var color: Color? - - @State var isPresented = false - - var body: some View { - HStack { - if let color = self.color { - Button(text) { self.isPresented = true } - .foregroundStyle(color) - } else { - Button(text) { self.isPresented = true } - } - Spacer() - SpinningWheel(isPresented: self.$isPresented, task: self.task) - } - } -} - -struct TextFieldWithCopyButton: View { - var text: String - @Binding var textBind: String - - var body: some View { - HStack { - TextField(self.text, text: self.$textBind) - .autocapitalization(.none) - Spacer() - Button(action: { - UIPasteboard.general.string = self.textBind - }) { - Image(systemName: "doc.on.doc") - } - .buttonStyle(.plain) - } - } -} - -struct SecureFieldWithCopyAndShowButton: View { - var text: String - @Binding var textBind: String - - @State var showPassword = false - - var body: some View { - HStack { - if self.showPassword { - TextField(self.text, text: self.$textBind) - .autocapitalization(.none) - } else { - SecureField(self.text, text: self.$textBind) - .autocapitalization(.none) - } - - Spacer() - - Button(action: { - self.showPassword.toggle() - }) { - if self.showPassword { - Image(systemName: "eye") - } else { - Image(systemName: "eye.fill") - } - } - .buttonStyle(.plain) - - Button(action: { - UIPasteboard.general.string = self.textBind - }) { - Image(systemName: "doc.on.doc") - } - .buttonStyle(.plain) - } - } -} - - -extension String { - func emptyStringToNil() -> Self? { - if self.isEmpty { - return nil - } - - return self - } -} diff --git a/Makefile b/Makefile deleted file mode 100644 index 6ecab89..0000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -DESTDIR = Build/Release-iphoneos -XCODEBUILD ?= xcodebuild - -$(DESTDIR)/LibrePass.ipa: $(DESTDIR)/LibrePass.app - cd $(DESTDIR) && \ - mkdir -p Payload && \ - cp -pr LibrePass.app Payload && \ - zip -r LibrePass.ipa Payload - -$(DESTDIR)/LibrePass.app: - xcodebuild -workspace LibrePass.xcworkspace -scheme LibrePass -sdk iphoneos -configuration Release CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO SYMROOT=$(PWD)/Build - -clean: - rm -rf Build diff --git a/Podfile b/Podfile deleted file mode 100644 index 7522475..0000000 --- a/Podfile +++ /dev/null @@ -1,11 +0,0 @@ -# Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' - -target 'LibrePass' do - # Comment the next line if you don't want to use dynamic frameworks - use_frameworks! - - # Pods for LibrePass - pod 'SignalArgon2' - pod 'SwiftOTP' -end diff --git a/Podfile.lock b/Podfile.lock deleted file mode 100644 index cbeb314..0000000 --- a/Podfile.lock +++ /dev/null @@ -1,20 +0,0 @@ -PODS: - - SignalArgon2 (1.3.2) - - SwiftOTP (3.0.0) - -DEPENDENCIES: - - SignalArgon2 - - SwiftOTP - -SPEC REPOS: - trunk: - - SignalArgon2 - - SwiftOTP - -SPEC CHECKSUMS: - SignalArgon2: 1c24183835ca861e6af06631c18b1671cdf35571 - SwiftOTP: ec3848f12a4541f29d96f6a0fe63dcbb8448d96f - -PODFILE CHECKSUM: 1da7a2cafcf08979a18d3ef0b1ddb528be0e2c71 - -COCOAPODS: 1.15.2 diff --git a/README.md b/README.md deleted file mode 100644 index 9ae4225..0000000 --- a/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# LibrePass-iOS - -LibrePass client for iOS. Written in SwiftUI. - -Vault screenshot - -### Update issues -If app crashes after update, you probably need to clear vault unfortunately. - -### Used libraries: -- SwiftUI -- Apple CryptoKit -- SignalArgon2 -- SwiftOTP diff --git a/readme/VaultScreenshot.png b/readme/VaultScreenshot.png deleted file mode 100644 index 36b4951..0000000 Binary files a/readme/VaultScreenshot.png and /dev/null differ