Search code examples
iosswiftsecuritykeychain

Save and Load from KeyChain | Swift


How to simply store a String in Keychain and load when needed. There are several SO solution which mostly refers to Git repo. But I need the smallest and the simplest solution on latest Swift. Certainly, I don't want to add git framework for simply storing a password in my project.

There are similar solution Save and retrieve value via KeyChain , which did not work for me. Tired with compiler errors.


Solution

  • Simplest Source

    import Foundation
    import Security
    
    // Constant Identifiers
    let userAccount = "AuthenticatedUser"
    let accessGroup = "SecuritySerivice"
    
    
    /** 
     *  User defined keys for new entry
     *  Note: add new keys for new secure item and use them in load and save methods
     */
    
    let passwordKey = "KeyForPassword"
    
    // Arguments for the keychain queries
    let kSecClassValue = NSString(format: kSecClass)
    let kSecAttrAccountValue = NSString(format: kSecAttrAccount)
    let kSecValueDataValue = NSString(format: kSecValueData)
    let kSecClassGenericPasswordValue = NSString(format: kSecClassGenericPassword)
    let kSecAttrServiceValue = NSString(format: kSecAttrService)
    let kSecMatchLimitValue = NSString(format: kSecMatchLimit)
    let kSecReturnDataValue = NSString(format: kSecReturnData)
    let kSecMatchLimitOneValue = NSString(format: kSecMatchLimitOne)
    
    public class KeychainService: NSObject {
    
        /**
         * Exposed methods to perform save and load queries.
         */
    
        public class func savePassword(token: NSString) {
            self.save(passwordKey, data: token)
        }
    
        public class func loadPassword() -> NSString? {
            return self.load(passwordKey)
        }
        
        /**
         * Internal methods for querying the keychain.
         */
    
        private class func save(service: NSString, data: NSString) {
            let dataFromString: NSData = data.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
    
            // Instantiate a new default keychain query
            let keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, userAccount, dataFromString], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecValueDataValue])
    
            // Delete any existing items
            SecItemDelete(keychainQuery as CFDictionaryRef)
    
            // Add the new keychain item
            SecItemAdd(keychainQuery as CFDictionaryRef, nil)
        }
    
        private class func load(service: NSString) -> NSString? {
            // Instantiate a new default keychain query
            // Tell the query to return a result
            // Limit our results to one item
            let keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, userAccount, kCFBooleanTrue, kSecMatchLimitOneValue], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecReturnDataValue, kSecMatchLimitValue])
    
            var dataTypeRef :AnyObject?
    
            // Search for the keychain items
            let status: OSStatus = SecItemCopyMatching(keychainQuery, &dataTypeRef)
            var contentsOfKeychain: NSString? = nil
    
            if status == errSecSuccess {
                if let retrievedData = dataTypeRef as? NSData {
                    contentsOfKeychain = NSString(data: retrievedData, encoding: NSUTF8StringEncoding)
                }
            } else {
                print("Nothing was retrieved from the keychain. Status code \(status)")
            }
    
            return contentsOfKeychain
        }
    }
    

    Example of Calling

    KeychainService.savePassword("Pa55worD")
    let password = KeychainService.loadPassword() // password = "Pa55worD"
    

    SWIFT 4: VERSION WITH UPDATE AND REMOVE PASSWORD

    import Cocoa
    import Security
    
    // see https://stackoverflow.com/a/37539998/1694526
    // Arguments for the keychain queries
    let kSecClassValue = NSString(format: kSecClass)
    let kSecAttrAccountValue = NSString(format: kSecAttrAccount)
    let kSecValueDataValue = NSString(format: kSecValueData)
    let kSecClassGenericPasswordValue = NSString(format: kSecClassGenericPassword)
    let kSecAttrServiceValue = NSString(format: kSecAttrService)
    let kSecMatchLimitValue = NSString(format: kSecMatchLimit)
    let kSecReturnDataValue = NSString(format: kSecReturnData)
    let kSecMatchLimitOneValue = NSString(format: kSecMatchLimitOne)
    
    public class KeychainService: NSObject {
        
        class func updatePassword(service: String, account:String, data: String) {
            if let dataFromString: Data = data.data(using: String.Encoding.utf8, allowLossyConversion: false) {
                
                // Instantiate a new default keychain query
                let keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, account], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue])
                
                let status = SecItemUpdate(keychainQuery as CFDictionary, [kSecValueDataValue:dataFromString] as CFDictionary)
                
                if (status != errSecSuccess) {
                    if let err = SecCopyErrorMessageString(status, nil) {
                        print("Read failed: \(err)")
                    }
                }
            }
        }
        
        
        class func removePassword(service: String, account:String) {
            
            // Instantiate a new default keychain query
            let keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, account, kCFBooleanTrue], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecReturnDataValue])
            
            // Delete any existing items
            let status = SecItemDelete(keychainQuery as CFDictionary)
            if (status != errSecSuccess) {
                if let err = SecCopyErrorMessageString(status, nil) {
                    print("Remove failed: \(err)")
                }
            }
            
        }
        
        
        class func savePassword(service: String, account:String, data: String) {
            if let dataFromString = data.data(using: String.Encoding.utf8, allowLossyConversion: false) {
                
                // Instantiate a new default keychain query
                let keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, account, dataFromString], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecValueDataValue])
                
                // Add the new keychain item
                let status = SecItemAdd(keychainQuery as CFDictionary, nil)
                
                if (status != errSecSuccess) {    // Always check the status
                    if let err = SecCopyErrorMessageString(status, nil) {
                        print("Write failed: \(err)")
                    }
                }
            }
        }
        
        class func loadPassword(service: String, account:String) -> String? {
            // Instantiate a new default keychain query
            // Tell the query to return a result
            // Limit our results to one item
            let keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, account, kCFBooleanTrue, kSecMatchLimitOneValue], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecReturnDataValue, kSecMatchLimitValue])
            
            var dataTypeRef :AnyObject?
            
            // Search for the keychain items
            let status: OSStatus = SecItemCopyMatching(keychainQuery, &dataTypeRef)
            var contentsOfKeychain: String?
            
            if status == errSecSuccess {
                if let retrievedData = dataTypeRef as? Data {
                    contentsOfKeychain = String(data: retrievedData, encoding: String.Encoding.utf8)
                }
            } else {
                print("Nothing was retrieved from the keychain. Status code \(status)")
            }
            
            return contentsOfKeychain
        }
        
    }
    

    You need to imagine the following wired up to a text input field and a label, then having four buttons wired up, one for each of the methods.

    class ViewController: NSViewController {
        @IBOutlet weak var enterPassword: NSTextField!
        @IBOutlet weak var retrievedPassword: NSTextField!
        
        let service = "myService"
        let account = "myAccount"
        
        // will only work after
        @IBAction func updatePassword(_ sender: Any) {
            KeychainService.updatePassword(service: service, account: account, data: enterPassword.stringValue)
        }
        
        @IBAction func removePassword(_ sender: Any) {
            KeychainService.removePassword(service: service, account: account)
        }
        
        @IBAction func passwordSet(_ sender: Any) {
            let password = enterPassword.stringValue
            KeychainService.savePassword(service: service, account: account, data: password)
        }
        
        @IBAction func passwordGet(_ sender: Any) {
            if let str = KeychainService.loadPassword(service: service, account: account) {
                retrievedPassword.stringValue = str
            }
            else {retrievedPassword.stringValue = "Password does not exist" }
        }
    }
    

    Swift 5

    Kosuke's version for Swift 5

    import Security
    import UIKit
    
    class KeyChain {
    
        class func save(key: String, data: Data) -> OSStatus {
            let query = [
                kSecClass as String       : kSecClassGenericPassword as String,
                kSecAttrAccount as String : key,
                kSecValueData as String   : data ] as [String : Any]
    
            SecItemDelete(query as CFDictionary)
    
            return SecItemAdd(query as CFDictionary, nil)
        }
    
        class func load(key: String) -> Data? {
            let query = [
                kSecClass as String       : kSecClassGenericPassword,
                kSecAttrAccount as String : key,
                kSecReturnData as String  : kCFBooleanTrue!,
                kSecMatchLimit as String  : kSecMatchLimitOne ] as [String : Any]
    
            var dataTypeRef: AnyObject? = nil
    
            let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
    
            if status == noErr {
                return dataTypeRef as! Data?
            } else {
                return nil
            }
        }
    
        class func createUniqueID() -> String {
            let uuid: CFUUID = CFUUIDCreate(nil)
            let cfStr: CFString = CFUUIDCreateString(nil, uuid)
    
            let swiftString: String = cfStr as String
            return swiftString
        }
    }
    
    extension Data {
    
        init<T>(from value: T) {
            var value = value
            self.init(buffer: UnsafeBufferPointer(start: &value, count: 1))
        }
    
        func to<T>(type: T.Type) -> T {
            return self.withUnsafeBytes { $0.load(as: T.self) }
        }
    }
    

    Example usage:

    let int: Int = 555
    let data = Data(from: int)
    let status = KeyChain.save(key: "MyNumber", data: data)
    print("status: ", status)
        
    if let receivedData = KeyChain.load(key: "MyNumber") {
        let result = receivedData.to(type: Int.self)
        print("result: ", result)
    }