diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0819df7bf..9f5ced7f3 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -87,8 +87,8 @@ "repositoryURL": "https://github.com/realm/realm-cocoa", "state": { "branch": null, - "revision": "bdbbd57f411a0f4e72b359113dbc6d23fdf96680", - "version": "10.20.0" + "revision": "f483fa0a52f6d49897d133a827510a35e21183c1", + "version": "10.20.1" } }, { diff --git a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift index af2f73314..a03955b03 100644 --- a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift @@ -157,10 +157,20 @@ extension SetupInitialViewController { } } + private func getIdToken() async throws -> String { + let googleService = GoogleUserService( + currentUserEmail: user.email, + appDelegateGoogleSessionContainer: nil + ) + + return try await googleService.getCachedOrRefreshedIdToken() + } + private func fetchKeysFromEKM() { Task { do { - let result = try await emailKeyManagerApi.getPrivateKeys(currentUserEmail: user.email) + let idToken = try await getIdToken() + let result = try await emailKeyManagerApi.getPrivateKeys(idToken: idToken) switch result { case .success(keys: let keys): proceedToSetupWithEKMKeys(keys: keys) @@ -183,7 +193,7 @@ extension SetupInitialViewController { if case .noPrivateKeysUrlString = error as? EmailKeyManagerApiError { return } - showAlert(message: error.localizedDescription, onOk: { [weak self] in + showAlert(message: error.errorMessage, onOk: { [weak self] in self?.state = .decidingIfEKMshouldBeUsed }) } diff --git a/FlowCrypt/Extensions/BundleExtensions.swift b/FlowCrypt/Extensions/BundleExtensions.swift index 0a249741f..d54aabadf 100644 --- a/FlowCrypt/Extensions/BundleExtensions.swift +++ b/FlowCrypt/Extensions/BundleExtensions.swift @@ -5,7 +5,7 @@ // Created by Tom on 03.12.2021 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - + import Foundation enum FlowCryptBundleType: String { @@ -20,5 +20,5 @@ extension Bundle { guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return .debug } return FlowCryptBundleType(rawValue: bundleIdentifier) ?? .debug } - + } diff --git a/FlowCrypt/Extensions/CommandLineExtensions.swift b/FlowCrypt/Extensions/CommandLineExtensions.swift index 3bc85d4b1..8c0bcbbde 100644 --- a/FlowCrypt/Extensions/CommandLineExtensions.swift +++ b/FlowCrypt/Extensions/CommandLineExtensions.swift @@ -6,15 +6,13 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - - import Foundation extension CommandLine { - + static func isDebugBundleWithArgument(_ argument: String) -> Bool { guard Bundle.flowCryptBundleType == .debug else { return false } return CommandLine.arguments.contains(argument) } - + } diff --git a/FlowCrypt/Extensions/String+Extension.swift b/FlowCrypt/Extensions/String+Extension.swift index 8966f1f52..859066bd3 100644 --- a/FlowCrypt/Extensions/String+Extension.swift +++ b/FlowCrypt/Extensions/String+Extension.swift @@ -14,7 +14,7 @@ extension String { let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailFormat) return emailPredicate.evaluate(with: self) } - + func capitalizingFirstLetter() -> String { prefix(1).uppercased() + self.lowercased().dropFirst() } diff --git a/FlowCrypt/Extensions/UIViewController+Spinner.swift b/FlowCrypt/Extensions/UIViewController+Spinner.swift index 653a0d68c..0f5764910 100644 --- a/FlowCrypt/Extensions/UIViewController+Spinner.swift +++ b/FlowCrypt/Extensions/UIViewController+Spinner.swift @@ -72,7 +72,7 @@ extension UIViewController { currentProgressHUD.mode = .customView currentProgressHUD.label.text = label } - + @MainActor func showIndeterminateHUD(with title: String) { self.currentProgressHUD.mode = .indeterminate diff --git a/FlowCrypt/Functionality/DataManager/DataService.swift b/FlowCrypt/Functionality/DataManager/DataService.swift index 80403692d..485ae8ee7 100644 --- a/FlowCrypt/Functionality/DataManager/DataService.swift +++ b/FlowCrypt/Functionality/DataManager/DataService.swift @@ -102,7 +102,7 @@ extension DataService: DataServiceType { return GoogleUserService( currentUserEmail: currentUser?.email, appDelegateGoogleSessionContainer: nil // needed only when signing in/out - ).userToken + ).accessToken default: return nil } diff --git a/FlowCrypt/Functionality/DataManager/SessionService.swift b/FlowCrypt/Functionality/DataManager/SessionService.swift index e081c0398..8554f4aea 100644 --- a/FlowCrypt/Functionality/DataManager/SessionService.swift +++ b/FlowCrypt/Functionality/DataManager/SessionService.swift @@ -102,7 +102,7 @@ extension SessionService: SessionServiceType { } } let users = encryptedStorage.getAllUsers() - if !users.contains(where: { $0.isActive }), let user = users.first(where: { encryptedStorage.doesAnyKeypairExist(for: $0.email ) }) { + if !users.contains(where: { $0.isActive }), let user = users.first(where: { encryptedStorage.doesAnyKeypairExist(for: $0.email) }) { try switchActiveSession(for: user) } } diff --git a/FlowCrypt/Functionality/Services/GoogleUserService.swift b/FlowCrypt/Functionality/Services/GoogleUserService.swift index 72a93e937..32344d0b6 100644 --- a/FlowCrypt/Functionality/Services/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/GoogleUserService.swift @@ -42,6 +42,29 @@ struct GoogleUser: Codable { let picture: URL? } +struct IdToken: Codable { + let exp: Int +} + +extension IdToken { + var isExpired: Bool { + Double(exp) < Date().timeIntervalSince1970 + } +} + +enum IdTokenError: Error, CustomStringConvertible { + case missingToken, invalidJWTFormat, invalidBase64EncodedData + + var description: String { + switch self { + case .missingToken: + return "id_token_missing_error_description".localized + case .invalidJWTFormat, .invalidBase64EncodedData: + return "id_token_invalid_error_description".localized + } + } +} + protocol GoogleUserServiceType { var authorization: GTMAppAuthFetcherAuthorization? { get } func renewSession() async throws @@ -73,16 +96,16 @@ final class GoogleUserService: NSObject, GoogleUserServiceType { private lazy var logger = Logger.nested(in: Self.self, with: .userAppStart) - var userToken: String? { - authorization?.authState - .lastTokenResponse? - .accessToken + private var tokenResponse: OIDTokenResponse? { + authorization?.authState.lastTokenResponse + } + + private var idToken: String? { + tokenResponse?.idToken } - var idToken: String? { - authorization?.authState - .lastTokenResponse? - .idToken + var accessToken: String? { + tokenResponse?.accessToken } var authorization: GTMAppAuthFetcherAuthorization? { @@ -110,7 +133,7 @@ extension GoogleUserService: UserServiceType { let error = self.parseSignInError(authError) return continuation.resume(throwing: error) } else { - let error = AppErr.unexpected("Shouldn't happen because received non nil error and non nil authState") + let error = AppErr.unexpected("Shouldn't happen because received nil error and nil authState") return continuation.resume(throwing: error) } } @@ -229,6 +252,55 @@ extension GoogleUserService { } } +// MARK: - Tokens +extension GoogleUserService { + func getCachedOrRefreshedIdToken() async throws -> String { + guard let idToken = idToken else { throw(IdTokenError.missingToken) } + + let decodedToken = try decode(idToken: idToken) + + guard !decodedToken.isExpired else { + let (_, updatedToken) = try await performTokenRefresh() + return updatedToken + } + + return idToken + } + + private func decode(idToken: String) throws -> IdToken { + let components = idToken.components(separatedBy: ".") + + guard components.count == 3 else { throw(IdTokenError.invalidJWTFormat) } + + var decodedString = components[1] + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + + while decodedString.utf16.count % 4 != 0 { + decodedString += "=" + } + + guard let decodedData = Data(base64Encoded: decodedString) + else { throw(IdTokenError.invalidBase64EncodedData) } + + return try JSONDecoder().decode(IdToken.self, from: decodedData) + } + + private func performTokenRefresh() async throws -> (accessToken: String, idToken: String) { + return try await withCheckedThrowingContinuation { continuation in + authorization?.authState.setNeedsTokenRefresh() + authorization?.authState.performAction { accessToken, idToken, error in + guard let accessToken = accessToken, let idToken = idToken else { + let tokenError = error ?? AppErr.unexpected("Shouldn't happen because received nil error and nil token") + return continuation.resume(throwing: tokenError) + } + let result = (accessToken, idToken) + return continuation.resume(with: .success(result)) + } + } + } +} + // MARK: - OIDAuthStateChangeDelegate extension GoogleUserService: OIDAuthStateChangeDelegate { func didChange(_ state: OIDAuthState) { diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift index b08595744..851eb8fbe 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift @@ -108,7 +108,9 @@ final class KeyService: KeyServiceType { logger.logDebug("findKeyByUserEmail: found key \(primaryEmailMatch.1.primaryFingerprint) by primary email match") return primaryEmailMatch.0 } - if let alternativeEmailMatch = keys.first(where: { $0.1.pgpUserEmails.map { $0.lowercased() }.contains(email.lowercased()) == true }) { + if let alternativeEmailMatch = keys.first(where: { + $0.1.pgpUserEmails.map { $0.lowercased() }.contains(email.lowercased()) == true + }) { logger.logDebug("findKeyByUserEmail: found key \(alternativeEmailMatch.1.primaryFingerprint) by alternative email match") return alternativeEmailMatch.0 } diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift index fe7548262..d5e62a726 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift @@ -89,8 +89,10 @@ final class PassPhraseService: PassPhraseServiceType { case .persistent: try encryptedStorage.save(passPhrase: passPhrase) case .memory: - if encryptedStorage.getPassPhrases().contains(where: { $0.primaryFingerprintOfAssociatedKey == passPhrase.primaryFingerprintOfAssociatedKey }) { - logger.logInfo("\(StorageMethod.persistent): removing pass phrase from for key \(passPhrase.primaryFingerprintOfAssociatedKey)") + let storedPassPhrases = encryptedStorage.getPassPhrases() + let fingerprint = passPhrase.primaryFingerprintOfAssociatedKey + if storedPassPhrases.contains(where: { $0.primaryFingerprintOfAssociatedKey == fingerprint }) { + logger.logInfo("\(StorageMethod.persistent): removing pass phrase for key \(fingerprint)") try encryptedStorage.remove(passPhrase: passPhrase) } try inMemoryStorage.save(passPhrase: passPhrase) diff --git a/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift b/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift index 95734a5f8..fc6709c2f 100644 --- a/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift +++ b/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift @@ -9,11 +9,10 @@ import Foundation protocol EmailKeyManagerApiType { - func getPrivateKeys(currentUserEmail: String) async throws -> EmailKeyManagerApiResult + func getPrivateKeys(idToken: String) async throws -> EmailKeyManagerApiResult } enum EmailKeyManagerApiError: Error { - case noGoogleIdToken case noPrivateKeysUrlString } @@ -26,7 +25,6 @@ enum EmailKeyManagerApiResult { extension EmailKeyManagerApiError: LocalizedError { var errorDescription: String? { switch self { - case .noGoogleIdToken: return "emai_keymanager_api_no_google_id_token_error_description".localized case .noPrivateKeysUrlString: return "" } } @@ -51,18 +49,8 @@ actor EmailKeyManagerApi: EmailKeyManagerApiType { self.core = core } - func getPrivateKeys(currentUserEmail: String) async throws -> EmailKeyManagerApiResult { - guard let urlString = getPrivateKeysUrlString() else { - throw EmailKeyManagerApiError.noPrivateKeysUrlString - } - - guard let idToken = GoogleUserService( - currentUserEmail: currentUserEmail, - appDelegateGoogleSessionContainer: nil // only needed when signing in/out - ).idToken else { - throw EmailKeyManagerApiError.noGoogleIdToken - } - + func getPrivateKeys(idToken: String) async throws -> EmailKeyManagerApiResult { + let urlString = try getPrivateKeysUrlString() let headers = [ URLHeader( value: "Bearer \(idToken)", @@ -83,7 +71,7 @@ actor EmailKeyManagerApi: EmailKeyManagerApiType { } let privateKeysArmored = decryptedPrivateKeysResponse.privateKeys - .map { $0.decryptedPrivateKey } + .map(\.decryptedPrivateKey) .joined(separator: "\n") .data() let parsedPrivateKeys = try await core.parseKeys(armoredOrBinary: privateKeysArmored) @@ -97,9 +85,9 @@ actor EmailKeyManagerApi: EmailKeyManagerApiType { return .success(keys: parsedPrivateKeys.keyDetails) } - private func getPrivateKeysUrlString() -> String? { + private func getPrivateKeysUrlString() throws -> String { guard let keyManagerUrlString = clientConfiguration.keyManagerUrlString else { - return nil + throw EmailKeyManagerApiError.noPrivateKeysUrlString } return "\(keyManagerUrlString)v1/keys/private" } diff --git a/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift b/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift index 374fa5f60..275c8620e 100644 --- a/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift +++ b/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift @@ -37,7 +37,7 @@ final class AttesterApi: AttesterApiType { } return "https://flowcrypt.com/attester" // live } - + private func pubUrl(email: String) -> String { let normalizedEmail = email .lowercased() diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 4d4e6f868..3949bcd71 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -248,8 +248,9 @@ "organisational_rules_ekm_keys_are_not_decrypted_error" = "Received private keys are not fully decrypted. Please try login flow again"; "organisational_wrong_email_error" = "Not a valid email %@"; -// Email key manager api error -"emai_keymanager_api_no_google_id_token_error_description" = "There is no Google ID token were found while getting client configuration"; +// Google id token errors +"id_token_missing_error_description" = "There is no Google ID token was found while getting client configuration"; +"id_token_invalid_error_description" = "There is no valid Google ID token was found while getting client configuration"; // Gmail Service errors "gmail_service_failed_to_parse_data_error_message" = "Failed to parse Gmail data"; diff --git a/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift b/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift index 493b6b503..1e071767c 100644 --- a/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift +++ b/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift @@ -46,7 +46,7 @@ class GmailServiceTest: XCTestCase { class GoogleUserServiceMock: GoogleUserServiceType { var authorization: GTMAppAuthFetcherAuthorization? func renewSession() async throws { - await Task.sleep(1_000_000_000) + try await Task.sleep(nanoseconds: 1_000_000_000) } } diff --git a/Podfile.lock b/Podfile.lock index 95812844b..549bf8ae0 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -15,7 +15,7 @@ PODS: - PINRemoteImage/PINCache (3.0.3): - PINCache (~> 3.0.3) - PINRemoteImage/Core - - SwiftFormat/CLI (0.48.18) + - SwiftFormat/CLI (0.49.1) - SwiftLint (0.45.1) - SwiftyRSA (1.7.0): - SwiftyRSA/ObjC (= 1.7.0) @@ -64,7 +64,7 @@ SPEC CHECKSUMS: PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086 PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20 PINRemoteImage: f1295b29f8c5e640e25335a1b2bd9d805171bd01 - SwiftFormat: 7dd2b33a0a3d61095b61c911b6d89ff962ae695c + SwiftFormat: 16b41f3229f5e7edb130ac4cd631cceed7af7d5e SwiftLint: 06ac37e4d38c7068e0935bb30cda95f093bec761 SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6 Texture: 2e8ab2519452515f7f5a520f5a8f7e0a413abfa3