diff --git a/GigyaSwift/Global/PersistenceService.swift b/GigyaSwift/Global/PersistenceService.swift index 3844dec..08abba5 100644 --- a/GigyaSwift/Global/PersistenceService.swift +++ b/GigyaSwift/Global/PersistenceService.swift @@ -11,6 +11,15 @@ import Foundation public final class PersistenceService { var isStartSdk: Bool = false var isInitSdk: Bool = false + let config: GigyaConfig? + + private var apiKey: String { + return config?.apiKey ?? "zxyt" + } + + init(config: GigyaConfig?) { + self.config = config + } // MARK: - UserDefault @@ -64,7 +73,7 @@ public final class PersistenceService { public var webAuthnlist: [GigyaWebAuthnCredential] { get { - if let data = UserDefaults.standard.object(forKey: InternalConfig.Storage.webAuthn) as? Data { + if let data = GigyaKeyChainService.read(key: InternalConfig.Storage.webAuthnKey(apiKey: apiKey)) { do { return try PropertyListDecoder().decode([GigyaWebAuthnCredential].self, from: data) } catch { @@ -102,10 +111,71 @@ public final class PersistenceService { func addWebAuthnKey(model: GigyaWebAuthnCredential) { var list = webAuthnlist list.append(model) - UserDefaults.standard.set(try? PropertyListEncoder().encode(list), forKey: InternalConfig.Storage.webAuthn) + if let data = try? PropertyListEncoder().encode(list) { + GigyaKeyChainService.save(key: InternalConfig.Storage.webAuthnKey(apiKey: apiKey), valueData: data) + } } internal func removeAllWebAuthnKeys() { - UserDefaults.standard.removeObject(forKey: InternalConfig.Storage.webAuthn) + GigyaKeyChainService.delete(key: InternalConfig.Storage.webAuthnKey(apiKey: apiKey)) + } +} + +class GigyaKeyChainService { + static func save(key: String, valueData: Data) { + var query = getQuery() + query[String(kSecAttrAccount)] = key + var attributes = getQuery() + attributes[String(kSecAttrAccount)] = key + attributes[String(kSecValueData)] = valueData + attributes[String(kSecAttrAccessible)] = kSecAttrAccessibleAfterFirstUnlock + let status = SecItemCopyMatching(query as CFDictionary, nil) + if status == noErr { + let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) + if updateStatus != noErr { + SecItemDelete(query as CFDictionary) + let addStatus = SecItemAdd(attributes as CFDictionary, nil) + if addStatus != noErr { + GigyaLogger.log(with: self, message: "Failed to store value for key = '\(addStatus)'") + } + } + } else { + SecItemDelete(query as CFDictionary) + let status = SecItemAdd(attributes as CFDictionary, nil) + if status != noErr { + GigyaLogger.log(with: self, message: "Failed to add value for key '\(key)'") + } + } + } + + static func read(key: String) -> Data? { + var query = getQuery() + query[String(kSecMatchLimit)] = kSecMatchLimitOne + query[String(kSecReturnData)] = kCFBooleanTrue + query[String(kSecAttrAccount)] = key + var dataTypeRef: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) + if status == noErr, let data = dataTypeRef as? Data { + return data + } else { + return nil + } + } + + static func delete(key: String) { + var query = getQuery() + query[String(kSecAttrAccount)] = key + query[String(kSecAttrSynchronizable)] = kSecAttrSynchronizableAny + SecItemDelete(query as CFDictionary) + } + + private class func getQuery() -> [String: Any] { + let bundleId = Bundle.main.bundleIdentifier ?? "" + var query: [String : Any] = [ + String(kSecClass): String(kSecClassGenericPassword), + String(kSecAttrService): bundleId + ] + query[String(kSecAttrSynchronizable)] = kCFBooleanTrue + return query } } diff --git a/GigyaSwift/Global/Utils/GigyaIOCContainer.swift b/GigyaSwift/Global/Utils/GigyaIOCContainer.swift index 2c1aad5..1cac213 100644 --- a/GigyaSwift/Global/Utils/GigyaIOCContainer.swift +++ b/GigyaSwift/Global/Utils/GigyaIOCContainer.swift @@ -161,8 +161,9 @@ final class GigyaIOCContainer: GigyaContainerProtocol { return AccountService() } - container.register(service: PersistenceService.self, isSingleton: true) { _ in - return PersistenceService() + container.register(service: PersistenceService.self, isSingleton: true) { resolver in + let config = resolver.resolve(GigyaConfig.self) + return PersistenceService(config: config) } container.register(service: InterruptionResolverFactoryProtocol.self) { _ in diff --git a/GigyaSwift/Global/WebAuthn/WebAuthnDeviceIntegration.swift b/GigyaSwift/Global/WebAuthn/WebAuthnDeviceIntegration.swift index 433cf3c..17a1fcf 100644 --- a/GigyaSwift/Global/WebAuthn/WebAuthnDeviceIntegration.swift +++ b/GigyaSwift/Global/WebAuthn/WebAuthnDeviceIntegration.swift @@ -113,8 +113,37 @@ class WebAuthnDeviceIntegration: NSObject { authController.presentationContextProvider = self authController.performRequests() } - - + + @available(iOS 16.0, *) + func loginWithAvailableCredentials(viewController: UIViewController, options: WebAuthnGetOptionsResponseModel, allowedKeys: [GigyaWebAuthnCredential], handler: @escaping WebAuthnIntegrationHandler) { + self.vc = viewController + self.handler = handler + + let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: options.options.rpId) + + let challenge = options.options.challenge.decodeBase64Url()! + + let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge) + + let publicKeys = allowedKeys.filter { $0.type == .platform } + .map { + ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: Data(base64Encoded: $0.key) ?? Data()) + } + + assertionRequest.allowedCredentials = publicKeys + + if let userVerification = options.options.userVerification { + assertionRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.init(rawValue: userVerification) + } + + let authorizationRequests: [ASAuthorizationRequest] = [assertionRequest] + + let authController = ASAuthorizationController(authorizationRequests: authorizationRequests ) + authController.delegate = self + authController.presentationContextProvider = self + authController.performRequests(options: .preferImmediatelyAvailableCredentials) + } + deinit { GigyaLogger.log(with: self, message: "deinit") } diff --git a/GigyaSwift/Global/WebAuthn/WebAuthnService.swift b/GigyaSwift/Global/WebAuthn/WebAuthnService.swift index b562f85..15c0352 100644 --- a/GigyaSwift/Global/WebAuthn/WebAuthnService.swift +++ b/GigyaSwift/Global/WebAuthn/WebAuthnService.swift @@ -206,7 +206,79 @@ public class WebAuthnService { return .failure(LoginApiError(error: error)) } } - + + @available(iOS 16.0.0, *) + public func loginWithAvailableCredentials(viewController: UIViewController, params: [String: Any] = [:]) async -> GigyaLoginResult { + if isActiveContinuation { + return .failure(.init(error: .providerError(data: "cancelled"))) + } + + let allowedKeys = persistenceService.webAuthnlist + if allowedKeys.isEmpty { + return .failure(.init(error: .providerError(data: "cancelled"))) + } + + isActiveContinuation.toggle() + oauthService.params = params + + let assertionOptions = await getAssertionOptions() + + switch assertionOptions { + case .success(let options): + return await withCheckedContinuation() { continuation in + webAuthnDeviceIntegration.loginWithAvailableCredentials(viewController: viewController, options: options, allowedKeys: allowedKeys) { [weak self] result in + guard let self = self else { + return + } + switch result { + case .login(let token): + let attestation: [String: Any] = self.attestationUtils.makeLoginData(object: token) + + Task { + let result = await self.verifyAssertion(params: ["authenticatorAssertion": attestation, "token": options.token]) + switch result { + case .success(data: let data): + let user: GigyaLoginResult = await self.oauthService.authorize(token: data["idToken"]!.value as! String) // idToken for login + continuation.resume(returning: user) + self.isActiveContinuation.toggle() + case .failure(let error): + continuation.resume(returning: .failure(LoginApiError(error: error))) + self.isActiveContinuation.toggle() + } + } + case .securityLogin(let token): + let attestation: [String: Any] = self.attestationUtils.makeSecurityLoginData(object: token) + + Task { + let result = await self.verifyAssertion(params: ["authenticatorAssertion": attestation, "token": options.token]) + switch result { + case .success(data: let data): + let user: GigyaLoginResult = await self.oauthService.authorize(token: data["idToken"]!.value as! String) // idToken for login + continuation.resume(returning: user) + self.isActiveContinuation.toggle() + case .failure(let error): + continuation.resume(returning: .failure(LoginApiError(error: error))) + self.isActiveContinuation.toggle() + } + } + case .canceled: + continuation + .resume(returning: .failure(LoginApiError(error: NetworkError.providerError(data: "cancelled")))) + self.isActiveContinuation.toggle() + default: + let error = GigyaResponseModel(statusCode: .unknown, errorCode: 400301, callId: "", errorMessage: "Operation failed", sessionInfo: nil) + continuation + .resume(returning: .failure(LoginApiError(error: NetworkError.gigyaError(data: error)))) + self.isActiveContinuation.toggle() + } + } + } + case .failure(let error): + self.isActiveContinuation.toggle() + return .failure(LoginApiError(error: error)) + } + } + @available(iOS 16.0.0, *) private func getAssertionOptions() async -> GigyaApiResult { return await withCheckedContinuation({ @@ -284,6 +356,7 @@ public class WebAuthnService { self.persistenceService.removeAllWebAuthnKeys() case .failure(_): continuation.resume(returning: false) + return } } diff --git a/GigyaSwift/Models/Config/InternalConfig.swift b/GigyaSwift/Models/Config/InternalConfig.swift index 9d1990d..bc766cf 100644 --- a/GigyaSwift/Models/Config/InternalConfig.swift +++ b/GigyaSwift/Models/Config/InternalConfig.swift @@ -31,7 +31,10 @@ struct InternalConfig { internal static let hasRunBefore = "com.gigya.GigyaSDK:hasRunBefore" internal static let expirationSession = "com.gigya.GigyaSDK:expirationSession" internal static let pushKey = "com.gigya.GigyaTfa:pushKey" - internal static let webAuthn = "com.gigya.GigyaSDK:webauthn" + internal static func webAuthnKey(apiKey: String) -> String { + let suffix = String(apiKey.suffix(4)) + return "com.gigya.GigyaSDK:webauthn:\(suffix)" + } } struct Network {