Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions FlowCrypt/Controllers/Setup/SetupInitialViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,20 @@ extension SetupInitialViewController {
}
}

private func getIdToken() async throws -> String {
let googleService = GoogleUserService(
currentUserEmail: user.email,
appDelegateGoogleSessionContainer: nil
)
Comment on lines +161 to +164
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably could/should dependency-inject this through AppContext. I'll file another issue.


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)
Expand All @@ -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
})
}
Expand Down
4 changes: 2 additions & 2 deletions FlowCrypt/Extensions/BundleExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -20,5 +20,5 @@ extension Bundle {
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return .debug }
return FlowCryptBundleType(rawValue: bundleIdentifier) ?? .debug
}

}
6 changes: 2 additions & 4 deletions FlowCrypt/Extensions/CommandLineExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

}
2 changes: 1 addition & 1 deletion FlowCrypt/Extensions/String+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
2 changes: 1 addition & 1 deletion FlowCrypt/Extensions/UIViewController+Spinner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ extension UIViewController {
currentProgressHUD.mode = .customView
currentProgressHUD.label.text = label
}

@MainActor
func showIndeterminateHUD(with title: String) {
self.currentProgressHUD.mode = .indeterminate
Expand Down
2 changes: 1 addition & 1 deletion FlowCrypt/Functionality/DataManager/DataService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion FlowCrypt/Functionality/DataManager/SessionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
90 changes: 81 additions & 9 deletions FlowCrypt/Functionality/Services/GoogleUserService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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? {
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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 ""
}
}
Expand All @@ -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)",
Expand All @@ -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)
Expand All @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ final class AttesterApi: AttesterApiType {
}
return "https://flowcrypt.com/attester" // live
}

private func pubUrl(email: String) -> String {
let normalizedEmail = email
.lowercased()
Expand Down
5 changes: 3 additions & 2 deletions FlowCrypt/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -64,7 +64,7 @@ SPEC CHECKSUMS:
PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086
PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20
PINRemoteImage: f1295b29f8c5e640e25335a1b2bd9d805171bd01
SwiftFormat: 7dd2b33a0a3d61095b61c911b6d89ff962ae695c
SwiftFormat: 16b41f3229f5e7edb130ac4cd631cceed7af7d5e
SwiftLint: 06ac37e4d38c7068e0935bb30cda95f093bec761
SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6
Texture: 2e8ab2519452515f7f5a520f5a8f7e0a413abfa3
Expand Down