Save and Load from KeyChain | Swift [duplicate]
Asked Answered
H

1

76

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.

Hispaniola answered 31/5, 2016 at 7:55 Comment(0)
H
175

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://mcmap.net/q/265865/-save-and-load-from-keychain-swift-duplicate
// 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)
}
Hispaniola answered 31/5, 2016 at 7:55 Comment(27)
Thanks a lot for this! I just have one question here. For saving password, you are first deleting and then adding. Would it have not been better to first check if the password exists and then either update or add it to keychain? What are the pros/cons of this approach over the other?Wolford
I find this much easier to understand than the answers to the duplicate question. I've edited to include updating, which the save code now implements automatically if it is unable to overwrite an existing password (rather than attempting a delete ahead of time). There is also an added method for removing a password. You will find in Swift 3 that you'll need to do some casting to NSString where this doesn't happen automatically anymore but apart from that all should work.Lightening
removePassword() and Update is not removing password for me. I am getting status code as "-50". I haven't changed code, was testing it.Gambia
Please check whether the key exists or notHispaniola
Hello @SazzadHissainKhan, Im using your code to save the password. I also want to save the email at the same time. Can you please advice me on how to do that? do I need a whole new set of private methods just for the account email? Please and thanks sir.Glabrous
did someone fix the remove & update methods? i have the same problem (error code -50)Eubank
I found a better solution that works: github.com/dagostini/DAKeychain/blob/master/DAKeychain/Classes/… @AsiGivatiRevocable
Im getting error status code -25300Traipse
when do u get this code?Hispaniola
the Update() doesn't seem to work! I get Update failed: -50.Muumuu
I updated to latest swift version, which changed this line in Update function: let dataFromString: NSData = data.data(using: String.Encoding.utf8.rawValue, allowLossyConversion: false)! as NSData. Is that still correct or wrong now?Muumuu
@Lightening please can you correct the update and remove functions to work correctly?Muumuu
@Muumuu I've no time at the moment to rework my code back into the example but I have copied code out of a simple app I built with all of the required elements which shouldn't be too difficult to adapt and makes the code more portable.Lightening
@Muumuu no problem. There shouldn't be any error messages unless the item being retrieved, deleted or updated doesn't exist. One reason there might have been error messages with the original sample code is that often when storing data you can't immediately retrieve or alter it, because the save happens in the background and takes a few moments. A better test is to use buttons to allow a slight pause as in real use.Lightening
I was running into the -50 OSStatus errors on the delete function and corrected it by removing the kSecReturnDataValue and kSecMatchLimitValue items from the keychain query before the SecItemDelete call. The -50 indicates a problem with the parameters that were passed into the function. Removing those two fixed it for me. I'm assuming the same problem exists in the update function but I haven't tried working with that one yet.Ondine
imjohnking is right, in Update and Remove the query line should be - let keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, account, kCFBooleanTrue, kSecMatchLimitOneValue], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecReturnDataValue, kSecMatchLimitValue])Balbo
What if loadPassword() fails? I mean...it works, and works, and works countless times, and then one day it fails (in my case, one user experiences this once every 10 days). Would a simple retry suffice? Or it might have failed for some more serious reason that requires something more involved ?Detrital
import cocoa & SecCopyErrorMessageString shows an error with message "Unresolved identifier"Shangrila
how can I use this code in iOS? (Look at here - #34053549)Shangrila
Thanks for the answer, quite useful. Btw, I would point out that SecCopyErrorMessageString is only available in MacOS.Monocular
this code is working great at my end. How can i get the same code in objective cAuditory
Is there any possibility of stored data in KeyChain get affected ? I mean OS update or etc ?Clover
@Muumuu Remove kSecReturnDataValue from update call.Phaih
Cannot convert value of type 'String.Encoding' to expected argument type 'UInt', Replace 'String.Encoding.utf8' with 'String.Encoding.utf8.rawValue'Carillonneur
yea... you shouldnt store the password and use access tokens instead, it's even worse if you do something like KeychainService.savePassword("Pa55worD") cause its readable, at least use md5 or something, but yea... its an example so... just warning peopleAlbaugh
Thanks for the answer. It's working well for me but I got a few warnings in the Swift5 version. Please someone clarify. 1. kSecReturnData as String : kCFBooleanTrue, -> "Coercion of implicitly unwrappable value of type 'CFBoolean?' to 'Any' does not unwrap optional". 2. self.init(buffer: UnsafeBufferPointer(start: &value, count: 1)) -> "Initialization of 'UnsafeBufferPointer<T>' results in a dangling buffer pointer" 3. return self.withUnsafeBytes { $0.pointee } -> "'withUnsafeBytes' is deprecated: use withUnsafeBytes<R>(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R instead"Kohl
Hello, i'm new with Swift. Where is keychain actually store? is it stored in device? or is it stored in icloud? is there any way to prove if it's located in icloud or device? is it accessable with user?Chelyuskin

© 2022 - 2024 — McMap. All rights reserved.