73

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.

Community
  • 1
  • 1
Sazzad Hissain Khan
  • 37,929
  • 33
  • 189
  • 256

1 Answers1

172

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)
}
Sazzad Hissain Khan
  • 37,929
  • 33
  • 189
  • 256
  • 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? – Vaibhav Misra Apr 21 '17 at 09:43
  • 2
    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. – sketchyTech Jun 07 '17 at 09:17
  • removePassword() and Update is not removing password for me. I am getting status code as "-50". I haven't changed code, was testing it. – helloWorld Jun 19 '17 at 21:52
  • Please check whether the key exists or not – Sazzad Hissain Khan Jun 20 '17 at 04:58
  • 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. – CRey Jun 25 '17 at 04:48
  • 2
    did someone fix the remove & update methods? i have the same problem (error code -50) – Asi Givati Jun 26 '17 at 10:03
  • 17
    I found a better solution that works: https://github.com/dagostini/DAKeychain/blob/master/DAKeychain/Classes/DAKeychain.swift @AsiGivati – Matthew Barker Jul 07 '17 at 18:43
  • Im getting error status code -25300 – Saranjith Jul 12 '17 at 06:55
  • when do u get this code? – Sazzad Hissain Khan Jul 12 '17 at 10:42
  • the Update() doesn't seem to work! I get Update failed: -50. – GameDev Jul 14 '17 at 14:49
  • 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? – GameDev Jul 14 '17 at 14:50
  • @sketchyTech please can you correct the update and remove functions to work correctly? – GameDev Jul 14 '17 at 15:12
  • @GameDev 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. – sketchyTech Jul 16 '17 at 15:32
  • @sketchyTech thank you, ill try this out! – GameDev Jul 19 '17 at 12:40
  • @GameDev 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. – sketchyTech Jul 19 '17 at 12:48
  • 5
    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. – imjohnking Aug 11 '17 at 13:08
  • 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]) – Boaz Saragossi Aug 24 '17 at 13:15
  • 3
    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 ? – Florin Odagiu Aug 25 '17 at 14:56
  • import cocoa & SecCopyErrorMessageString shows an error with message "Unresolved identifier" – Krunal Oct 25 '17 at 10:11
  • how can I use this code in iOS? (Look at here - https://stackoverflow.com/questions/34053049/seccopyerrormessagestring-gives-use-of-unresolved-identifier-in-swift.) – Krunal Oct 25 '17 at 10:13
  • Thanks for the answer, quite useful. Btw, I would point out that `SecCopyErrorMessageString` is only available in MacOS. – kikettas Jan 21 '18 at 10:41
  • this code is working great at my end. How can i get the same code in objective c – Parv Bhasker Apr 26 '18 at 13:36
  • Is there any possibility of stored data in KeyChain get affected ? I mean OS update or etc ? – Mitesh Dobareeya May 12 '18 at 05:28
  • @GameDev Remove kSecReturnDataValue from update call. – vntstudy Oct 04 '18 at 11:05
  • Cannot convert value of type 'String.Encoding' to expected argument type 'UInt', Replace 'String.Encoding.utf8' with 'String.Encoding.utf8.rawValue' – ScottyBlades Nov 25 '18 at 19:15
  • 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 people – João Serra Apr 10 '19 at 14:44
  • 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' results in a dangling buffer pointer" 3. return self.withUnsafeBytes { $0.pointee } -> "'withUnsafeBytes' is deprecated: use `withUnsafeBytes(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R` instead" – Alwin Jose Oct 23 '21 at 09:08
  • 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? – lauwis Nov 10 '22 at 02:12