From 5dd71d821c81633c48a1c70a2c40c0f2b1c272c1 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Tue, 1 Aug 2017 15:26:02 -0600 Subject: [PATCH 1/9] simple cli interface for listing registrations --- SoftU2FTool/AppDelegate.swift | 37 ++++++++++++++++- SoftU2FTool/KeyPair.swift | 64 ++++++++++++++++++++++++++++++ SoftU2FTool/Keychain.swift | 28 +++++++++++++ SoftU2FTool/U2FAuthenticator.swift | 22 +++++++++- SoftU2FTool/U2FRegistration.swift | 37 +++++++++++++++++ 5 files changed, 185 insertions(+), 3 deletions(-) diff --git a/SoftU2FTool/AppDelegate.swift b/SoftU2FTool/AppDelegate.swift index 84672f9..8b16144 100644 --- a/SoftU2FTool/AppDelegate.swift +++ b/SoftU2FTool/AppDelegate.swift @@ -7,17 +7,28 @@ import Cocoa +let listKeysArgument = "--list" + @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { + if CommandLine.arguments.contains(listKeysArgument) { + listKeys() + quit() + return + } + + if !U2FAuthenticator.start() { print("Error starting authenticator") + quit() + return } } func applicationWillTerminate(_ aNotification: Notification) { - if !U2FAuthenticator.stop() { + if U2FAuthenticator.running && !U2FAuthenticator.stop() { print("Error stopping authenticator") } } @@ -28,4 +39,28 @@ class AppDelegate: NSObject, NSApplicationDelegate { // with our notification. NSApplication.shared().hide(nil) } + + private func quit() { + NSApplication.shared().terminate(self) + } + + private func listKeys() { + if let numRegs = U2FRegistration.count { + print("Registrations: ", numRegs) + } + + U2FRegistration.all.forEach { reg in + print("base64(key handle): ", reg.keyHandle.base64EncodedString()) + print("base64(sha256(appid)): ", reg.applicationParameter.base64EncodedString()) + + if let kf = KnownFacets[reg.applicationParameter] { + print("site: ", kf) + } else { + print("site: unknown") + } + + print("counter: ", reg.counter) + print("") + } + } } diff --git a/SoftU2FTool/KeyPair.swift b/SoftU2FTool/KeyPair.swift index 54b4380..346656f 100644 --- a/SoftU2FTool/KeyPair.swift +++ b/SoftU2FTool/KeyPair.swift @@ -7,7 +7,63 @@ import Foundation +fileprivate class tmpKeyPair { + var label: String + var applicationLabel: Data + var publicKey: SecKey? + var privateKey: SecKey? + + var keyPair: KeyPair? { + guard let pub = publicKey else { return nil } + guard let priv = privateKey else { return nil } + + return KeyPair(label: label, appLabel: applicationLabel, publicKey: pub, privateKey: priv) + } + + init(label l: String, applicationLabel al: Data) { + label = l + applicationLabel = al + } +} + class KeyPair { + /// Get all KeyPairs with the given label. + static func all(label: String) -> [KeyPair] { + let secKeys = Keychain.getSecKeys(attrLabel: label as CFString) + var tmpKeyPairs: [tmpKeyPair] = [] + var keyPairs: [KeyPair] = [] + + secKeys.forEach { secKey in + guard let appLabel: CFData = Keychain.getSecKeyAttr(key: secKey, attr: kSecAttrApplicationLabel) else { return } + + let applicationLabel = appLabel as Data + var tmpKP: tmpKeyPair + + if let kp = tmpKeyPairs.first(where: { $0.applicationLabel == applicationLabel }) { + tmpKP = kp + } else { + tmpKP = tmpKeyPair.init(label: label, applicationLabel: applicationLabel) + tmpKeyPairs.append(tmpKP) + } + + guard let klass: CFString = Keychain.getSecKeyAttr(key: secKey, attr: kSecAttrKeyClass) else { return } + + if klass == kSecAttrKeyClassPublic { + tmpKP.publicKey = secKey + } else { + tmpKP.privateKey = secKey + } + } + + tmpKeyPairs.forEach { tmpKP in + guard let kp = tmpKP.keyPair else { return } + + keyPairs.append(kp) + } + + return keyPairs + } + // The number of key pairs (keys/2) in the keychain. static func count(label: String) -> Int? { guard let c = Keychain.count(attrLabel: label as CFString) else { return nil } @@ -74,6 +130,14 @@ class KeyPair { guard let priv = Keychain.getSecKey(attrAppLabel: applicationLabel as CFData, keyClass: kSecAttrKeyClassPrivate) else { return nil } privateKey = priv } + + // Initialize a key pair with all the necessary data. + init(label l: String, appLabel al: Data, publicKey pub: SecKey, privateKey priv: SecKey) { + label = l + applicationLabel = al + publicKey = pub + privateKey = priv + } // Delete this key pair. func delete() -> Bool { diff --git a/SoftU2FTool/Keychain.swift b/SoftU2FTool/Keychain.swift index f5eab50..1908ec2 100644 --- a/SoftU2FTool/Keychain.swift +++ b/SoftU2FTool/Keychain.swift @@ -151,6 +151,34 @@ class Keychain { return opaqueResult as! CFData as Data } + + // Lookup all keys with the given label. + static func getSecKeys(attrLabel: CFString) -> [SecKey] { + let query = makeCFDictionary( + (kSecClass, kSecClassKey), + (kSecAttrKeyType, kSecAttrKeyTypeEC), + (kSecAttrLabel, attrLabel), + (kSecReturnRef, kCFBooleanTrue), + (kSecMatchLimit, 1000 as CFNumber) + ) + + var optionalOpaqueResult: CFTypeRef? = nil + let err = SecItemCopyMatching(query, &optionalOpaqueResult) + + if err != errSecSuccess { + print("Error from keychain: \(err)") + return [] + } + + guard let opaqueResult = optionalOpaqueResult else { + print("Unexpected nil returned from keychain") + return [] + } + + let result = opaqueResult as! [SecKey] + + return result + } static func getSecKey(attrAppLabel: CFData, keyClass: CFString) -> SecKey? { // Lookup public key. diff --git a/SoftU2FTool/U2FAuthenticator.swift b/SoftU2FTool/U2FAuthenticator.swift index 9a5eace..f901ffa 100644 --- a/SoftU2FTool/U2FAuthenticator.swift +++ b/SoftU2FTool/U2FAuthenticator.swift @@ -14,6 +14,13 @@ class U2FAuthenticator { private static var hasShared = false private let u2fhid: U2FHID + + var running: Bool + + static var running: Bool { + guard let ua: U2FAuthenticator = shared else { return false } + return ua.running + } static func start() -> Bool { guard let ua: U2FAuthenticator = shared else { return false } @@ -28,16 +35,27 @@ class U2FAuthenticator { init?() { guard let uh: U2FHID = U2FHID.shared else { return nil } + running = false u2fhid = uh installMsgHandler() } func start() -> Bool { - return u2fhid.run() + if u2fhid.run() { + running = true + return true + } + + return false } func stop() -> Bool { - return u2fhid.stop() + if u2fhid.stop() { + running = false + return true + } + + return false } func installMsgHandler() { diff --git a/SoftU2FTool/U2FRegistration.swift b/SoftU2FTool/U2FRegistration.swift index 94872f8..a27f9e8 100644 --- a/SoftU2FTool/U2FRegistration.swift +++ b/SoftU2FTool/U2FRegistration.swift @@ -10,6 +10,22 @@ import Foundation class U2FRegistration { // Allow using separate keychain namespace for tests. static var namespace = "SoftU2F Security Key" + + static var all: [U2FRegistration] { + let kps = KeyPair.all(label: namespace) + var regs: [U2FRegistration] = [] + + kps.forEach { kp in + guard let reg = U2FRegistration(keyPair: kp) else { + print("Error initializing U2FRegistration") + return + } + + regs.append(reg) + } + + return regs + } // The number of key pairs (keys/2) in the keychain. static var count: Int? { @@ -68,6 +84,27 @@ class U2FRegistration { return nil } } + + // Initialize a registration with all the necessary data. + init?(keyPair kp: KeyPair) { + keyPair = kp + + // Read our application parameter from the keychain. + guard let appTag = keyPair.applicationTag else { return nil } + + let counterSize = MemoryLayout.size + let appTagSize = Int(U2F_APPID_SIZE) + + if appTag.count != counterSize + appTagSize { + return nil + } + + counter = appTag.withUnsafeBytes { (ptr:UnsafePointer) -> UInt32 in + return ptr.pointee.bigEndian + } + + applicationParameter = appTag.subdata(in: counterSize..<(counterSize + appTagSize)) + } // Sign some data with the private key and increment our counter. func sign(_ data: Data) -> Data? { From 9ceb617a2a3f4ed0b4978869bbaa880aeed950a2 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Wed, 2 Aug 2017 12:45:58 -0600 Subject: [PATCH 2/9] abstract out settings/cli stuff --- SoftU2F.xcodeproj/project.pbxproj | 8 +++ SoftU2FTool/AppDelegate.swift | 35 +---------- SoftU2FTool/CLI.swift | 96 +++++++++++++++++++++++++++++++ SoftU2FTool/Settings.swift | 24 ++++++++ 4 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 SoftU2FTool/CLI.swift create mode 100644 SoftU2FTool/Settings.swift diff --git a/SoftU2F.xcodeproj/project.pbxproj b/SoftU2F.xcodeproj/project.pbxproj index fc8696b..7ee9d9a 100644 --- a/SoftU2F.xcodeproj/project.pbxproj +++ b/SoftU2F.xcodeproj/project.pbxproj @@ -97,6 +97,8 @@ F7B5DBC31E4A85EB00E5ABD4 /* VersionRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B5DBBF1E4A85EB00E5ABD4 /* VersionRequestTests.swift */; }; F7B5DBC91E4A8CF000E5ABD4 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B5DBC81E4A8CF000E5ABD4 /* Response.swift */; }; F7B5DBCA1E4A8CF000E5ABD4 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B5DBC81E4A8CF000E5ABD4 /* Response.swift */; }; + F7DD7DE81F313E65007F056E /* CLI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DD7DE71F313E65007F056E /* CLI.swift */; }; + F7DD7DEA1F324EE0007F056E /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DD7DE91F324EE0007F056E /* Settings.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -249,6 +251,8 @@ F7B5DBBE1E4A85EB00E5ABD4 /* RegisterRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegisterRequestTests.swift; sourceTree = ""; }; F7B5DBBF1E4A85EB00E5ABD4 /* VersionRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionRequestTests.swift; sourceTree = ""; }; F7B5DBC81E4A8CF000E5ABD4 /* Response.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; + F7DD7DE71F313E65007F056E /* CLI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLI.swift; sourceTree = ""; }; + F7DD7DE91F324EE0007F056E /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -374,6 +378,8 @@ 51213EC51E3916EB005454E0 /* U2FHID.swift */, 51B289E41E39903F00AD90CC /* U2FAuthenticator.swift */, 51FE30EC1E403DA000BAE824 /* U2FRegistration.swift */, + F7DD7DE71F313E65007F056E /* CLI.swift */, + F7DD7DE91F324EE0007F056E /* Settings.swift */, 518537BF1E380E4600600911 /* SoftU2F-Bridging-Header.h */, 51F090181E37E8C600F03AD3 /* Info.plist */, ); @@ -843,12 +849,14 @@ files = ( 5190B6121E3BFE3D00E6FE06 /* UserPresence.swift in Sources */, 51FE30F11E410B3D00BAE824 /* Utils.swift in Sources */, + F7DD7DEA1F324EE0007F056E /* Settings.swift in Sources */, 5119862E1E3C1519006A3BBB /* KnownFacets.swift in Sources */, 51F090101E37E8C600F03AD3 /* AppDelegate.swift in Sources */, 51213EC61E3916EB005454E0 /* U2FHID.swift in Sources */, 51E214601E3823E7005B2864 /* SHA256.swift in Sources */, 51B289E51E39903F00AD90CC /* U2FAuthenticator.swift in Sources */, 514F3D821E43C833008FA513 /* Keychain.swift in Sources */, + F7DD7DE81F313E65007F056E /* CLI.swift in Sources */, 51FE30ED1E403DA000BAE824 /* U2FRegistration.swift in Sources */, 51E214621E382521005B2864 /* WebSafeBase64.swift in Sources */, 514F3D871E43E828008FA513 /* KeyPair.swift in Sources */, diff --git a/SoftU2FTool/AppDelegate.swift b/SoftU2FTool/AppDelegate.swift index 8b16144..728d863 100644 --- a/SoftU2FTool/AppDelegate.swift +++ b/SoftU2FTool/AppDelegate.swift @@ -7,24 +7,15 @@ import Cocoa -let listKeysArgument = "--list" - @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { - if CommandLine.arguments.contains(listKeysArgument) { - listKeys() + if CLI(CommandLine.arguments).run() { quit() - return - } - - - if !U2FAuthenticator.start() { + } else if !U2FAuthenticator.start(){ print("Error starting authenticator") quit() - return } - } func applicationWillTerminate(_ aNotification: Notification) { @@ -39,28 +30,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { // with our notification. NSApplication.shared().hide(nil) } - + private func quit() { NSApplication.shared().terminate(self) } - - private func listKeys() { - if let numRegs = U2FRegistration.count { - print("Registrations: ", numRegs) - } - - U2FRegistration.all.forEach { reg in - print("base64(key handle): ", reg.keyHandle.base64EncodedString()) - print("base64(sha256(appid)): ", reg.applicationParameter.base64EncodedString()) - - if let kf = KnownFacets[reg.applicationParameter] { - print("site: ", kf) - } else { - print("site: unknown") - } - - print("counter: ", reg.counter) - print("") - } - } } diff --git a/SoftU2FTool/CLI.swift b/SoftU2FTool/CLI.swift new file mode 100644 index 0000000..d3810b4 --- /dev/null +++ b/SoftU2FTool/CLI.swift @@ -0,0 +1,96 @@ +// +// CLI.swift +// SoftU2F +// +// Created by Ben Toews on 8/1/17. +// + +import Foundation + +// Command line flags +fileprivate let listFlag = "--list" +fileprivate let deleteAllFlag = "--delete-all" +fileprivate let showSEPFlag = "--show-sep" +fileprivate let enableSEPFlag = "--enable-sep" +fileprivate let disableSEPFlag = "--disable-sep" + +// Settings keys +fileprivate let useSEPKey = "useSEP" + +class CLI { + private let args: [String] + + init(_ arguments: [String]) { + args = arguments + } + + func run() -> Bool { + if args.contains(listFlag) { + listRegistrations() + return true + } else if args.contains(deleteAllFlag) { + deleteAll() + return true + } else if args.contains(showSEPFlag) { + showSEP() + return true + } + + return false + } + + private func listRegistrations() { + print("The following is a list of U2F registrations stored in your keychain. Each key contains several fields:") + print(" - Key handle: This is the key handle that we registered with a website. For Soft U2F, the key handle is simply a hash of the public key.") + print(" - Application parameter: This is the sha256 of the app-id of the site.") + print(" - Known facet: For some sites we know the application parameter → site name mapping.") + print(" - Counter: How many times this registration has been used.") + print("") + + U2FRegistration.all.forEach { reg in + print("Key handle: ", reg.keyHandle.base64EncodedString()) + print("Application parameter: ", reg.applicationParameter.base64EncodedString()) + + if let kf = KnownFacets[reg.applicationParameter] { + print("Known facet: ", kf) + } else { + print("Known facet: N/A") + } + + print("Counter: ", reg.counter) + print("") + } + } + + private func deleteAll() { + guard let initialCount = U2FRegistration.count else { + print("Error getting registration count from keychain.") + return + } + + if !U2FRegistration.deleteAll() { + print("Error deleting registrations from keychain.") + return + } + + print("Deleted ", initialCount, " registrations") + } + + private func showSEP() { + if Settings.sepEnabled { + print("SEP storage is enabled for new keys") + } else { + print("SEP storage is disabled for new keys") + } + } + + private func enableSEP() { + Settings.enableSEP() + print("SEP storage is now enabled for new keys") + } + + private func disableSEP() { + Settings.disableSEP() + print("SEP storage is now disabled for new keys") + } +} diff --git a/SoftU2FTool/Settings.swift b/SoftU2FTool/Settings.swift new file mode 100644 index 0000000..499b027 --- /dev/null +++ b/SoftU2FTool/Settings.swift @@ -0,0 +1,24 @@ +// +// Settings.swift +// SoftU2F +// +// Created by Ben Toews on 8/2/17. +// + +import Foundation + +class Settings { + private static let sepEnabledKey = "sepEnabled" + + static var sepEnabled: Bool { + return UserDefaults.standard.bool(forKey: sepEnabledKey) + } + + static func enableSEP() { + return UserDefaults.standard.set(true, forKey: sepEnabledKey) + } + + static func disableSEP() { + return UserDefaults.standard.set(false, forKey: sepEnabledKey) + } +} From 3c29d9b7b229ec01783b7f569fbc22c358fd1e30 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Wed, 2 Aug 2017 15:03:31 -0600 Subject: [PATCH 3/9] allow keys to optionally be stored in SEP --- SoftU2FTool/CLI.swift | 2 ++ SoftU2FTool/KeyPair.swift | 32 +++++++++++--------- SoftU2FTool/Keychain.swift | 47 ++++++++++++++++++++++-------- SoftU2FTool/Settings.swift | 22 +++++++++++--- SoftU2FTool/U2FAuthenticator.swift | 22 +++++++------- SoftU2FTool/U2FRegistration.swift | 8 +++-- SoftU2FTool/UserPresence.swift | 4 +-- 7 files changed, 94 insertions(+), 43 deletions(-) diff --git a/SoftU2FTool/CLI.swift b/SoftU2FTool/CLI.swift index d3810b4..bebda80 100644 --- a/SoftU2FTool/CLI.swift +++ b/SoftU2FTool/CLI.swift @@ -45,6 +45,7 @@ class CLI { print(" - Application parameter: This is the sha256 of the app-id of the site.") print(" - Known facet: For some sites we know the application parameter → site name mapping.") print(" - Counter: How many times this registration has been used.") + print(" — In SEP: Whether this registration's private key is stored in the SEP.") print("") U2FRegistration.all.forEach { reg in @@ -58,6 +59,7 @@ class CLI { } print("Counter: ", reg.counter) + print("In SEP: ", reg.inSEP) print("") } } diff --git a/SoftU2FTool/KeyPair.swift b/SoftU2FTool/KeyPair.swift index 346656f..e1b45c1 100644 --- a/SoftU2FTool/KeyPair.swift +++ b/SoftU2FTool/KeyPair.swift @@ -12,14 +12,14 @@ fileprivate class tmpKeyPair { var applicationLabel: Data var publicKey: SecKey? var privateKey: SecKey? - + var keyPair: KeyPair? { guard let pub = publicKey else { return nil } guard let priv = privateKey else { return nil } - + return KeyPair(label: label, appLabel: applicationLabel, publicKey: pub, privateKey: priv) } - + init(label l: String, applicationLabel al: Data) { label = l applicationLabel = al @@ -32,38 +32,38 @@ class KeyPair { let secKeys = Keychain.getSecKeys(attrLabel: label as CFString) var tmpKeyPairs: [tmpKeyPair] = [] var keyPairs: [KeyPair] = [] - + secKeys.forEach { secKey in guard let appLabel: CFData = Keychain.getSecKeyAttr(key: secKey, attr: kSecAttrApplicationLabel) else { return } let applicationLabel = appLabel as Data var tmpKP: tmpKeyPair - + if let kp = tmpKeyPairs.first(where: { $0.applicationLabel == applicationLabel }) { tmpKP = kp } else { tmpKP = tmpKeyPair.init(label: label, applicationLabel: applicationLabel) tmpKeyPairs.append(tmpKP) } - + guard let klass: CFString = Keychain.getSecKeyAttr(key: secKey, attr: kSecAttrKeyClass) else { return } - + if klass == kSecAttrKeyClassPublic { tmpKP.publicKey = secKey } else { tmpKP.privateKey = secKey } } - + tmpKeyPairs.forEach { tmpKP in guard let kp = tmpKP.keyPair else { return } keyPairs.append(kp) } - + return keyPairs } - + // The number of key pairs (keys/2) in the keychain. static func count(label: String) -> Int? { guard let c = Keychain.count(attrLabel: label as CFString) else { return nil } @@ -105,11 +105,17 @@ class KeyPair { return Keychain.getSecItemData(attrAppLabel: applicationLabel) } + var inSEP: Bool { + let tokenID: String = Keychain.getSecItemAttr(attrAppLabel: applicationLabel as CFData, name: kSecAttrTokenID) ?? "nope" + + return tokenID == kSecAttrTokenIDSecureEnclave as String + } + // Generate a new key pair. - init?(label l: String) { + init?(label l: String, inSEP sep: Bool) { label = l - guard let (pub, priv) = Keychain.generateKeyPair(attrLabel: label as CFString) else { return nil } + guard let (pub, priv) = Keychain.generateKeyPair(attrLabel: label as CFString, inSEP: sep) else { return nil } publicKey = pub privateKey = priv @@ -130,7 +136,7 @@ class KeyPair { guard let priv = Keychain.getSecKey(attrAppLabel: applicationLabel as CFData, keyClass: kSecAttrKeyClassPrivate) else { return nil } privateKey = priv } - + // Initialize a key pair with all the necessary data. init(label l: String, appLabel al: Data, publicKey pub: SecKey, privateKey priv: SecKey) { label = l diff --git a/SoftU2FTool/Keychain.swift b/SoftU2FTool/Keychain.swift index 1908ec2..1766268 100644 --- a/SoftU2FTool/Keychain.swift +++ b/SoftU2FTool/Keychain.swift @@ -99,7 +99,6 @@ class Keychain { } guard let opaqueResult = dict[name as String] else { - print("Missing key in dictionary") return nil } @@ -208,25 +207,49 @@ class Keychain { return result } - static func generateKeyPair(attrLabel: CFString) -> (SecKey, SecKey)? { + static func generateKeyPair(attrLabel: CFString, inSEP: Bool) -> (SecKey, SecKey)? { + // Make ACL controlling access to generated keys. + let acl: SecAccessControl? var err: Unmanaged? = nil defer { err?.release() } - // Make ACL controlling access to generated keys. - let acl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlocked, SecAccessControlCreateFlags(rawValue: 0), &err) + if inSEP { + if #available(OSX 10.12.1, *) { + acl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, [.privateKeyUsage, .touchIDCurrentSet], &err) + } else { + print("Cannot generate keys in SEP on macOS<10.12.1") + return nil + } + } else { + acl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlocked, [], &err) + } + if acl == nil || err != nil { print("Error generating ACL for key generation") return nil } // Make parameters for generating keys. - let params = makeCFDictionary( - (kSecAttrKeyType, kSecAttrKeyTypeEC), - (kSecAttrKeySizeInBits, 256 as CFNumber), - (kSecAttrAccessControl, acl!), - (kSecAttrIsPermanent, kCFBooleanTrue), - (kSecAttrLabel, attrLabel) - ) + let params: CFDictionary + + if inSEP { + params = makeCFDictionary( + (kSecAttrKeyType, kSecAttrKeyTypeEC), + (kSecAttrKeySizeInBits, 256 as CFNumber), + (kSecAttrAccessControl, acl!), + (kSecAttrIsPermanent, kCFBooleanTrue), + (kSecAttrLabel, attrLabel), + (kSecAttrTokenID, kSecAttrTokenIDSecureEnclave) + ) + } else { + params = makeCFDictionary( + (kSecAttrKeyType, kSecAttrKeyTypeEC), + (kSecAttrKeySizeInBits, 256 as CFNumber), + (kSecAttrAccessControl, acl!), + (kSecAttrIsPermanent, kCFBooleanTrue), + (kSecAttrLabel, attrLabel) + ) + } // Generate key pair. var pub: SecKey? = nil @@ -251,7 +274,7 @@ class Keychain { var err: Unmanaged? = nil defer { err?.release() } - let sig = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &err) as? Data + let sig = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &err) as Data? if err != nil { print("Error creating signature: \(err!.takeUnretainedValue().localizedDescription)") diff --git a/SoftU2FTool/Settings.swift b/SoftU2FTool/Settings.swift index 499b027..bc8639f 100644 --- a/SoftU2FTool/Settings.swift +++ b/SoftU2FTool/Settings.swift @@ -6,19 +6,33 @@ // import Foundation +import LocalAuthentication class Settings { private static let sepEnabledKey = "sepEnabled" static var sepEnabled: Bool { - return UserDefaults.standard.bool(forKey: sepEnabledKey) + return sepAvailable && UserDefaults.standard.bool(forKey: sepEnabledKey) } - static func enableSEP() { - return UserDefaults.standard.set(true, forKey: sepEnabledKey) + static func enableSEP() -> Bool { + if sepAvailable { + UserDefaults.standard.set(true, forKey: sepEnabledKey) + return true + } else { + return false + } } static func disableSEP() { - return UserDefaults.standard.set(false, forKey: sepEnabledKey) + UserDefaults.standard.set(false, forKey: sepEnabledKey) + } + + private static var sepAvailable: Bool { + if #available(OSX 10.12.2, *) { + return LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) + } else { + return false + } } } diff --git a/SoftU2FTool/U2FAuthenticator.swift b/SoftU2FTool/U2FAuthenticator.swift index f901ffa..f9ba65d 100644 --- a/SoftU2FTool/U2FAuthenticator.swift +++ b/SoftU2FTool/U2FAuthenticator.swift @@ -13,10 +13,10 @@ class U2FAuthenticator { static let shared = U2FAuthenticator() private static var hasShared = false - private let u2fhid: U2FHID - var running: Bool - + + private let u2fhid: U2FHID + static var running: Bool { guard let ua: U2FAuthenticator = shared else { return false } return ua.running @@ -45,7 +45,7 @@ class U2FAuthenticator { running = true return true } - + return false } @@ -54,7 +54,7 @@ class U2FAuthenticator { running = false return true } - + return false } @@ -90,14 +90,15 @@ class U2FAuthenticator { let facet = KnownFacets[req.applicationParameter] let notification = UserPresence.Notification.Register(facet: facet) + let skipTUP = Settings.sepEnabled - UserPresence.test(notification) { success in - if !success { + UserPresence.test(notification, skip: skipTUP) { tupSuccess in + if !tupSuccess { // Send no response. Otherwise Chrome will re-prompt immediately. return } - guard let reg = U2FRegistration(applicationParameter: req.applicationParameter) else { + guard let reg = U2FRegistration(applicationParameter: req.applicationParameter, inSEP: Settings.sepEnabled) else { print("Error creating registration.") self.sendError(status: .OtherError, cid: cid) return @@ -146,9 +147,10 @@ class U2FAuthenticator { let facet = KnownFacets[req.applicationParameter] let notification = UserPresence.Notification.Authenticate(facet: facet) + let skipTUP = reg.inSEP - UserPresence.test(notification) { success in - if !success { + UserPresence.test(notification, skip: skipTUP) { tupSuccess in + if !tupSuccess { // Send no response. Otherwise Chrome will re-prompt immediately. return } diff --git a/SoftU2FTool/U2FRegistration.swift b/SoftU2FTool/U2FRegistration.swift index a27f9e8..53f95b8 100644 --- a/SoftU2FTool/U2FRegistration.swift +++ b/SoftU2FTool/U2FRegistration.swift @@ -45,12 +45,16 @@ class U2FRegistration { var keyHandle: Data { return padKeyHandle(keyPair.applicationLabel) } + + var inSEP: Bool { + return keyPair.inSEP + } // Generate a new registration. - init?(applicationParameter ap: Data) { + init?(applicationParameter ap: Data, inSEP sep: Bool) { applicationParameter = ap - guard let kp = KeyPair(label: U2FRegistration.namespace) else { return nil } + guard let kp = KeyPair(label: U2FRegistration.namespace, inSEP: sep) else { return nil } keyPair = kp counter = 1 diff --git a/SoftU2FTool/UserPresence.swift b/SoftU2FTool/UserPresence.swift index 1d6b9c5..8a12261 100644 --- a/SoftU2FTool/UserPresence.swift +++ b/SoftU2FTool/UserPresence.swift @@ -37,8 +37,8 @@ class UserPresence: NSObject { // Display a notification, wait for the user to click it, and call the callback with `true`. // Calls callback with `false` if another test is done while we're waiting for this one. - static func test(_ type: Notification, with callback: @escaping Callback) { - if skip { + static func test(_ type: Notification, skip skipOnce: Bool = false, with callback: @escaping Callback) { + if skip || skipOnce { callback(true) } else { // Fail any outstanding test. From bcc7f1c082cc85fb760b89887baccb59353d62d0 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Fri, 18 Aug 2017 16:01:56 -0600 Subject: [PATCH 4/9] get SEP actually working --- SoftU2FTool/AppDelegate.swift | 2 + SoftU2FTool/KeyPair.swift | 82 +++++------------- SoftU2FTool/Keychain.swift | 134 ++++++++++++++++++----------- SoftU2FTool/U2FAuthenticator.swift | 3 +- SoftU2FTool/U2FRegistration.swift | 7 +- 5 files changed, 113 insertions(+), 115 deletions(-) diff --git a/SoftU2FTool/AppDelegate.swift b/SoftU2FTool/AppDelegate.swift index 728d863..95990f4 100644 --- a/SoftU2FTool/AppDelegate.swift +++ b/SoftU2FTool/AppDelegate.swift @@ -10,6 +10,8 @@ import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { + U2FRegistration.repair() + if CLI(CommandLine.arguments).run() { quit() } else if !U2FAuthenticator.start(){ diff --git a/SoftU2FTool/KeyPair.swift b/SoftU2FTool/KeyPair.swift index e1b45c1..88f9b03 100644 --- a/SoftU2FTool/KeyPair.swift +++ b/SoftU2FTool/KeyPair.swift @@ -7,73 +7,31 @@ import Foundation -fileprivate class tmpKeyPair { - var label: String - var applicationLabel: Data - var publicKey: SecKey? - var privateKey: SecKey? - - var keyPair: KeyPair? { - guard let pub = publicKey else { return nil } - guard let priv = privateKey else { return nil } - - return KeyPair(label: label, appLabel: applicationLabel, publicKey: pub, privateKey: priv) - } - - init(label l: String, applicationLabel al: Data) { - label = l - applicationLabel = al - } -} - class KeyPair { + static func repair(label: String) { + Keychain.repair(attrLabel: label as CFString) + } + /// Get all KeyPairs with the given label. static func all(label: String) -> [KeyPair] { - let secKeys = Keychain.getSecKeys(attrLabel: label as CFString) - var tmpKeyPairs: [tmpKeyPair] = [] + let secKeys = Keychain.getPrivateSecKeys(attrLabel: label as CFString) var keyPairs: [KeyPair] = [] - secKeys.forEach { secKey in - guard let appLabel: CFData = Keychain.getSecKeyAttr(key: secKey, attr: kSecAttrApplicationLabel) else { return } - - let applicationLabel = appLabel as Data - var tmpKP: tmpKeyPair - - if let kp = tmpKeyPairs.first(where: { $0.applicationLabel == applicationLabel }) { - tmpKP = kp - } else { - tmpKP = tmpKeyPair.init(label: label, applicationLabel: applicationLabel) - tmpKeyPairs.append(tmpKP) - } - - guard let klass: CFString = Keychain.getSecKeyAttr(key: secKey, attr: kSecAttrKeyClass) else { return } - - if klass == kSecAttrKeyClassPublic { - tmpKP.publicKey = secKey - } else { - tmpKP.privateKey = secKey - } - } - - tmpKeyPairs.forEach { tmpKP in - guard let kp = tmpKP.keyPair else { return } - + secKeys.forEach { priv in + guard let pub = SecKeyCopyPublicKey(priv) else { return } + guard let appLabel: CFData = Keychain.getSecKeyAttr(key: priv, attr: kSecAttrApplicationLabel) else { return } + + let kp = KeyPair(label: label, appLabel: appLabel as Data, publicKey: pub, privateKey: priv) + keyPairs.append(kp) } return keyPairs } - // The number of key pairs (keys/2) in the keychain. + // The number of private keys in the keychain. static func count(label: String) -> Int? { - guard let c = Keychain.count(attrLabel: label as CFString) else { return nil } - - if c % 2 != 0 { - print("Uneven number of keys in the keychain.") - return nil - } - - return c / 2 + return Keychain.count(attrLabel: label as CFString) } // Delete all keys with the given label from the keychain. @@ -97,12 +55,12 @@ class KeyPair { set { let value = (newValue ?? Data()) - Keychain.setSecItemAttr(attrAppLabel: applicationLabel as CFData, name: kSecAttrApplicationTag, value: value as CFData) + _ = Keychain.setSecItemAttr(attrAppLabel: applicationLabel as CFData, name: kSecAttrApplicationTag, value: value as CFData) } } var publicKeyData: Data? { - return Keychain.getSecItemData(attrAppLabel: applicationLabel) + return Keychain.exportSecKey(publicKey) } var inSEP: Bool { @@ -128,13 +86,13 @@ class KeyPair { label = l applicationLabel = al - // Lookup public key. - guard let pub = Keychain.getSecKey(attrAppLabel: applicationLabel as CFData, keyClass: kSecAttrKeyClassPublic) else { return nil } - publicKey = pub - // Lookup private key. - guard let priv = Keychain.getSecKey(attrAppLabel: applicationLabel as CFData, keyClass: kSecAttrKeyClassPrivate) else { return nil } + guard let priv = Keychain.getPrivateSecKey(attrAppLabel: applicationLabel as CFData) else { return nil } privateKey = priv + + // Generate public key from private key + guard let pub = SecKeyCopyPublicKey(priv) else { return nil } + publicKey = pub } // Initialize a key pair with all the necessary data. diff --git a/SoftU2FTool/Keychain.swift b/SoftU2FTool/Keychain.swift index 578bf25..63d8586 100644 --- a/SoftU2FTool/Keychain.swift +++ b/SoftU2FTool/Keychain.swift @@ -75,7 +75,7 @@ class Keychain { let query = makeCFDictionary( (kSecClass, kSecClassKey), (kSecAttrKeyType, kSecAttrKeyTypeEC), - (kSecAttrKeyClass, kSecAttrKeyClassPublic), + (kSecAttrKeyClass, kSecAttrKeyClassPrivate), (kSecAttrApplicationLabel, attrAppLabel), (kSecReturnAttributes, kCFBooleanTrue) ) @@ -106,11 +106,11 @@ class Keychain { } // Get the given attribute for the SecItem with the given kSecAttrApplicationLabel. - static func setSecItemAttr(attrAppLabel: CFData, name: CFString, value: T) { + static func setSecItemAttr(attrAppLabel: CFData, name: CFString, value: T) -> Bool { let query = makeCFDictionary( (kSecClass, kSecClassKey), (kSecAttrKeyType, kSecAttrKeyTypeEC), - (kSecAttrKeyClass, kSecAttrKeyClassPublic), + (kSecAttrKeyClass, kSecAttrKeyClassPrivate), (kSecAttrApplicationLabel, attrAppLabel) ) @@ -122,40 +122,31 @@ class Keychain { if err != errSecSuccess { print("Error from keychain: \(err)") + return false } + + return true } - - // Get the raw data for the SecItem with the given kSecAttrApplicationLabel. - static func getSecItemData(attrAppLabel: Data) -> Data? { - let query = makeCFDictionary( - (kSecClass, kSecClassKey), - (kSecAttrKeyType, kSecAttrKeyTypeEC), - (kSecAttrKeyClass, kSecAttrKeyClassPublic), - (kSecAttrApplicationLabel, attrAppLabel as CFData), - (kSecReturnData, kCFBooleanTrue) - ) - - var optionalOpaqueResult: CFTypeRef? = nil - let err = SecItemCopyMatching(query, &optionalOpaqueResult) - - if err != errSecSuccess { - print("Error from keychain: \(err)") - return nil - } - - guard let opaqueResult = optionalOpaqueResult else { - print("Unexpected nil returned from keychain") + + // Get the raw data from the key. + static func exportSecKey(_ key: SecKey) -> Data? { + var err: Unmanaged? = nil + let data = SecKeyCopyExternalRepresentation(key, &err) + + if err != nil { + print("Error exporting key") return nil } - - return opaqueResult as! CFData as Data + + return data as Data? } - // Lookup all keys with the given label. - static func getSecKeys(attrLabel: CFString) -> [SecKey] { + // Lookup all private keys with the given label. + static func getPrivateSecKeys(attrLabel: CFString) -> [SecKey] { let query = makeCFDictionary( (kSecClass, kSecClassKey), (kSecAttrKeyType, kSecAttrKeyTypeEC), + (kSecAttrKeyClass, kSecAttrKeyClassPrivate), (kSecAttrLabel, attrLabel), (kSecReturnRef, kCFBooleanTrue), (kSecMatchLimit, 1000 as CFNumber) @@ -179,12 +170,11 @@ class Keychain { return result } - static func getSecKey(attrAppLabel: CFData, keyClass: CFString) -> SecKey? { - // Lookup public key. + static func getPrivateSecKey(attrAppLabel: CFData) -> SecKey? { let query = makeCFDictionary( (kSecClass, kSecClassKey), (kSecAttrKeyType, kSecAttrKeyTypeEC), - (kSecAttrKeyClass, keyClass), + (kSecAttrKeyClass, kSecAttrKeyClassPrivate), (kSecAttrApplicationLabel, attrAppLabel), (kSecReturnRef, kCFBooleanTrue) ) @@ -258,7 +248,7 @@ class Keychain { // Generate key pair. var pub: SecKey? = nil var priv: SecKey? = nil - var status = SecKeyGeneratePair(params, &pub, &priv) + let status = SecKeyGeneratePair(params, &pub, &priv) if status != errSecSuccess { print("Error calling SecKeyGeneratePair: \(status)") @@ -269,22 +259,6 @@ class Keychain { print("Keys not returned from SecKeyGeneratePair") return nil } - - // We can't make the public key permanent during generation with - // SEP, so we have to save the public key as a separate step. - let addParams = makeCFDictionary( - (kSecClass, kSecClassKey), - (kSecAttrKeyType, kSecAttrKeyTypeEC), - (kSecAttrKeyClass, kSecAttrKeyClassPublic), - (kSecAttrLabel, attrLabel), - (kSecValueRef, pub!) - ) - - status = SecItemAdd(addParams, nil) - if status != errSecSuccess { - print("Error calling SecItemAdd: \(status)") - return nil - } return (pub!, priv!) } @@ -318,4 +292,68 @@ class Keychain { return ret } + + static func repair(attrLabel: CFString) { + // Lookup public keys + let query = makeCFDictionary( + (kSecClass, kSecClassKey), + (kSecAttrKeyType, kSecAttrKeyTypeEC), + (kSecAttrKeyClass, kSecAttrKeyClassPublic), + (kSecAttrLabel, attrLabel), + (kSecReturnAttributes, kCFBooleanTrue), + (kSecMatchLimit, 100 as CFNumber) + ) + + var optionalOpaqueResult: CFTypeRef? = nil + let err = SecItemCopyMatching(query, &optionalOpaqueResult) + + if err != errSecSuccess { + print("Error from keychain: \(err)") + return + } + + guard let opaqueResult = optionalOpaqueResult else { + print("Unexpected nil returned from keychain") + return + } + + let publicKeys = opaqueResult as! [[String:AnyObject]] + + publicKeys.forEach { publicKey in + print("Repairing one keypair") + + guard let attrAppTag = publicKey[kSecAttrApplicationTag as String] as? Data else { + print("error getting kSecAttrApplicationTag for public key") + return + } + + guard let attrAppLabel = publicKey[kSecAttrApplicationLabel as String] as? Data else { + print("error getting kSecAttrApplicationLabel for public key") + return + } + + guard let _ = getPrivateSecKey(attrAppLabel: attrAppLabel as CFData) else { + print("error getting private key for public key") + return + } + + if !setSecItemAttr(attrAppLabel: attrAppLabel as CFData, name: kSecAttrApplicationTag, value: attrAppTag as CFData) { + print("Error copying kSecAttrApplicationTag to private key") + return + } + + let ok = delete( + (kSecClass, kSecClassKey), + (kSecAttrKeyClass, kSecAttrKeyClassPublic), + (kSecAttrApplicationLabel, attrAppLabel as CFData) + ) + + if !ok { + print("Error deleting public keys") + return + } + + print("Success") + } + } } diff --git a/SoftU2FTool/U2FAuthenticator.swift b/SoftU2FTool/U2FAuthenticator.swift index f9ba65d..5bb139a 100644 --- a/SoftU2FTool/U2FAuthenticator.swift +++ b/SoftU2FTool/U2FAuthenticator.swift @@ -90,9 +90,8 @@ class U2FAuthenticator { let facet = KnownFacets[req.applicationParameter] let notification = UserPresence.Notification.Register(facet: facet) - let skipTUP = Settings.sepEnabled - UserPresence.test(notification, skip: skipTUP) { tupSuccess in + UserPresence.test(notification) { tupSuccess in if !tupSuccess { // Send no response. Otherwise Chrome will re-prompt immediately. return diff --git a/SoftU2FTool/U2FRegistration.swift b/SoftU2FTool/U2FRegistration.swift index 53f95b8..8c05210 100644 --- a/SoftU2FTool/U2FRegistration.swift +++ b/SoftU2FTool/U2FRegistration.swift @@ -31,6 +31,10 @@ class U2FRegistration { static var count: Int? { return KeyPair.count(label: namespace) } + + static func repair() { + KeyPair.repair(label: namespace) + } // Delete all SoftU2F keys from keychain. static func deleteAll() -> Bool { @@ -124,9 +128,6 @@ class U2FRegistration { writeApplicationTag() } - func readApplicationTag(appTag: Data?) { - } - // Persist the applicationParameter and counter in the keychain. func writeApplicationTag() { let counterSize = MemoryLayout.size From 4085c78c5c6028dd7b4cf994ba6747cdbfc444fe Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Fri, 18 Aug 2017 16:04:26 -0600 Subject: [PATCH 5/9] remove TouchID support without SEP --- SoftU2FTool/CLI.swift | 33 --------------------------------- SoftU2FTool/Settings.swift | 24 +++--------------------- SoftU2FTool/UserPresence.swift | 34 ---------------------------------- 3 files changed, 3 insertions(+), 88 deletions(-) diff --git a/SoftU2FTool/CLI.swift b/SoftU2FTool/CLI.swift index 3b91ce7..d9f0266 100644 --- a/SoftU2FTool/CLI.swift +++ b/SoftU2FTool/CLI.swift @@ -13,9 +13,6 @@ fileprivate let deleteAllFlag = "--delete-all" fileprivate let showSEPFlag = "--show-sep" fileprivate let enableSEPFlag = "--enable-sep" fileprivate let disableSEPFlag = "--disable-sep" -fileprivate let showTouchidFlag = "--show-touchid" -fileprivate let enableTouchidFlag = "--enable-touchid" -fileprivate let disableTouchidFlag = "--disable-touchid" class CLI { private let args: [String] @@ -31,15 +28,6 @@ class CLI { } else if args.contains(deleteAllFlag) { deleteAll() return true - } else if args.contains(showTouchidFlag) { - showTouchid() - return true - } else if args.contains(enableTouchidFlag) { - enableTouchid() - return true - } else if args.contains(disableTouchidFlag) { - disableTouchid() - return true } else if args.contains(showSEPFlag) { showSEP() return true @@ -93,27 +81,6 @@ class CLI { print("Deleted ", initialCount, " registrations") } - private func showTouchid() { - if Settings.touchidDisabled { - print("TouchID is disabled") - } else { - print("TouchID is enabled") - } - } - - private func enableTouchid() { - if Settings.enableTouchid() { - print("TouchID is now enabled") - } else { - print("Error enabling TouchID. Does your system support it?") - } - } - - private func disableTouchid() { - Settings.disableTouchid() - print("TouchID is now disabled") - } - private func showSEP() { if Settings.sepEnabled { print("SEP storage is enabled for new keys") diff --git a/SoftU2FTool/Settings.swift b/SoftU2FTool/Settings.swift index 4849573..f65b305 100644 --- a/SoftU2FTool/Settings.swift +++ b/SoftU2FTool/Settings.swift @@ -9,18 +9,13 @@ import Foundation import LocalAuthentication class Settings { - private static let touchidDisabledKey = "touchidDisabled" private static let sepEnabledKey = "sepEnabled" - static var touchidDisabled: Bool { - return !touchidAvailable || UserDefaults.standard.bool(forKey: touchidDisabledKey) - } - static var sepEnabled: Bool { - return touchidAvailable && UserDefaults.standard.bool(forKey: sepEnabledKey) + return sepAvailable && UserDefaults.standard.bool(forKey: sepEnabledKey) } - private static var touchidAvailable: Bool { + private static var sepAvailable: Bool { if #available(OSX 10.12.2, *) { return LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) } else { @@ -28,21 +23,8 @@ class Settings { } } - static func enableTouchid() -> Bool { - if touchidAvailable { - UserDefaults.standard.set(false, forKey: touchidDisabledKey) - return true - } else { - return false - } - } - - static func disableTouchid() { - UserDefaults.standard.set(true, forKey: touchidDisabledKey) - } - static func enableSEP() -> Bool { - if touchidAvailable { + if sepAvailable { UserDefaults.standard.set(true, forKey: sepEnabledKey) return true } else { diff --git a/SoftU2FTool/UserPresence.swift b/SoftU2FTool/UserPresence.swift index 3c839cf..40f4ff6 100644 --- a/SoftU2FTool/UserPresence.swift +++ b/SoftU2FTool/UserPresence.swift @@ -79,40 +79,6 @@ class UserPresence: NSObject { // Send a notification popup to the user. func test(_ type: Notification) { - if #available(OSX 10.12.2, *) { - if !Settings.touchidDisabled { - let ctx = LAContext() - - if ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { - ctx.localizedCancelTitle = "Reject" - ctx.localizedFallbackTitle = "Skip TouchID" - - var prompt: String - switch type { - case let .Register(facet): - prompt = "register with " + (facet ?? "site") - case let .Authenticate(facet): - prompt = "authenticate with " + (facet ?? "site") - } - - ctx.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: prompt) { (success, err) in - guard let lerr = err as? LAError else { - self.complete(success) - return - } - - switch lerr.code { - case .userFallback, .touchIDNotAvailable, .touchIDNotEnrolled: - self.sendNotification(type) - default: - self.complete(false) - } - } - return - } - } - } - sendNotification(type) } From 5bc1c31c63f940cf471cae6235fa39f9e4ef9cdd Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Fri, 18 Aug 2017 16:39:50 -0600 Subject: [PATCH 6/9] use known facets in prompts --- SoftU2FTool/KeyPair.swift | 4 ++-- SoftU2FTool/Keychain.swift | 15 ++++++++------- SoftU2FTool/U2FRegistration.swift | 6 +++++- SoftU2FTool/UserPresence.swift | 1 - 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/SoftU2FTool/KeyPair.swift b/SoftU2FTool/KeyPair.swift index 88f9b03..10fda6d 100644 --- a/SoftU2FTool/KeyPair.swift +++ b/SoftU2FTool/KeyPair.swift @@ -82,12 +82,12 @@ class KeyPair { } // Find a key pair with the given label and application label. - init?(label l: String, appLabel al: Data) { + init?(label l: String, appLabel al: Data, signPrompt sp: String) { label = l applicationLabel = al // Lookup private key. - guard let priv = Keychain.getPrivateSecKey(attrAppLabel: applicationLabel as CFData) else { return nil } + guard let priv = Keychain.getPrivateSecKey(attrAppLabel: applicationLabel as CFData, signPrompt: sp as CFString) else { return nil } privateKey = priv // Generate public key from private key diff --git a/SoftU2FTool/Keychain.swift b/SoftU2FTool/Keychain.swift index 63d8586..e62c6e9 100644 --- a/SoftU2FTool/Keychain.swift +++ b/SoftU2FTool/Keychain.swift @@ -170,13 +170,14 @@ class Keychain { return result } - static func getPrivateSecKey(attrAppLabel: CFData) -> SecKey? { + static func getPrivateSecKey(attrAppLabel: CFData, signPrompt: CFString) -> SecKey? { let query = makeCFDictionary( - (kSecClass, kSecClassKey), - (kSecAttrKeyType, kSecAttrKeyTypeEC), - (kSecAttrKeyClass, kSecAttrKeyClassPrivate), - (kSecAttrApplicationLabel, attrAppLabel), - (kSecReturnRef, kCFBooleanTrue) + (kSecClass, kSecClassKey), + (kSecAttrKeyType, kSecAttrKeyTypeEC), + (kSecAttrKeyClass, kSecAttrKeyClassPrivate), + (kSecAttrApplicationLabel, attrAppLabel), + (kSecReturnRef, kCFBooleanTrue), + (kSecUseOperationPrompt, signPrompt as CFString) ) var optionalOpaqueResult: CFTypeRef? = nil @@ -332,7 +333,7 @@ class Keychain { return } - guard let _ = getPrivateSecKey(attrAppLabel: attrAppLabel as CFData) else { + guard let _ = getPrivateSecKey(attrAppLabel: attrAppLabel as CFData, signPrompt: "" as CFString) else { print("error getting private key for public key") return } diff --git a/SoftU2FTool/U2FRegistration.swift b/SoftU2FTool/U2FRegistration.swift index 8c05210..dfb6485 100644 --- a/SoftU2FTool/U2FRegistration.swift +++ b/SoftU2FTool/U2FRegistration.swift @@ -68,7 +68,11 @@ class U2FRegistration { // Find a registration with the given key handle. init?(keyHandle kh: Data, applicationParameter ap: Data) { let appLabel = unpadKeyHandle(kh) - guard let kp = KeyPair(label: U2FRegistration.namespace, appLabel: appLabel) else { return nil } + + let kf = KnownFacets[ap] ?? "site" + let prompt = "authenticate with \(kf)" + + guard let kp = KeyPair(label: U2FRegistration.namespace, appLabel: appLabel, signPrompt: prompt) else { return nil } keyPair = kp // Read our application parameter from the keychain and make sure it matches. diff --git a/SoftU2FTool/UserPresence.swift b/SoftU2FTool/UserPresence.swift index 40f4ff6..5e694eb 100644 --- a/SoftU2FTool/UserPresence.swift +++ b/SoftU2FTool/UserPresence.swift @@ -6,7 +6,6 @@ // import Foundation -import LocalAuthentication class UserPresence: NSObject { enum Notification { From 48686c01edcbb836bc74f9bc9aa8e58027a00a80 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Fri, 18 Aug 2017 16:43:39 -0600 Subject: [PATCH 7/9] fix test --- SoftU2FToolTests/U2FRegistrationTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftU2FToolTests/U2FRegistrationTests.swift b/SoftU2FToolTests/U2FRegistrationTests.swift index 75d21c4..a46668b 100644 --- a/SoftU2FToolTests/U2FRegistrationTests.swift +++ b/SoftU2FToolTests/U2FRegistrationTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import SoftU2F class U2FRegistrationTests: SoftU2FTestCase { - var makeKey: U2FRegistration? { return U2FRegistration(applicationParameter: randData(length: 32)) } + var makeKey: U2FRegistration? { return U2FRegistration(applicationParameter: randData(length: 32), inSEP: false) } override func tearDown() { let _ = U2FRegistration.deleteAll() From e6121ad799796fab6f6afdecc0ec657fa2936b52 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Tue, 22 Aug 2017 09:49:02 -0600 Subject: [PATCH 8/9] switch ACL too .touchIDAny --- SoftU2FTool/Keychain.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SoftU2FTool/Keychain.swift b/SoftU2FTool/Keychain.swift index e62c6e9..6defb3d 100644 --- a/SoftU2FTool/Keychain.swift +++ b/SoftU2FTool/Keychain.swift @@ -206,7 +206,7 @@ class Keychain { if inSEP { if #available(OSX 10.12.1, *) { - acl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, [.privateKeyUsage, .touchIDCurrentSet], &err) + acl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlocked, [.privateKeyUsage, .touchIDAny], &err) } else { print("Cannot generate keys in SEP on macOS<10.12.1") return nil @@ -231,7 +231,8 @@ class Keychain { (kSecPrivateKeyAttrs, makeCFDictionary( (kSecAttrAccessControl, acl!), (kSecAttrLabel, attrLabel), - (kSecAttrIsPermanent, kCFBooleanTrue) + (kSecAttrIsPermanent, kCFBooleanTrue), + (kSecAttrSynchronizable, kCFBooleanFalse) )) ) } else { From 5babfb7eb7ecc9dbcbd19b9b519538c1d6cb7c39 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Tue, 22 Aug 2017 10:10:15 -0600 Subject: [PATCH 9/9] cleanup/comments --- SoftU2FTool/AppDelegate.swift | 3 +- SoftU2FTool/CLI.swift | 8 ++++- SoftU2FTool/KeyPair.swift | 1 + SoftU2FTool/Keychain.swift | 57 +++++++++++++++++++------------ SoftU2FTool/U2FRegistration.swift | 23 +++++++------ 5 files changed, 58 insertions(+), 34 deletions(-) diff --git a/SoftU2FTool/AppDelegate.swift b/SoftU2FTool/AppDelegate.swift index 95990f4..4f73b1a 100644 --- a/SoftU2FTool/AppDelegate.swift +++ b/SoftU2FTool/AppDelegate.swift @@ -10,6 +10,7 @@ import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { + // Fix up legacy keychain items. U2FRegistration.repair() if CLI(CommandLine.arguments).run() { @@ -27,7 +28,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationDidBecomeActive(_ notification: Notification) { - // Chrome gives ignores our U2F responses if it isn't active when we send them. + // Chrome ignores our U2F responses if it isn't active when we send them. // This hack should give focus back to Chrome immediately after the user interacts // with our notification. NSApplication.shared().hide(nil) diff --git a/SoftU2FTool/CLI.swift b/SoftU2FTool/CLI.swift index d9f0266..1e4017a 100644 --- a/SoftU2FTool/CLI.swift +++ b/SoftU2FTool/CLI.swift @@ -43,6 +43,12 @@ class CLI { } private func listRegistrations() { + let registrations = U2FRegistration.all + if registrations.count == 0 { + print("No registrations to list") + return + } + print("The following is a list of U2F registrations stored in your keychain. Each key contains several fields:") print(" - Key handle: This is the key handle that we registered with a website. For Soft U2F, the key handle is simply a hash of the public key.") print(" - Application parameter: This is the sha256 of the app-id of the site.") @@ -51,7 +57,7 @@ class CLI { print(" — In SEP: Whether this registration's private key is stored in the SEP.") print("") - U2FRegistration.all.forEach { reg in + registrations.forEach { reg in print("Key handle: ", reg.keyHandle.base64EncodedString()) print("Application parameter: ", reg.applicationParameter.base64EncodedString()) diff --git a/SoftU2FTool/KeyPair.swift b/SoftU2FTool/KeyPair.swift index 10fda6d..7a77cfd 100644 --- a/SoftU2FTool/KeyPair.swift +++ b/SoftU2FTool/KeyPair.swift @@ -8,6 +8,7 @@ import Foundation class KeyPair { + // Fix up legacy keychain items. static func repair(label: String) { Keychain.repair(attrLabel: label as CFString) } diff --git a/SoftU2FTool/Keychain.swift b/SoftU2FTool/Keychain.swift index 6defb3d..10798cd 100644 --- a/SoftU2FTool/Keychain.swift +++ b/SoftU2FTool/Keychain.swift @@ -124,23 +124,23 @@ class Keychain { print("Error from keychain: \(err)") return false } - + return true } - + // Get the raw data from the key. static func exportSecKey(_ key: SecKey) -> Data? { var err: Unmanaged? = nil let data = SecKeyCopyExternalRepresentation(key, &err) - + if err != nil { print("Error exporting key") return nil } - + return data as Data? } - + // Lookup all private keys with the given label. static func getPrivateSecKeys(attrLabel: CFString) -> [SecKey] { let query = makeCFDictionary( @@ -151,22 +151,26 @@ class Keychain { (kSecReturnRef, kCFBooleanTrue), (kSecMatchLimit, 1000 as CFNumber) ) - + var optionalOpaqueResult: CFTypeRef? = nil let err = SecItemCopyMatching(query, &optionalOpaqueResult) - + + if err == errSecItemNotFound { + return [] + } + if err != errSecSuccess { print("Error from keychain: \(err)") return [] } - + guard let opaqueResult = optionalOpaqueResult else { print("Unexpected nil returned from keychain") return [] } - + let result = opaqueResult as! [SecKey] - + return result } @@ -294,9 +298,16 @@ class Keychain { return ret } - + + // Previously, we had been storing both the public and private key in the keychain and + // using the application tag attribute on the public key for smuggling the U2F + // registration's counter. When generating a private key in the SEP, the public key + // isn't persisted in the keychain. From now on, we're using the application tag + // attribute on the private key for storing the counter and just deriving the public + // key from the private key whenever we need it. This function makes legacy keys + // consistent by deleting the public key from the keychain and copying its application + // tag into the private key. static func repair(attrLabel: CFString) { - // Lookup public keys let query = makeCFDictionary( (kSecClass, kSecClassKey), (kSecAttrKeyType, kSecAttrKeyTypeEC), @@ -305,20 +316,24 @@ class Keychain { (kSecReturnAttributes, kCFBooleanTrue), (kSecMatchLimit, 100 as CFNumber) ) - + var optionalOpaqueResult: CFTypeRef? = nil let err = SecItemCopyMatching(query, &optionalOpaqueResult) - + + if err == errSecItemNotFound { + return + } + if err != errSecSuccess { print("Error from keychain: \(err)") return } - + guard let opaqueResult = optionalOpaqueResult else { print("Unexpected nil returned from keychain") return } - + let publicKeys = opaqueResult as! [[String:AnyObject]] publicKeys.forEach { publicKey in @@ -328,12 +343,12 @@ class Keychain { print("error getting kSecAttrApplicationTag for public key") return } - + guard let attrAppLabel = publicKey[kSecAttrApplicationLabel as String] as? Data else { print("error getting kSecAttrApplicationLabel for public key") return } - + guard let _ = getPrivateSecKey(attrAppLabel: attrAppLabel as CFData, signPrompt: "" as CFString) else { print("error getting private key for public key") return @@ -343,18 +358,18 @@ class Keychain { print("Error copying kSecAttrApplicationTag to private key") return } - + let ok = delete( (kSecClass, kSecClassKey), (kSecAttrKeyClass, kSecAttrKeyClassPublic), (kSecAttrApplicationLabel, attrAppLabel as CFData) ) - + if !ok { print("Error deleting public keys") return } - + print("Success") } } diff --git a/SoftU2FTool/U2FRegistration.swift b/SoftU2FTool/U2FRegistration.swift index dfb6485..07f4b0e 100644 --- a/SoftU2FTool/U2FRegistration.swift +++ b/SoftU2FTool/U2FRegistration.swift @@ -10,11 +10,11 @@ import Foundation class U2FRegistration { // Allow using separate keychain namespace for tests. static var namespace = "SoftU2F Security Key" - + static var all: [U2FRegistration] { let kps = KeyPair.all(label: namespace) var regs: [U2FRegistration] = [] - + kps.forEach { kp in guard let reg = U2FRegistration(keyPair: kp) else { print("Error initializing U2FRegistration") @@ -23,7 +23,7 @@ class U2FRegistration { regs.append(reg) } - + return regs } @@ -31,7 +31,8 @@ class U2FRegistration { static var count: Int? { return KeyPair.count(label: namespace) } - + + // Fix up legacy keychain items. static func repair() { KeyPair.repair(label: namespace) } @@ -49,7 +50,7 @@ class U2FRegistration { var keyHandle: Data { return padKeyHandle(keyPair.applicationLabel) } - + var inSEP: Bool { return keyPair.inSEP } @@ -96,25 +97,25 @@ class U2FRegistration { return nil } } - + // Initialize a registration with all the necessary data. init?(keyPair kp: KeyPair) { keyPair = kp - + // Read our application parameter from the keychain. guard let appTag = keyPair.applicationTag else { return nil } - + let counterSize = MemoryLayout.size let appTagSize = Int(U2F_APPID_SIZE) - + if appTag.count != counterSize + appTagSize { return nil } - + counter = appTag.withUnsafeBytes { (ptr:UnsafePointer) -> UInt32 in return ptr.pointee.bigEndian } - + applicationParameter = appTag.subdata(in: counterSize..<(counterSize + appTagSize)) }