Reputation: 4302
I'm looking for the simples way to store/load an accessToken and refreshToken in the iOS Keychain.
So far I've come to this:
enum Key: String {
case accessToken = "some.keys.accessToken"
case refreshToken = "some.keys.refreshToken"
fileprivate var tag: Data {
rawValue.data(using: .utf8)!
}
}
enum KeychainError: Error {
case storeFailed
case loadFailed
}
func store(key: Key, value: String) throws {
let addQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: key.tag,
kSecValueRef as String: value
]
let status = SecItemAdd(addQuery as CFDictionary, nil)
guard status == errSecSuccess else {
print("Store key: '\(key.rawValue)' in Keychain failed with status: \(status.description)")
throw KeychainError.storeFailed
}
}
func load(key: Key) throws -> String? {
let getQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: key.tag,
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecReturnRef as String: true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(getQuery as CFDictionary, &item)
guard status == errSecSuccess else {
print("Load key: '\(key.rawValue)' in Keychain failed with status: \(status.description)")
throw KeychainError.loadFailed
}
return item as? String
}
But this fail with messages:
When running store
:
Store key: 'some.keys.accessToken' in Keychain failed with status: -50
When running load
:
Load key: 'some.keys.accessToken' in Keychain failed with status: -25300
What am I doing wrong here?
Upvotes: 2
Views: 7064
Reputation: 3264
As per recommendations of Apple, you should use a kSecClassGenericPassword
class to store arbitrary data, i.e. tokens, securely. To do that properly, you'll need a String
identifier to store under the kSecAttrAccount
key and a Data
representation of the secure value to store under the kSecValueData
key. You can easily transform your string value to Data
by doing the following (assuming the token contains UTF8 data).
tokenString.data(using: .utf8)
// or
Data(tokenString.utf8)
First some nice to haves.
/// Errors that can be thrown when the Keychain is queried.
enum KeychainError: LocalizedError {
/// The requested item was not found in the Keychain.
case itemNotFound
/// Attempted to save an item that already exists.
/// Update the item instead.
case duplicateItem
/// The operation resulted in an unexpected status.
case unexpectedStatus(OSStatus)
}
/// A service that can be used to group the tokens
/// as the kSecAttrAccount should be unique.
let service = "com.bundle.stuff.token-service"
Inserting a token into the Keychain.
func insertToken(_ token: Data, identifier: String, service: String = service) throws {
let attributes = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: identifier,
kSecValueData: token
] as CFDictionary
let status = SecItemAdd(attributes, nil)
guard status == errSecSuccess else {
if status == errSecDuplicateItem {
throw KeychainError.duplicateItem
}
throw KeychainError.unexpectedStatus(status)
}
}
The retrieval will be done as follows.
func getToken(identifier: String, service: String = service) throws -> String {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: identifier,
kSecMatchLimit: kSecMatchLimitOne,
kSecReturnData: true
] as CFDictionary
var result: AnyObject?
let status = SecItemCopyMatching(query, &result)
guard status == errSecSuccess else {
if status == errSecItemNotFound {
// Technically could make the return optional and return nil here
// depending on how you like this to be taken care of
throw KeychainError.itemNotFound
}
throw KeychainError.unexpectedStatus(status)
}
// Lots of bang operators here, due to the nature of Keychain functionality.
// You could work with more guards/if let or others.
return String(data: result as! Data, encoding: .utf8)!
}
Note that a generic password has certain specifications, as mentioned before, and I guess the most important one, is that the kSecAttrAccount
flag must be unique for each token you store. You cannot store access token A and access token B for the same identifier. This will cause the .duplicateItem
error to trigger.
I'd also like to point out that the OSStatus website is very useful for getting more info about your error code. Besides the website there is also the SecCopyErrorMessageString(OSStatus, UnsafeMutableRawPointer?)
function that can get you more information about your error code.
Now that technically answers your question, but below I've added some more nice to haves. Update updates a token value for an existing item, make sure the item exists before calling update!. Upsert inserts a token when it doesn't already exist, if it does, it will update the token value. Delete will remove the token value from the Keychain.
func updateToken(_ token: Data, identifier: String, service: String = service) throws {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: identifier
] as CFDictionary
let attributes = [
kSecValueData: token
] as CFDictionary
let status = SecItemUpdate(query, attributes)
guard status == errSecSuccess else {
if status == errSecItemNotFound {
throw KeychainError.itemNotFound
}
throw KeychainError.unexpectedStatus(status)
}
}
func upsertToken(_ token: Data, identifier: String, service: String = service) throws {
do {
_ = try getToken(identifier: identifier, service: service)
try updateToken(token, identifier: identifier, service: service)
} catch KeychainError.itemNotFound {
try insertToken(token, identifier: identifier, service: service)
}
}
func deleteToken(identifier: String, service: String = service) throws {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: identifier
] as CFDictionary
let status = SecItemDelete(query)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unexpectedStatus(status)
}
}
Upvotes: 10
Reputation: 13458
Please try the following edits, marked with comments:
enum KeychainError: Error {
case storeFailed
case loadFailed
}
func store(key: Key, value: String) throws {
let addQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: key.tag,
// use kSecValueData, converting your String to Data
kSecValueData as Data: value.data(using: .utf8)
]
let status = SecItemAdd(addQuery as CFDictionary, nil)
guard status == errSecSuccess else {
print("Store key: '\(key.rawValue)' in Keychain failed with status: \(status.description)")
throw KeychainError.storeFailed
}
}
func load(key: Key) throws -> String? {
let getQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: key.tag,
// remove kSecAttrKeyType and add kSecReturnData
kSecReturnData as String: kCFBooleanTrue
]
// this is different
var item: AnyObject?
let status: OSStatus = withUnsafeMutablePointer(to:&item)
{ (result: UnsafeMutablePointer<AnyObject?>?) -> OSStatus in
return SecItemCopyMatching(getQuery as CFDictionary, result)
}
guard status == errSecSuccess else {
print("Load key: '\(key.rawValue)' in Keychain failed with status: \(status.description)")
throw KeychainError.loadFailed
}
// convert Data to String
guard let itemData = item as? Data else { throw KeychainError.loadFailed }
return String(decoding: itemData, as: UTF8.self)
}
how does this change the behavior?
A couple comments:
kSecAttrKeyType
property to store a
StringkSecValueRef
vs. kSecValueData
, only
making this suggestion based on what has worked for meHope this helps!?
Upvotes: -1