Skip to content
Closed
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

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

11 changes: 10 additions & 1 deletion Authenticator/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,9 @@
}
}
},
"Decryption failed" : {
"comment" : "PIV extension decryption failed error message"
},
"Delete" : {
"comment" : "Menu",
"localizations" : {
Expand Down Expand Up @@ -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" : {
Expand Down Expand Up @@ -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" : {
Expand Down Expand Up @@ -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" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
98 changes: 69 additions & 29 deletions Authenticator/Model/TokenRequestViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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
}
}
}
Expand Down Expand Up @@ -186,6 +216,11 @@ class TokenRequestViewModel: NSObject {
}
}

enum OperationType: String {
case signData = "signData"
case decryptData = "decryptData"
}


extension TokenRequestViewModel {

Expand Down Expand Up @@ -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 {}
Expand Down
3 changes: 3 additions & 0 deletions TokenExtension/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
}
}
}
},
"Tap here to complete the decryption request using your YubiKey." : {

},
"Tap here to complete the request using your YubiKey." : {
"localizations" : {
Expand Down
107 changes: 94 additions & 13 deletions TokenExtension/TokenSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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")
}
}

Expand All @@ -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)

Expand Down
Loading