Reputation: 1594
I'm attempting to store auth credentials using iOS's Keychain library (targeting the iphone) during my login process. Also, I'm attempting to store a session_id in kSecClassKey
and the user id in kSecAttrAccount
. This solution feels a little hacky but it also seems like the best one after researching the issue (how to store session data in iOS).
EDIT: I think I'm just using Keychain wrong. This felt wrong. If anyone has any advice here lemme know but I'll probably delete this soon.
The code looks like this:
loginUser() {
...
let account = user_id
let server = "www.foo.com"
let token = auth_token
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: account,
kSecAttrServer as String: server,
kSecClassKey as String: token
]
let status = SecItemAdd(query as CFDictionary, nil)
print(status)
}
When I run that code, the status prints -50
. This code could mean several things according to osstatus including errSecNoSuchClass
, errSecUseKeychainListUnsupported
or errSecUseKeychainUnsupported
.
I'm following this tutorial from Apple's docs. I'm not quite sure how to proceed from here. Any ideas?
Upvotes: 0
Views: 1982
Reputation: 23500
Just for learning/teaching purposes, I will write how to use the keychain manually.. However, I'd consider using a library as the API is a fairly low level C-API..
First you need to create a function to check if the item exists already.. If it exists, you need to "update" the item instead. If it doesn't exist, you need to add it..
The following code (Swift 4.2) should do it.. There's also no need to store the data as a InternetPassword.. You can just store it as generic password.. but that part is entirely up to you.
I used generic password below and I simply store a Dictionary with the "user" as the key.
So the code:
static let keychainIdentifier = "com.myproject.keychain.identifier"
// Checks if an item exists in the keychain already..
func exists(key: String, completion: @escaping (_ error: OSStatus, _ query: [CFString: Any], _ result: [CFString: Any]?) -> Void) {
var query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrGeneric: keychainIdentifier.data(using: .utf8)!
kSecMatchLimit: kSecMatchLimitOne,
kSecReturnAttributes: kCFBooleanTrue,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAttrAccount: key
]
var result: CFTypeRef?
let error = SecItemCopyMatching(accountQuery as CFDictionary, &result)
completion(error, accountQuery, result as? [CFString: Any])
}
// Adds or Updates an item in the keychain..
func setItem(key: String, data: Data) throws {
exists(key: key) { error, query, _ in
var query = query
query[kSecMatchLimit] = nil
query[kSecReturnAttributes] = nil
if error == noErr || error == errSecDuplicateItem || error == errSecInteractionNotAllowed {
let updateQuery = [kSecValueData: data]
let err = SecItemUpdate(query as CFDictionary, updateQuery as CFDictionary)
if err != noErr {
throw KeychainError((code: err, message: "Cannot Update Item")
}
}
else if error == errSecItemNotFound {
query[kSecValueData] = data
let err = SecItemAdd(query as CFDictionary, nil)
if err != noErr {
throw KeychainError((code: err, message: "Cannot Set Item")
}
} else {
if err != noErr {
throw KeychainError(code: err, message: "Error Occurred")
}
}
}
}
// Convenience function to save JSON to the keychain
func setItem(key: String, data: Codable) throws {
let encodedData = try JSONEncoder().encode(data)
setItem(key: key, data: encodedData)
}
and the usage:
struct User: Codable {
let username: String
let token: String?
let otherInfo: String
}
let user = User(username: "Brandon", token: "...", otherInfo: "...")
setItem(key: user.username, data: user)
IMO, this makes it a lot easier to use.. but beware, you're not really supposed to store "A LOT" of data in the keychain such as an entire model or something (as generic password)..
Alternatively, if it's just the token, then:
setItem(key: user.username, data: "...".data(using: .utf8)!)
Upvotes: 5