diff --git a/Authenticator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Authenticator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..ad81a68b --- /dev/null +++ b/Authenticator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "bbb50b0c21b9941c9e66ea947e0b6e9cecb412ff3ab973c6c15e86a87f8bee81", + "pins" : [ + { + "identity" : "yubikit-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Yubico/yubikit-ios", + "state" : { + "branch" : "main", + "revision" : "805cab56042303254070f46414dbef24e0fa2abc" + } + } + ], + "version" : 3 +} diff --git a/Authenticator/Localizable.xcstrings b/Authenticator/Localizable.xcstrings index bddf4501..49583af0 100644 --- a/Authenticator/Localizable.xcstrings +++ b/Authenticator/Localizable.xcstrings @@ -1411,6 +1411,9 @@ } } }, + "Decryption failed" : { + "comment" : "PIV extension decryption failed error message" + }, "Delete" : { "comment" : "Menu", "localizations" : { @@ -2635,6 +2638,9 @@ } } }, + "Invalid decryption" : { + "comment" : "PIV extension NFC invalid decryption" + }, "Invalid device info received from YubiKey" : { "comment" : "Internal error message not to be displayed to the user.", "localizations" : { @@ -5666,6 +5672,9 @@ } } }, + "Successfully decrypted cipher data" : { + "comment" : "PIV extension NFC successfully decrypted cipher data" + }, "Successfully read" : { "comment" : "iOS NFC alert successfully read", "localizations" : { @@ -5897,7 +5906,7 @@ } }, "The private key on the YubiKey does not match the certificate or there is no private key stored on the YubiKey." : { - "comment" : "PIV extension NFC invalid signature no private key", + "comment" : "PIV extension NFC invalid decryption no private key\nPIV extension NFC invalid signature no private key", "localizations" : { "de" : { "stringUnit" : { diff --git a/Authenticator/Model/CryptoTokenKit/TokenCertificateStorage.swift b/Authenticator/Model/CryptoTokenKit/TokenCertificateStorage.swift index 839f64c0..dd746394 100644 --- a/Authenticator/Model/CryptoTokenKit/TokenCertificateStorage.swift +++ b/Authenticator/Model/CryptoTokenKit/TokenCertificateStorage.swift @@ -41,6 +41,8 @@ struct TokenCertificateStorage { tokenKeychainKey.label = label tokenKeychainKey.canSign = true tokenKeychainKey.isSuitableForLogin = true + tokenKeychainKey.canDecrypt = true + tokenKeychainKey.canPerformKeyExchange = true // TODO: figure out when there might be multiple driverConfigurations and how to handle it guard let tokenDriverConfiguration = TKTokenDriver.Configuration.driverConfigurations.first?.value else { diff --git a/Authenticator/Model/TokenRequestViewModel.swift b/Authenticator/Model/TokenRequestViewModel.swift index 500e247f..7ff6a906 100644 --- a/Authenticator/Model/TokenRequestViewModel.swift +++ b/Authenticator/Model/TokenRequestViewModel.swift @@ -101,6 +101,7 @@ class TokenRequestViewModel: NSObject { connection.startConnection { connection in connection.pivSession { session, error in guard let session = session else { Logger.ctk.error("No session: \(error!)"); return } + guard let operationType = userInfo.operationType() else { Logger.ctk.error("No OperationType defined"); return } guard let type = userInfo.keyType(), let objectId = userInfo.objectId(), let algorithm = userInfo.algorithm(), @@ -127,35 +128,64 @@ class TokenRequestViewModel: NSObject { return } } - session.signWithKey(in: slot, type: type, algorithm: algorithm, message: message) { signature, error in - // Handle any errors - if let error = error, (error as NSError).code == 0x6a80 { - YubiKitManager.shared.stopNFCConnection(withErrorMessage: String(localized: "Invalid signature", comment: "PIV extension NFC invalid signature")) - completion(.communicationError(ErrorMessage(title: String(localized: "Invalid signature", comment: "PIV extension NFC invalid signature"), - text: String(localized: "The private key on the YubiKey does not match the certificate or there is no private key stored on the YubiKey.", comment: "PIV extension NFC invalid signature no private key")))) - return - } - if let error = error { - completion(.communicationError(ErrorMessage(title: String(localized: "Signing failed", comment: "PIV extension signing failed error message"), text: error.localizedDescription))) - return - } - guard let signature = signature else { fatalError() } - // Verify signature - let signatureError = self.verifySignature(signature, data: message, objectId: objectId, algorithm: algorithm) - if signatureError != nil { - YubiKitManager.shared.stopNFCConnection(withErrorMessage: String(localized: "Invalid signature", comment: "PIV extension invalid signature")) - completion(.communicationError(ErrorMessage(title: String(localized: "Invalid signature", comment: "PIV extension invalid signature"), - text: String(localized: "The private key on the YubiKey does not match the certificate.", comment: "PIV extension invalid signature message")))) - return - } - - YubiKitManager.shared.stopNFCConnection(withMessage: String(localized: "Successfully signed data", comment: "PIV extension NFC successfully signed data")) - - if let userDefaults = UserDefaults(suiteName: "group.com.yubico.Authenticator") { - Logger.ctk.debug("Save data to userDefaults...") - userDefaults.setValue(signature, forKey: "signedData") - completion(nil) - } + + switch operationType { + case .signData: + session.signWithKey(in: slot, type: type, algorithm: algorithm, message: message) { signature, error in + // Handle any errors + if let error = error, (error as NSError).code == 0x6a80 { + YubiKitManager.shared.stopNFCConnection(withErrorMessage: String(localized: "Invalid signature", comment: "PIV extension NFC invalid signature")) + completion(.communicationError(ErrorMessage(title: String(localized: "Invalid signature", comment: "PIV extension NFC invalid signature"), + text: String(localized: "The private key on the YubiKey does not match the certificate or there is no private key stored on the YubiKey.", comment: "PIV extension NFC invalid signature no private key")))) + return + } + if let error = error { + completion(.communicationError(ErrorMessage(title: String(localized: "Signing failed", comment: "PIV extension signing failed error message"), text: error.localizedDescription))) + return + } + guard let signature = signature else { fatalError() } + // Verify signature + let signatureError = self.verifySignature(signature, data: message, objectId: objectId, algorithm: algorithm) + if signatureError != nil { + YubiKitManager.shared.stopNFCConnection(withErrorMessage: String(localized: "Invalid signature", comment: "PIV extension invalid signature")) + completion(.communicationError(ErrorMessage(title: String(localized: "Invalid signature", comment: "PIV extension invalid signature"), + text: String(localized: "The private key on the YubiKey does not match the certificate.", comment: "PIV extension invalid signature message")))) + return + } + + YubiKitManager.shared.stopNFCConnection(withMessage: String(localized: "Successfully signed data", comment: "PIV extension NFC successfully signed data")) + + if let userDefaults = UserDefaults(suiteName: "group.com.yubico.Authenticator") { + Logger.ctk.debug("Save data to userDefaults...") + userDefaults.setValue(signature, forKey: "signedData") + completion(nil) + } + } // End signWithKey Session + case .decryptData: + // Begin Decryption Session + session.decryptWithKey(in: slot, algorithm: algorithm, encrypted: message) { decryptedData, error in + // Handle any errors + if let error = error, (error as NSError).code == 0x6a80 { + YubiKitManager.shared.stopNFCConnection(withErrorMessage: String(localized: "Invalid decryption", comment: "PIV extension NFC invalid decryption")) + completion(.communicationError(ErrorMessage(title: String(localized: "Invalid decryption", comment: "PIV extension NFC invalid decryption"), + text: String(localized: "The private key on the YubiKey does not match the certificate or there is no private key stored on the YubiKey.", comment: "PIV extension NFC invalid decryption no private key")))) + return + } + if let error = error { + completion(.communicationError(ErrorMessage(title: String(localized: "Decryption failed", comment: "PIV extension decryption failed error message"), text: error.localizedDescription))) + return + } + + guard let decryptedData = decryptedData else { fatalError() } + + YubiKitManager.shared.stopNFCConnection(withMessage: String(localized: "Successfully decrypted cipher data", comment: "PIV extension NFC successfully decrypted cipher data")) + + if let userDefaults = UserDefaults(suiteName: "group.com.yubico.Authenticator") { + Logger.ctk.debug("Save decrypted data to userDefaults...") + userDefaults.setValue(decryptedData, forKey: "decryptedData") + completion(nil) + } + } // End Decryption Session } } } @@ -186,6 +216,11 @@ class TokenRequestViewModel: NSObject { } } +enum OperationType: String { + case signData = "signData" + case decryptData = "decryptData" +} + extension TokenRequestViewModel { @@ -300,6 +335,11 @@ private extension Dictionary where Key == AnyHashable, Value: Any { guard let rawValue = self["algorithm"] as? String else { return nil } return SecKeyAlgorithm(rawValue: rawValue as CFString) } + + func operationType() -> OperationType? { + guard let rawValue = self["operationType"] as? String else { return nil } + return OperationType.init(rawValue: rawValue) + } } extension String: Error {} diff --git a/TokenExtension/Localizable.xcstrings b/TokenExtension/Localizable.xcstrings index 5334bc71..769cce2d 100644 --- a/TokenExtension/Localizable.xcstrings +++ b/TokenExtension/Localizable.xcstrings @@ -28,6 +28,9 @@ } } } + }, + "Tap here to complete the decryption request using your YubiKey." : { + }, "Tap here to complete the request using your YubiKey." : { "localizations" : { diff --git a/TokenExtension/TokenSession.swift b/TokenExtension/TokenSession.swift index 0d967e6e..5fd57ca0 100644 --- a/TokenExtension/TokenSession.swift +++ b/TokenExtension/TokenSession.swift @@ -29,6 +29,11 @@ class TokenSession: TKTokenSession, TKTokenSessionDelegate { case eccp384 = 0x14 case unknown = 0x00 } + + enum OperationType: String { + case signData = "signData" + case decryptData = "decryptData" + } func tokenSession(_ session: TKTokenSession, beginAuthFor operation: TKTokenOperation, constraint: Any) throws -> TKTokenAuthOperation { // Insert code here to create an instance of TKTokenAuthOperation based on the specified operation and constraint. @@ -37,7 +42,12 @@ class TokenSession: TKTokenSession, TKTokenSessionDelegate { } func tokenSession(_ session: TKTokenSession, supports operation: TKTokenOperation, keyObjectID: Any, algorithm: TKTokenKeyAlgorithm) -> Bool { - return operation == .signData + switch operation { + case .readData, .signData, .decryptData, .performKeyExchange: + return true + default: + return false + } } func tokenSession(_ session: TKTokenSession, sign dataToSign: Data, keyObjectID: Any, algorithm: TKTokenKeyAlgorithm) throws -> Data { @@ -103,19 +113,67 @@ class TokenSession: TKTokenSession, TKTokenSessionDelegate { throw NSError(domain: TKErrorDomain, code: TKError.Code.canceledByUser.rawValue, userInfo: nil) } + // Decryption func tokenSession(_ session: TKTokenSession, decrypt ciphertext: Data, keyObjectID: Any, algorithm: TKTokenKeyAlgorithm) throws -> Data { - var plaintext: Data? - // Insert code here to decrypt the ciphertext using the specified key and algorithm. - plaintext = nil + // if we're not passed sessionEndTime throw error and cancel all notifications + if sessionEndTime.timeIntervalSinceNow > 0 { + cancelAllNotifications() + throw NSError(domain: TKErrorDomain, code: TKError.Code.canceledByUser.rawValue, userInfo: nil) + } + + // if we're past sessionEndTime set a new endtime and reset + if sessionEndTime.timeIntervalSinceNow < 0 { + reset() + sessionEndTime = Date(timeIntervalSinceNow: 100) + } - if let plaintext = plaintext { - return plaintext - } else { - // If the operation failed for some reason, fill in an appropriate error like objectNotFound, corruptedData, etc. - // Note that responding with TKErrorCodeAuthenticationNeeded will trigger user authentication after which the current operation will be re-attempted. - throw NSError(domain: TKErrorDomain, code: TKError.Code.authenticationNeeded.rawValue, userInfo: nil) + guard let key = try? session.token.configuration.key(for: keyObjectID), let objectId = keyObjectID as? String else { + throw "No key for you!" + } + + var possibleKeyType: KeyType? = nil + if key.keyType == kSecAttrKeyTypeRSA as String { + if key.keySizeInBits == 1024 { + possibleKeyType = .rsa1024 + } else if key.keySizeInBits == 2048 { + possibleKeyType = .rsa2048 + } + } else if key.keyType == kSecAttrKeyTypeECSECPrimeRandom as String { + if key.keySizeInBits == 256 { + possibleKeyType = .eccp256 + } else if key.keySizeInBits == 384 { + possibleKeyType = .eccp384 + } + } + + guard let keyType = possibleKeyType, let secKeyAlgorithm = algorithm.secKeyAlgorithm else { + throw NSError(domain: TKErrorDomain, code: TKError.Code.canceledByUser.rawValue, userInfo: nil) } + + sendNotificationWithEncryptedData(ciphertext, keyObjectID: objectId, keyType: keyType, algorithm: secKeyAlgorithm) + + let loopEndTime = Date(timeIntervalSinceNow: 95) + var runLoop = true + while(runLoop) { + Thread.sleep(forTimeInterval: 1) + if let userDefaults = UserDefaults(suiteName: "group.com.yubico.Authenticator"), let decryptedData = userDefaults.value(forKey: "decryptedData") as? Data { + sessionEndTime = Date(timeIntervalSinceNow: -10) + reset() + return decryptedData + } + if let userDefaults = UserDefaults(suiteName: "group.com.yubico.Authenticator"), let _ = userDefaults.value(forKey: "canceledByUser") { + sessionEndTime = Date(timeIntervalSinceNow: 3) + reset() + throw NSError(domain: TKErrorDomain, code: TKError.Code.canceledByUser.rawValue, userInfo: nil) + } + + if loopEndTime < Date() { + runLoop = false + } + } + reset() + throw NSError(domain: TKErrorDomain, code: TKError.Code.canceledByUser.rawValue, userInfo: nil) } func tokenSession(_ session: TKTokenSession, performKeyExchange otherPartyPublicKeyData: Data, keyObjectID objectID: Any, algorithm: TKTokenKeyAlgorithm, parameters: TKTokenKeyExchangeParameters) throws -> Data { @@ -138,6 +196,7 @@ class TokenSession: TKTokenSession, TKTokenSessionDelegate { if let userDefaults = UserDefaults(suiteName: "group.com.yubico.Authenticator") { userDefaults.removeObject(forKey: "canceledByUser") userDefaults.removeObject(forKey: "signedData") + userDefaults.removeObject(forKey: "decryptedData") } } @@ -147,14 +206,36 @@ class TokenSession: TKTokenSession, TKTokenSessionDelegate { center.removeAllPendingNotificationRequests() } + // Send local notification with data to sign private func sendNotificationWithData(_ data: Data, keyObjectID: String, keyType: KeyType, algorithm: SecKeyAlgorithm) { - cancelAllNotifications() - let categoryID = "SignData" + cancelAllNotifications() + let categoryID = OperationType.signData.rawValue let content = UNMutableNotificationContent() content.title = String(localized: "YubiKey required") content.body = String(localized: "Tap here to complete the request using your YubiKey.") content.categoryIdentifier = categoryID - content.userInfo = ["data": data, "keyObjectID": keyObjectID, "algorithm": algorithm.rawValue, "keyType": keyType.rawValue]; + content.userInfo = ["operationType": categoryID, "data": data, "keyObjectID": keyObjectID, "algorithm": algorithm.rawValue, "keyType": keyType.rawValue]; + content.sound = UNNotificationSound.default + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) + + let show = UNNotificationAction(identifier: categoryID, title: String(localized: "Launch Yubico Authenticator"), options: .foreground) + let category = UNNotificationCategory(identifier: categoryID, actions: [show], intentIdentifiers: []) + + let center = UNUserNotificationCenter.current() + center.setNotificationCategories([category]) + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) + center.add(request) + } + + // Send local notification with encryption data + private func sendNotificationWithEncryptedData(_ cipherData: Data, keyObjectID: String, keyType: KeyType, algorithm: SecKeyAlgorithm) { + cancelAllNotifications() + let categoryID = OperationType.decryptData.rawValue + let content = UNMutableNotificationContent() + content.title = String(localized: "YubiKey required") + content.body = String(localized: "Tap here to complete the decryption request using your YubiKey.") + content.categoryIdentifier = categoryID + content.userInfo = ["operationType": categoryID, "data": cipherData, "keyObjectID": keyObjectID, "algorithm": algorithm.rawValue, "keyType": keyType.rawValue]; content.sound = UNNotificationSound.default let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)