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?
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)
}
}
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:
I'm not sure why you're using kSecAttrKeyType property to store a
String
I'm unsure of the use of kSecValueRef vs. kSecValueData, only
making this suggestion based on what has worked for me
Hope this helps!?
Related
When I save items to the keychain I get a success but after fetching it I get an error not found.
enum KeychainError: Error {
case itemNotFound
case duplicateItem
case invalidItemFormat
case unexpectedStatus(OSStatus)
}
final class KeyChainManager {
static let service = "www.test.de"
static func save(password: String, account: String) throws {
let passwordData = password.data(using: String.Encoding.utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: account,
kSecAttrServer as String: service,
kSecValueData as String: passwordData
]
let status = SecItemAdd(
query as CFDictionary,
nil
)
if status == errSecDuplicateItem {
throw KeychainError.duplicateItem
}
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
print("success")
}
static func readPassword( account: String) throws -> Data {
let query: [String: AnyObject] = [
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: kCFBooleanTrue
]
var itemCopy: AnyObject?
let status = SecItemCopyMatching(
query as CFDictionary,
&itemCopy
)
guard status != errSecItemNotFound else {
throw KeychainError.itemNotFound
}
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
guard let password = itemCopy as? Data else {
throw KeychainError.invalidItemFormat
}
return password
}
}
If I try
try KeyChainManager.save(password: input.password ?? "", account: input.username ?? "")
I get success but if I try to load the password it stops on this line
throw KeychainError.itemNotFound
I am looking for a way to share account credentials like userID and accessToken between my iOS app and my WatchOS app. As i understand i can use the WatchConnectivity framework but that does not seem reliable. For example if my iOS app is killed i could not find a way to force wake the app to fetch the data.
The other think that i tried was keychain sharing which also does not seem to work and gives
keyStore.retrieve SecItemCopyMatching error -25300
error
Below is the code that i am using, which i got from this tutorial.
class KeyStore {
let account = "accessToken"
let group = "[TeamID].[BundleID]" //Setup same as in keychain sharing options in capabilities
func store(token : String) {
let data = token.data(using: .utf8)!
let addquery: [String: Any] = [kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: account,
kSecValueData as String: data,
kSecAttrSynchronizable as String : kCFBooleanTrue!,
kSecAttrAccessGroup as String : group
]
SecItemDelete(addquery as CFDictionary)
let status : OSStatus = SecItemAdd(addquery as CFDictionary, nil)
guard status == errSecSuccess else {
os_log("store: whoops")
return
}
}
func clear() {
let addquery: [String: Any] = [kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: account,
kSecAttrSynchronizable as String : kCFBooleanTrue!,
kSecAttrAccessGroup as String : group
]
SecItemDelete(addquery as CFDictionary)
}
func retrieve() -> String? {
let getquery: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String : kSecMatchLimitOne,
kSecAttrSynchronizable as String : kCFBooleanTrue!,
kSecAttrAccessGroup as String : group
]
var item: CFTypeRef?
let status = SecItemCopyMatching(getquery as CFDictionary, &item)
guard status == errSecSuccess else {
os_log("keyStore.retrieve SecItemCopyMatching error \(status)")
return nil
}
guard let data = item as? Data? else {
os_log("keyStore.retrieve not data")
return nil
}
return String(data: data!, encoding: String.Encoding.utf8)
}
func getAllKeychainItems() throws {
let classes = [kSecClassGenericPassword as String, // Generic password items
kSecClassInternetPassword as String, // Internet password items
kSecClassCertificate as String, // Certificate items
kSecClassKey as String, // Cryptographic key items
kSecClassIdentity as String,
kSecAttrAccount as String] // Identity items
classes.forEach { secClass in
let items = getAllKeyChainItemsOfClass( secClass )
NSLog(items.description)
}
}
func getAllKeyChainItemsOfClass(_ secClass: String) -> [String: AnyObject] {
let query: [String: Any] = [
kSecClass as String : secClass,
kSecReturnData as String : true,
kSecReturnAttributes as String : true,
kSecReturnRef as String : true,
kSecMatchLimit as String: kSecMatchLimitAll
]
var result: AnyObject?
let lastResultCode = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
var values = [String: AnyObject]()
if lastResultCode == noErr {
let array = result as? Array<Dictionary<String, Any>>
for item in array! {
if let key = item[kSecAttrAccount as String] as? String,
let value = item[kSecValueData as String] as? Data {
values[key] = String(data: value, encoding:.utf8) as AnyObject?
}
else if let key = item[kSecAttrLabel as String] as? String,
let value = item[kSecValueRef as String] {
values[key] = value as AnyObject
}
}
}
return values
}
}
Any references that could help me share login credentials even when the app is killed would really help
I have a problem updating and deleting keychain.
public func clearKeychain(username: String) throws -> Any?{
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: username
]
let status = SecItemDelete(query as CFDictionary)
if status != errSecSuccess {
throw KeychainError.unhandledError(status: status)
}
print("Clear Keychain")
return status
}
public func updateKeychain(username: String, password: String) throws -> Any?{
let credentials = Credentials.init(username: username, password: password)
let data = credentials.password.data(using: .utf8)!
// store password as data and if you want to store username
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: username,
kSecValueData as String: data]
let fields: [String: Any] = [
kSecAttrAccount as String: username,
kSecValueData as String: data
]
let status = SecItemUpdate(query as CFDictionary, fields as CFDictionary)
guard status == errSecSuccess else {
throw KeychainError.unhandledError(status: status) }
print("Updated Password")
return status
}
Usage:
let _ = (try? keychain.clearKeychain(username: "KeychainUser")) as Any?
let _ = (try? keychain.updateKeychain(username: "KeychainUser", password: "123456789")) as Any?
Can't delete or update keychain. I have no problem creating and getting the value of the keychain, but I have a problem when updating or deleting. No error is shown. And status returns nil.
To update & delete your item, you need to search particular type of data. So you don't require to pass data in query. Just update your query as follow will fix issue on updating data.
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword]
By default, keychain services deletes all keychain items that match
the search parameters. If you want to delete a specific item that you
already have a reference or persistent reference to, add that to the
search dictionary as the value for the kSecMatchItemList key. In this
way, you limit the deletion to only the specified item.
I am developing an iOS project with XCode8 + Swift3.
I have created the following two functions to store string to keychain and read it back from keychain:
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "my service",
kSecAttrAccount as String: "my-key"
]
func storeString(value: String) -> Bool {
if let data = value.data(using: .utf8) {
// delete data if exist
SecItemDelete(query as CFDictionary)
// add value to query
query[kSecValueData as String] = data
// add to keychain
let status = SecItemAdd(query as CFDictionary, nil)
return status == noErr
}
return false
}
func readString() -> String? {
// update query
query[kSecReturnData as String] = kCFBooleanTrue
query[kSecMatchLimit as String] = kSecMatchLimit
var result: AnyObject?
// fetch items from keychain
let status: OSStatus = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
// I always get error -50 here
if status == noErr {
if let resultData = result as? Data {
if let strVal = String(data: resultData, encoding: .utf8) {
return strVal
}
}
}
return nil
}
I invoke the functions:
boolean hasStored = storeString(value: "John")
let readVal = readString()
I got hasStored is true, but readVal always nil. I investigated my functions, I see I always get error -50 as status code in reader function (see my comment in my function).
Why? Why I can't read the value I stored in keychain (I am not sure whether it is really stored but I do get status == noErr always retuns true in storeString(value:) function)
There is a typo in your code:
query[kSecMatchLimit as String] = kSecMatchLimit
// ^~~~~~~~~~~~~~
kSecMatchLimit is the key and not a valid value. The expected value should be a CFNumber or kSecMatchLimitOne or kSecMatchLimitAll. If you expect a single item to be returned, use kSecMatchLimitOne. See also Search Attribute Keys and Values.
iam trying save a value to Keychain. Code is right, but OSStatus returns code -34018.
In other project same code works right.
Code:
fileprivate func save(key: String, value: String) -> Bool {
guard let data: Data = value.data(using: String.Encoding.utf8) else {
return false
}
let query = [
kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: key,
kSecValueData as String: data,
] as [String : Any]
SecItemDelete(query as CFDictionary)
let status: OSStatus = SecItemAdd(query as CFDictionary, nil)
return status == noErr
}
Any idea, please?
Is there any error with project settings or?