소스 검색

Add touch ID authentication support

kishikawa katsumi 10 년 전
부모
커밋
37bee397a4
2개의 변경된 파일293개의 추가작업 그리고 108개의 파일을 삭제
  1. 193 104
      Lib/KeychainAccess/Keychain.swift
  2. 100 4
      README.md

+ 193 - 104
Lib/KeychainAccess/Keychain.swift

@@ -9,6 +9,8 @@
 import Foundation
 import Security
 
+public let KeychainAccessErrorDomain = "com.kishikawakatsumi.KeychainAccess.error"
+
 public enum ItemClass {
     case GenericPassword
     case InternetPassword
@@ -69,6 +71,10 @@ public enum Accessibility {
     case AlwaysThisDeviceOnly
 }
 
+public enum AuthenticationPolicy : Int {
+    case UserPresence
+}
+
 public enum FailableOf<T> {
     case Success(Value<T?>)
     case Failure(NSError)
@@ -81,9 +87,18 @@ public enum FailableOf<T> {
         self = .Failure(error)
     }
     
+    public var successed: Bool {
+        switch self {
+        case .Success:
+            return true
+        default:
+            return false
+        }
+    }
+    
     public var failed: Bool {
         switch self {
-        case .Failure(let error):
+        case .Failure:
             return true
         default:
             return false
@@ -155,10 +170,6 @@ public class Keychain {
         return options.itemClass
     }
     
-    private class var errorDomain: String {
-        return "KeychainAccess"
-    }
-    
     private let options: Options
     
     // MARK:
@@ -218,6 +229,13 @@ public class Keychain {
         return Keychain(options)
     }
     
+    public func accessibility(accessibility: Accessibility, authenticationPolicy: AuthenticationPolicy) -> Keychain {
+        var options = self.options
+        options.accessibility = accessibility
+        options.authenticationPolicy = authenticationPolicy
+        return Keychain(options)
+    }
+    
     public func synchronizable(synchronizable: Bool) -> Keychain {
         var options = self.options
         options.synchronizable = synchronizable
@@ -236,6 +254,14 @@ public class Keychain {
         return Keychain(options)
     }
     
+    #if os(iOS)
+    public func authenticationPrompt(authenticationPrompt: String) -> Keychain {
+        var options = self.options
+        options.authenticationPrompt = authenticationPrompt
+        return Keychain(options)
+    }
+    #endif
+    
     // MARK:
     
     public func get(key: String) -> String? {
@@ -271,6 +297,7 @@ public class Keychain {
     
     public func getDataOrError(key: String) -> FailableOf<NSData> {
         var query = options.query()
+        
         query[kSecMatchLimit] = kSecMatchLimitOne
         query[kSecReturnData] = kCFBooleanTrue
         
@@ -304,23 +331,38 @@ public class Keychain {
     
     public func set(value: NSData, key: String) -> NSError? {
         var query = options.query()
+        
         query[kSecAttrAccount] = key
+        #if os(iOS)
+        query[kSecUseNoAuthenticationUI] = kCFBooleanTrue
+        #endif
         
         var status = SecItemCopyMatching(query, nil)
         switch status {
-        case errSecSuccess:
-            var attributes = options.attributes(value: value)
+        case errSecSuccess, errSecInteractionNotAllowed:
+            var query = options.query()
+            query[kSecAttrAccount] = key
             
-            status = SecItemUpdate(query, attributes)
-            if status != errSecSuccess {
-                return securityError(status: status)
+            var (attributes, error) = options.attributes(key: nil, value: value)
+            if var error = error {
+                println("error:[\(error.code)] \(error.localizedDescription)")
+                return error
+            } else {
+                status = SecItemUpdate(query, attributes)
+                if status != errSecSuccess {
+                    return securityError(status: status)
+                }
             }
         case errSecItemNotFound:
-            var attributes = options.attributes(key: key, value: value)
-            
-            status = SecItemAdd(attributes, nil)
-            if status != errSecSuccess {
-                return securityError(status: status)
+            var (attributes, error) = options.attributes(key: key, value: value)
+            if var error = error {
+                println("error:[\(error.code)] \(error.localizedDescription)")
+                return error
+            } else {
+                status = SecItemAdd(attributes, nil)
+                if status != errSecSuccess {
+                    return securityError(status: status)
+                }
             }
         default:
             return securityError(status: status)
@@ -540,8 +582,8 @@ public class Keychain {
     // MARK:
     
     private class func conversionError(#message: String) -> NSError {
-        let error = NSError(domain: errorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey: message])
-        log(error)
+        let error = NSError(domain: KeychainAccessErrorDomain, code: Int(Status.ConversionError.rawValue), userInfo: [NSLocalizedDescriptionKey: message])
+        println("error:[\(error.code)] \(error.localizedDescription)")
         
         return error
     }
@@ -553,8 +595,8 @@ public class Keychain {
     private class func securityError(#status: OSStatus) -> NSError {
         let message = Status(rawValue: status).description
         
-        let error = NSError(domain: errorDomain, code: Int(status), userInfo: [NSLocalizedDescriptionKey: message])
-        log(error)
+        let error = NSError(domain: KeychainAccessErrorDomain, code: Int(status), userInfo: [NSLocalizedDescriptionKey: message])
+        println("OSStatus error:[\(error.code)] \(error.localizedDescription)")
         
         return error
     }
@@ -562,14 +604,6 @@ public class Keychain {
     private func securityError(#status: OSStatus) -> NSError {
         return self.dynamicType.securityError(status: status)
     }
-    
-    private class func log(error: NSError) {
-        println("OSStatus error:[\(error.code)] \(error.localizedDescription)")
-    }
-    
-    private func log(error: NSError) {
-        self.dynamicType.log(error)
-    }
 }
 
 struct Options {
@@ -583,11 +617,15 @@ struct Options {
     var authenticationType: AuthenticationType = .Default
     
     var accessibility: Accessibility = .AfterFirstUnlock
+    var authenticationPolicy: AuthenticationPolicy?
+    
     var synchronizable: Bool = false
     
     var label: String?
     var comment: String?
     
+    var authenticationPrompt: String?
+    
     init() {}
 }
 
@@ -615,6 +653,7 @@ extension Options {
     
     func query() -> [String: AnyObject] {
         var query = [String: AnyObject]()
+        
         query[kSecClass] = itemClass.rawValue
         query[kSecAttrSynchronizable] = kSecAttrSynchronizableAny
         
@@ -633,17 +672,26 @@ extension Options {
             query[kSecAttrAuthenticationType] = authenticationType.rawValue
         }
         
+        #if os(iOS)
+        if authenticationPrompt != nil {
+            query[kSecUseOperationPrompt] = authenticationPrompt
+        }
+        #endif
+        
         return query
     }
     
-    func attributes(#key: String, value: NSData) -> [String: AnyObject] {
-        var attributes = query()
+    func attributes(#key: String?, value: NSData) -> ([String: AnyObject], NSError?) {
+        var attributes: [String: AnyObject]
         
-        attributes[kSecAttrAccount] = key
-        attributes[kSecValueData] = value
+        if key != nil {
+            attributes = query()
+            attributes[kSecAttrAccount] = key
+        } else {
+            attributes = [String: AnyObject]()
+        }
         
-        attributes[kSecAttrAccessible] = accessibility.rawValue
-        attributes[kSecAttrSynchronizable] = synchronizable
+        attributes[kSecValueData] = value
         
         if label != nil {
             attributes[kSecAttrLabel] = label
@@ -652,18 +700,33 @@ extension Options {
             attributes[kSecAttrComment] = comment
         }
         
-        return attributes
-    }
-    
-    func attributes(#value: NSData) -> [String: AnyObject] {
-        var attributes = [String: AnyObject]()
-        
-        attributes[kSecValueData] = value
+        if let policy = authenticationPolicy {
+            var error: Unmanaged<CFError>?
+            let accessControl = SecAccessControlCreateWithFlags(
+                kCFAllocatorDefault,
+                accessibility.rawValue,
+                SecAccessControlCreateFlags(policy.rawValue),
+                &error
+            )
+            if let error = error?.takeUnretainedValue() {
+                var code = CFErrorGetCode(error)
+                var domain = CFErrorGetDomain(error)
+                var userInfo = CFErrorCopyUserInfo(error)
+                
+                return (attributes, NSError(domain: domain, code: code, userInfo: userInfo))
+            }
+            if accessControl == nil {
+                let message = Status.UnexpectedError.description
+                return (attributes, NSError(domain: KeychainAccessErrorDomain, code: Int(Status.UnexpectedError.rawValue), userInfo: [NSLocalizedDescriptionKey: message]))
+            }
+            attributes[kSecAttrAccessControl] = accessControl.takeUnretainedValue()
+        } else {
+            attributes[kSecAttrAccessible] = accessibility.rawValue
+        }
         
-        attributes[kSecAttrAccessible] = accessibility.rawValue
         attributes[kSecAttrSynchronizable] = synchronizable
         
-        return attributes
+        return (attributes, nil)
     }
 }
 
@@ -701,68 +764,6 @@ extension ItemClass : RawRepresentable, Printable {
     }
 }
 
-extension Accessibility : RawRepresentable, Printable {
-    
-    public init?(rawValue: String) {
-        switch rawValue {
-        case kSecAttrAccessibleWhenUnlocked:
-            self = WhenUnlocked
-        case kSecAttrAccessibleAfterFirstUnlock:
-            self = AfterFirstUnlock
-        case kSecAttrAccessibleAlways:
-            self = Always
-        case kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly:
-            self = WhenPasscodeSetThisDeviceOnly
-        case kSecAttrAccessibleWhenUnlockedThisDeviceOnly:
-            self = WhenUnlockedThisDeviceOnly
-        case kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
-            self = AfterFirstUnlockThisDeviceOnly
-        case kSecAttrAccessibleAlwaysThisDeviceOnly:
-            self = AlwaysThisDeviceOnly
-        default:
-            return nil
-        }
-    }
-    
-    public var rawValue: String {
-        switch self {
-        case WhenUnlocked:
-            return kSecAttrAccessibleWhenUnlocked
-        case AfterFirstUnlock:
-            return kSecAttrAccessibleAfterFirstUnlock
-        case Always:
-            return kSecAttrAccessibleAlways
-        case WhenPasscodeSetThisDeviceOnly:
-            return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
-        case WhenUnlockedThisDeviceOnly:
-            return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
-        case AfterFirstUnlockThisDeviceOnly:
-            return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
-        case AlwaysThisDeviceOnly:
-            return kSecAttrAccessibleAlwaysThisDeviceOnly
-        }
-    }
-    
-    public var description : String {
-        switch self {
-        case WhenUnlocked:
-            return "WhenUnlocked"
-        case AfterFirstUnlock:
-            return "AfterFirstUnlock"
-        case Always:
-            return "Always"
-        case WhenPasscodeSetThisDeviceOnly:
-            return "WhenPasscodeSetThisDeviceOnly"
-        case WhenUnlockedThisDeviceOnly:
-            return "WhenUnlockedThisDeviceOnly"
-        case AfterFirstUnlockThisDeviceOnly:
-            return "AfterFirstUnlockThisDeviceOnly"
-        case AlwaysThisDeviceOnly:
-            return "AlwaysThisDeviceOnly"
-        }
-    }
-}
-
 extension ProtocolType : RawRepresentable, Printable {
     
     public init?(rawValue: String) {
@@ -1037,6 +1038,94 @@ extension AuthenticationType : RawRepresentable, Printable {
     }
 }
 
+extension Accessibility : RawRepresentable, Printable {
+    
+    public init?(rawValue: String) {
+        switch rawValue {
+        case kSecAttrAccessibleWhenUnlocked:
+            self = WhenUnlocked
+        case kSecAttrAccessibleAfterFirstUnlock:
+            self = AfterFirstUnlock
+        case kSecAttrAccessibleAlways:
+            self = Always
+        case kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly:
+            self = WhenPasscodeSetThisDeviceOnly
+        case kSecAttrAccessibleWhenUnlockedThisDeviceOnly:
+            self = WhenUnlockedThisDeviceOnly
+        case kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
+            self = AfterFirstUnlockThisDeviceOnly
+        case kSecAttrAccessibleAlwaysThisDeviceOnly:
+            self = AlwaysThisDeviceOnly
+        default:
+            return nil
+        }
+    }
+    
+    public var rawValue: String {
+        switch self {
+        case WhenUnlocked:
+            return kSecAttrAccessibleWhenUnlocked
+        case AfterFirstUnlock:
+            return kSecAttrAccessibleAfterFirstUnlock
+        case Always:
+            return kSecAttrAccessibleAlways
+        case WhenPasscodeSetThisDeviceOnly:
+            return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
+        case WhenUnlockedThisDeviceOnly:
+            return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
+        case AfterFirstUnlockThisDeviceOnly:
+            return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+        case AlwaysThisDeviceOnly:
+            return kSecAttrAccessibleAlwaysThisDeviceOnly
+        }
+    }
+    
+    public var description : String {
+        switch self {
+        case WhenUnlocked:
+            return "WhenUnlocked"
+        case AfterFirstUnlock:
+            return "AfterFirstUnlock"
+        case Always:
+            return "Always"
+        case WhenPasscodeSetThisDeviceOnly:
+            return "WhenPasscodeSetThisDeviceOnly"
+        case WhenUnlockedThisDeviceOnly:
+            return "WhenUnlockedThisDeviceOnly"
+        case AfterFirstUnlockThisDeviceOnly:
+            return "AfterFirstUnlockThisDeviceOnly"
+        case AlwaysThisDeviceOnly:
+            return "AlwaysThisDeviceOnly"
+        }
+    }
+}
+
+extension AuthenticationPolicy : RawRepresentable, Printable {
+    
+    public init?(rawValue: Int) {
+        switch rawValue {
+        case SecAccessControlCreateFlags.UserPresence.rawValue:
+            self = UserPresence
+        default:
+            return nil
+        }
+    }
+    
+    public var rawValue: Int {
+        switch self {
+        case UserPresence:
+            return SecAccessControlCreateFlags.UserPresence.rawValue
+        }
+    }
+    
+    public var description : String {
+        switch self {
+        case UserPresence:
+            return "UserPresence"
+        }
+    }
+}
+
 public enum Status : OSStatus {
     case Success
     case Unimplemented

+ 100 - 4
README.md

@@ -9,6 +9,15 @@ KeychainAccess is a simple Swift wrapper for Keychain that works on iOS and OS X
 
 <img src="https://raw.githubusercontent.com/kishikawakatsumi/KeychainAccess/master/Screenshots/01.png" width="320px" />
 
+## Features
+
+- Simple interface
+- Support access group
+- [Support accessibility](#accessibility)
+- [Support iCloud sharing](#icloud_sharing)
+- **[Support TouchID and Keychain integration (iOS 8+)](#touch_id_integration)**
+- Works on both iOS & OS X
+
 ## Usage
 
 ##### :eyes: See also:  
@@ -183,7 +192,7 @@ let keychain = Keychain(service: "com.example.github-token")
     .accessibility(.AfterFirstUnlock)
 ```
 
-#### :closed_lock_with_key: Accessibility
+#### <a name="accessibility"> Accessibility
 
 ##### Default accessibility matches background application (=kSecAttrAccessibleAfterFirstUnlock)
 
@@ -226,20 +235,20 @@ keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
 ###### One-shot
 
 ```swift
-let keychain = Keychain(service: "Twitter")
+let keychain = Keychain(service: "com.example.github-token")
 
 keychain
     .accessibility(.WhenUnlocked)
     .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
 ```
 
-#### :closed_lock_with_key: Sharing Keychain items
+#### Sharing Keychain items
 
 ```swift
 let keychain = Keychain(service: "com.example.github-token", accessGroup: "12ABCD3E4F.shared")
 ```
 
-#### :closed_lock_with_key: Synchronizing Keychain items with iCloud
+#### <a name="icloud_sharing"> Synchronizing Keychain items with iCloud
 
 ###### Creating instance
 
@@ -260,6 +269,93 @@ keychain
     .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
 ```
 
+### <a name="touch_id_integration"> :fu: Touch ID integration
+
+** Any Operation that require authentication must be run in the background thread.
+If you run in the main thread, UI thread will lock for the system to try to display the authentication dialog.**
+
+#### :closed_lock_with_key: Adding a Touch ID protected item
+
+If you want to store the Touch ID protected Keychain item, specify `accessibility` and `authenticationPolicy` attributes.  
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
+    let error = keychain
+        .accessibility(.WhenPasscodeSetThisDeviceOnly, authenticationPolicy: .UserPresence)
+        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
+
+    if error != nil {
+        // Error handling if needed...
+    }
+}
+```
+
+#### :closed_lock_with_key: Updating a Touch ID protected item
+
+The same way as when adding.  
+
+**Do not run in the main thread If there is a possibility that the item you are trying to add already exists, and protected.
+Because updating protected items requires authentication.**
+
+Additionally, you want to show custom authentication prompt message when updating, specify an `authenticationPrompt` attribute.
+If the item not protected, the `authenticationPrompt` parameter just be ignored.
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
+    let error = keychain
+        .accessibility(.WhenPasscodeSetThisDeviceOnly, authenticationPolicy: .UserPresence)
+        .authenticationPrompt("Authenticate to update your access token")
+        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
+
+    if error != nil {
+        // Error handling if needed...
+    }
+}
+```
+
+#### :closed_lock_with_key: Obtaining a Touch ID protected item
+
+The same way as when you get a normal item. It will be displayed automatically Touch ID or passcode authentication If the item you try to get is protected.  
+If you want to show custom authentication prompt message, specify an `authenticationPrompt` attribute.
+If the item not protected, the `authenticationPrompt` parameter just be ignored.
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
+    let failable = keychain
+        .authenticationPrompt("Authenticate to login to server")
+        .getStringOrError("kishikawakatsumi")
+
+    if failable.successed {
+        println("value: \(failable.value)")
+    } else {
+        println("error: \(failable.error?.localizedDescription)")
+        // Error handling if needed...
+    }
+}
+```
+
+#### :closed_lock_with_key: Removing a Touch ID protected item
+
+The same way as when you remove a normal item.
+There is no way to show Touch ID or passcode authentication when removing Keychain items.
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+let error = keychain.remove("kishikawakatsumi")
+
+if error != nil {
+    println("error: \(error?.localizedDescription)")
+    // Error handling if needed...
+}
+```
+
 ### :key: Debugging
 
 #### Display all stored items if print keychain object