Client certificates and identities in iOS - ios

I have generated private key and public key to my Swift-based iOS application using SecKeyGeneratePair function.Then, I generated Certificate Signing Request using iOS CSR generationand my server replied with certificate chain in PEM format.I converted PEM-certificate to DER-format using following code:
var modifiedCert = certJson.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "")
modifiedCert = modifiedCert.replacingOccurrences(of: "-----END CERTIFICATE-----", with: "")
modifiedCert = modifiedCert.replacingOccurrences(of: "\n", with: "")
let dataDecoded = NSData(base64Encoded: modifiedCert, options: [])
Now, I should create certificate from DER-data using let certificate = SecCertificateCreateWithData(nil, certDer)
My question is following : How can I connect the certificate with private key I have created in the beginning and get the identity where both of these(keys and certificate) belongs?Maybe, add certificate to keychain and get the identity using SecItemCopyMatching? I have followed the procedure presented in question SecIdentityRef procedure
Edit:
When adding the certificate to keychain, I get the status response 0, which I believe means that certificate has been added to keychain.
let certificate: SecCertificate? = SecCertificateCreateWithData(nil, certDer)
if certificate != nil{
let params : [String: Any] = [
kSecClass as String : kSecClassCertificate,
kSecValueRef as String : certificate!
]
let status = SecItemAdd(params as CFDictionary, &certRef)
print(status)
}
Now when I'm trying to get the identity, I get status -25300 (errSecItemNotFound). Following code is used to get the identity. tag is the private key tag I have used to generate private/public key.
let query: [String: Any] = [
kSecClass as String : kSecClassIdentity,
kSecAttrApplicationTag as String : tag,
kSecReturnRef as String: true
]
var retrievedData: SecIdentity?
var extractedData: AnyObject?
let status = SecItemCopyMatching(query as NSDictionary, &extractedData)
if (status == errSecSuccess) {
retrievedData = extractedData as! SecIdentity?
}
I'm able to get the private key & public key & certificate from the keychain using SecItemCopyMatching and add the certificate to keychain, but querying the SecIdentity does not work. Is it possible that my certificate does not match to my keys? How is that checked?
I printed public key from iOS in base64 format. The following was printed:
MIIBCgKCAQEAo/MRST9oZpO3nTl243o+ocJfFCyKLtPgO/QiO9apb2sWq4kqexHy
58jIehBcz4uGJLyKYi6JHx/NgxdSRKE3PcjU2sopdMN35LeO6jZ34auH37gX41Sl
4HWkpMOB9v/OZvMoKrQJ9b6/qmBVZXYsrSJONbr+74/mI/m1VNtLOM2FIzewVYcL
HHsM38XOg/kjSUsHEUKET/FfJkozgp76r0r3E0khcbxwU70qc77YPgeJHglHcZKF
ZHFbvNz4E9qUy1mWJvoCmAEItWnyvuw+N9svD1Rri3t5qlaBwaIN/AtayHwJWoWA
/HF+Jg87eVvEErqeT1wARzJL2xv5V1O4ZwIDAQAB
Then from the certificate signing request I extracted the public key using openssl (openssl req -in ios.csr -pubkey -noout). The following response was printed:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo/MRST9oZpO3nTl243o+
ocJfFCyKLtPgO/QiO9apb2sWq4kqexHy58jIehBcz4uGJLyKYi6JHx/NgxdSRKE3
PcjU2sopdMN35LeO6jZ34auH37gX41Sl4HWkpMOB9v/OZvMoKrQJ9b6/qmBVZXYs
rSJONbr+74/mI/m1VNtLOM2FIzewVYcLHHsM38XOg/kjSUsHEUKET/FfJkozgp76
r0r3E0khcbxwU70qc77YPgeJHglHcZKFZHFbvNz4E9qUy1mWJvoCmAEItWnyvuw+
N9svD1Rri3t5qlaBwaIN/AtayHwJWoWA/HF+Jg87eVvEErqeT1wARzJL2xv5V1O4
ZwIDAQAB
-----END PUBLIC KEY----
It seems that there is a minor difference in the beginning of the key generated from CSR. (MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A). Based on the question RSA encryption, it seems that MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A is base64-formatted identifier for RSA encryption "1.2.840.113549.1.1.1". So I guess the public key might be fine?

We don't use that same method of CSR, but we have an equivalent thing where we do the following:
Generate key pair
Ship the public key to the remote server
Remote server generates a signed client certificate using the public key
Ship the client certificate back to the iOS device
Add the client certificate to the keychain
Later on, use the client certificate in an NSURLSession or similar.
As you seem to have discovered, iOS needs this extra thing called an "identity" to tie the client cert.
We also discovered that iOS has a weird thing where you need to DELETE the public key from the keychain before you add the client cert and identity into it, otherwise the identity doesn't seem to locate the client certificate properly instead. We chose to add the public key back in but as a "generic password" (i.e arbitrary user data) - we only do this because iOS doesn't have a sensible API for extracting a public key from a cert on the fly, and we need the public key for other strange things we happen to be doing.
If you're just doing TLS client certificate auth, once you have the certificate you won't need an explicit copy of the public key so you can simplify the process by simply deleting it, and skip the "add-back-in-as-generic-password" bit
Please excuse the giant pile of code, crypto stuff always seems to require a lot of work.
Here's bits of code to perform the above tasks:
Generating the keypair, and deleting/re-saving the public key
/// Returns the public key binary data in ASN1 format (DER encoded without the key usage header)
static func generateKeyPairWithPublicKeyAsGenericPassword(privateKeyTag: String, publicKeyAccount: String, publicKeyService: String) throws -> Data {
let tempPublicKeyTag = "TMPPUBLICKEY:\(privateKeyTag)" // we delete this public key and replace it with a generic password, but it needs a tag during the transition
let privateKeyAttr: [NSString: Any] = [
kSecAttrApplicationTag: privateKeyTag.data(using: .utf8)!,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrIsPermanent: true ]
let publicKeyAttr: [NSString: Any] = [
kSecAttrApplicationTag: tempPublicKeyTag.data(using: .utf8)!,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrIsPermanent: true ]
let keyPairAttr: [NSString: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits: 2048,
kSecPrivateKeyAttrs: privateKeyAttr,
kSecPublicKeyAttrs: publicKeyAttr ]
var publicKey: SecKey?, privateKey: SecKey?
let genKeyPairStatus = SecKeyGeneratePair(keyPairAttr as CFDictionary, &publicKey, &privateKey)
guard genKeyPairStatus == errSecSuccess else {
log.error("Generation of key pair failed. Error = \(genKeyPairStatus)")
throw KeychainError.generateKeyPairFailed(genKeyPairStatus)
}
// Would need CFRelease(publicKey and privateKey) here but swift does it for us
// we store the public key in the keychain as a "generic password" so that it doesn't interfere with retrieving certificates
// The keychain will normally only store the private key and the certificate
// As we want to keep a reference to the public key itself without having to ASN.1 parse it out of the certificate
// we can stick it in the keychain as a "generic password" for convenience
let findPubKeyArgs: [NSString: Any] = [
kSecClass: kSecClassKey,
kSecValueRef: publicKey!,
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecReturnData: true ]
var resultRef:AnyObject?
let status = SecItemCopyMatching(findPubKeyArgs as CFDictionary, &resultRef)
guard status == errSecSuccess, let publicKeyData = resultRef as? Data else {
log.error("Public Key not found: \(status))")
throw KeychainError.publicKeyNotFound(status)
}
// now we have the public key data, add it in as a generic password
let attrs: [NSString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrService: publicKeyService,
kSecAttrAccount: publicKeyAccount,
kSecValueData: publicKeyData ]
var result: AnyObject?
let addStatus = SecItemAdd(attrs as CFDictionary, &result)
if addStatus != errSecSuccess {
log.error("Adding public key to keychain failed. Error = \(addStatus)")
throw KeychainError.cannotAddPublicKeyToKeychain(addStatus)
}
// delete the "public key" representation of the public key from the keychain or it interferes with looking up the certificate
let pkattrs: [NSString: Any] = [
kSecClass: kSecClassKey,
kSecValueRef: publicKey! ]
let deleteStatus = SecItemDelete(pkattrs as CFDictionary)
if deleteStatus != errSecSuccess {
log.error("Deletion of public key from keychain failed. Error = \(deleteStatus)")
throw KeychainError.cannotDeletePublicKeyFromKeychain(addStatus)
}
// no need to CFRelease, swift does this.
return publicKeyData
}
NOTE that publicKeyData isn't strictly in DER format, it's in "DER with the first 24 bytes trimmed off" format. I'm not sure what this is called officially, but both microsoft and apple seem to use it as the raw format for public keys. If your server is a microsoft one running .NET (desktop or core) then it will probably be happy with the public key bytes as-is. If it's Java and expects DER you may need to generate the DER header - this is a fixed sequence of 24 bytes you can probably just concatenate on.
Adding the client certificate to the keychain, generating an Identity
static func addIdentity(clientCertificate: Data, label: String) throws {
log.info("Adding client certificate to keychain with label \(label)")
guard let certificateRef = SecCertificateCreateWithData(kCFAllocatorDefault, clientCertificate as CFData) else {
log.error("Could not create certificate, data was not valid DER encoded X509 cert")
throw KeychainError.invalidX509Data
}
// Add the client certificate to the keychain to create the identity
let addArgs: [NSString: Any] = [
kSecClass: kSecClassCertificate,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrLabel: label,
kSecValueRef: certificateRef,
kSecReturnAttributes: true ]
var resultRef: AnyObject?
let addStatus = SecItemAdd(addArgs as CFDictionary, &resultRef)
guard addStatus == errSecSuccess, let certAttrs = resultRef as? [NSString: Any] else {
log.error("Failed to add certificate to keychain, error: \(addStatus)")
throw KeychainError.cannotAddCertificateToKeychain(addStatus)
}
// Retrieve the client certificate issuer and serial number which will be used to retrieve the identity
let issuer = certAttrs[kSecAttrIssuer] as! Data
let serialNumber = certAttrs[kSecAttrSerialNumber] as! Data
// Retrieve a persistent reference to the identity consisting of the client certificate and the pre-existing private key
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassIdentity,
kSecAttrIssuer: issuer,
kSecAttrSerialNumber: serialNumber,
kSecReturnPersistentRef: true] // we need returnPersistentRef here or the keychain makes a temporary identity that doesn't stick around, even though we don't use the persistentRef
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef);
guard copyStatus == errSecSuccess, let _ = resultRef as? Data else {
log.error("Identity not found, error: \(copyStatus) - returned attributes were \(certAttrs)")
throw KeychainError.cannotCreateIdentityPersistentRef(addStatus)
}
// no CFRelease(identityRef) due to swift
}
In our code we chose to return a label, and then look up the identity as-required using the label, and the following code. You could also chose to just return the identity ref from the above function rather than the label. Here's our getIdentity function anyway
Getting the identity later on
// Remember any OBJECTIVE-C code that calls this method needs to call CFRetain
static func getIdentity(label: String) -> SecIdentity? {
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassIdentity,
kSecAttrLabel: label,
kSecReturnRef: true ]
var resultRef: AnyObject?
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef)
guard copyStatus == errSecSuccess else {
log.error("Identity not found, error: \(copyStatus)")
return nil
}
// back when this function was all ObjC we would __bridge_transfer into ARC, but swift can't do that
// It wants to manage CF types on it's own which is fine, except they release when we return them out
// back into ObjC code.
return (resultRef as! SecIdentity)
}
// Remember any OBJECTIVE-C code that calls this method needs to call CFRetain
static func getCertificate(label: String) -> SecCertificate? {
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassCertificate,
kSecAttrLabel: label,
kSecReturnRef: true]
var resultRef: AnyObject?
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef)
guard copyStatus == errSecSuccess else {
log.error("Identity not found, error: \(copyStatus)")
return nil
}
// back when this function was all ObjC we would __bridge_transfer into ARC, but swift can't do that
// It wants to manage CF types on it's own which is fine, except they release when we return them out
// back into ObjC code.
return (resultRef as! SecCertificate)
}
And finally
Using the identity to authenticate against a server
This bit is in objc because that's how our app happens to work, but you get the idea:
SecIdentityRef _clientIdentity = [XYZ getClientIdentityWithLabel: certLabel];
if(_clientIdentity) {
CFRetain(_clientIdentity);
}
SecCertificateRef _clientCertificate = [XYZ getClientCertificateWithLabel:certLabel];
if(_clientCertificate) {
CFRetain(_clientCertificate);
}
...
- (void)URLSession:(nullable NSURLSession *)session
task:(nullable NSURLSessionTask *)task
didReceiveChallenge:(nullable NSURLAuthenticationChallenge *)challenge
completionHandler:(nullable void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate) {
// supply the appropriate client certificate
id bridgedCert = (__bridge id)_clientCertificate;
NSArray* certificates = bridgedCert ? #[bridgedCert] : #[];
NSURLCredential* credential = [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceForSession];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
}
This code took a lot of time to get right. iOS certificate stuff is exceedingly poorly documented, hopefully this helps.

The usual way to generate SSL certificates is that private key is used to generate the CSR, Certificate Signing Request info. In fact, you're hidding as well company, email, etc info with that key signature. With that CSR, then, you sign your certificate, so it will be associated with your private key and info stored in CSR, nevermind the public key. I'm currently not able to see in IOS CSR Generation project where you can pass your generated key: seems to me that CSR generated with IOS CSR Generation project is using it's own generated key, or no private key at all. That will got then logic with the fact that you cannot extract private key from CER or DER, because it isn't there.

Related

How to securely implement biometry with fallback application password to authenticate signing within Secure Enclave?

I am looking to implement a local authentication flow similar to many banking apps in an iOS (Swift) app for using a key in the Secure Enclave:
By default you set up an app-specific pin code
The user is then able to turn on biometry (Face ID or Touch ID) for quick authentication
The user can then sign a message using their private key stored in the Secure Enclave by either using biometrics or the user can fall back to their app-specific pin (either by choice, or if they cancel or can't be recognized for example).
What I've tried so far
The .applicationPassword flag for SecAccessControlCreateFlags seemed like a reasonable option to allow an application-defined password to be used. Furthermore, the set of possible flags also contain constraints such as such as devicePasscode and biometryCurrentSet to set access constraints that can even be combined by using conjunctions. It should be noted that applicationPassword is not a constraint but an 'option' according to the docs. It's also not very well-documented what this flag actually does. Still, I tried the following:
let flags1: SecAccessControlCreateFlags = [.privateKeyUsage,
.biometryCurrentSet, .or, .applicationPassword]
let flags2: SecAccessControlCreateFlags = [.privateKeyUsage,
.biometryCurrentSet, .applicationPassword]
let access1 = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
flags1,
&error)
let access2 = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
flags2,
&error)
Both of these options seem to work essentially the same, where first the phone prompts for biometric verification and then shows a prompt where you can enter the application password.
I tried programmatically supplying the application password using LAContext.setCredential, and this works fine as the application password prompt will no longer be shown, but iOS will still always prompt for biometrics as well (even when using the .or flag). Thus, it seems that the .or flag is not working as I had hoped together with .applicationPassword. However, these access control policies do seem to enforce that both biometrics and application password should pass, which is a nice possibility but not exactly what I was looking for.
I have also tried preventing the respective prompts such as the Face ID prompt from being shown with LAContext.interactionNotAllowed but this also does not work because of the access control flags.
Basic setup
For testing this, I have a simple iOS app set up with the following functions derived from the Secure Enclave documentation:
func getAccessControl() -> SecAccessControl {
var access: SecAccessControl?
access = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage,
.biometryCurrentSet,
.or,
.applicationPassword],
nil)!
return access
}
func generatePrivateKey() throws -> SecKey {
let context = LAContext()
context.setCredential("pwd123".data(using: .utf8), type: .applicationPassword)
let attributes: NSDictionary = [
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits: 256,
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
kSecUseAuthenticationContext as String: context,
kSecAttrLabel: "label-for-reference",
kSecClass: kSecClassKey,
kSecPrivateKeyAttrs: [
kSecAttrIsPermanent: true,
kSecAttrApplicationTag: "tag-for-reference",
kSecAttrAccessControl: getAccessControl(),
]
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes, &error) else {
throw error!.takeRetainedValue() as Error
}
return privateKey
}
func retrieveKey(context: LAContext? = nil) -> SecKey? {
var attributes: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrLabel as String: "label-for-reference",
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnRef as String: true,
]
if let context = context {
attributes[kSecUseAuthenticationContext as String] = context
}
var item: CFTypeRef?
let res = SecItemCopyMatching(attributes as CFDictionary, &item)
if (res == errSecSuccess) {
return (item as! SecKey)
} else {
return nil
}
}
func sign(data: String, key: SecKey) throws -> Data? {
if (SecKeyIsAlgorithmSupported(key, .sign, .ecdsaSignatureMessageX962SHA256)) {
var error: Unmanaged<CFError>?
guard let signature = SecKeyCreateSignature(key,
.ecdsaSignatureMessageX962SHA256,
data.data(using: .utf8)! as CFData,
&error) as Data? else {
throw error!.takeRetainedValue() as Error
}
return signature
}
return nil
}
I can then generate a signature by doing something like this (leaving out some details):
let privateKey = generatePrivateKey()
let context = LAContext()
context.setCredential("pwd123".data(using: .utf8), type: .applicationPassword)
let retrievedKey = retrieveKey(context)
var signedMessage: Data?
try signedMessage = sign(data: "abc", key: retrievedKey!))
What I'd like to learn
I'd like to learn if I'm missing something in this documentation (specifically on the access control of [secure enclave] keychain items), or alternatively, if there is a particular workaround to make this work. For example, I've also thought about chaining the access options, e.g. Face ID --unlocks--> Application passcode --unlocks--> Private key so that the application passcode can either be provided by using Face ID first or directly by the user. However, this would load the application passcode in memory (which may not be an issue when the user already enters their pin code in a custom user interface anyway). How do apps with similar functionality generally solve this?

ios kSecClassCertificate yields -25303

I'm trying to store p12(pfx) certificate into keychain on ios
with code from keychainswift essentially, just the klass changed from password to certificate:
#discardableResult
open func setCertificate(_ value: Data, forKey key: String,
withAccess access: KeychainSwiftAccessOptions? = nil) -> Bool {
// The lock prevents the code to be run simultaneously
// from multiple threads which may result in crashing
lock.lock()
defer { lock.unlock() }
deleteNoLock(key) // Delete any existing key before saving it
let accessible = access?.value ?? KeychainSwiftAccessOptions.defaultOption.value
let prefixedKey = keyWithPrefix(key)
var query: [String : Any] = [
KeychainSwiftConstants.klass : kSecClassCertificate,
KeychainSwiftConstants.attrAccount : prefixedKey,
KeychainSwiftConstants.valueData : value,
KeychainSwiftConstants.accessible : accessible
]
query = addAccessGroupWhenPresent(query)
query = addSynchronizableIfRequired(query, addingItems: true)
lastQueryParameters = query
lastResultCode = SecItemAdd(query as CFDictionary, nil)
return lastResultCode == noErr
}
getting -25303 (invalid attribute that is)
Should I piecemeal store separetely identity, certificate chain and trust with different keys for this to work?
what's the difference between kSecClassCertificate and kSecClassPassword klasses of storage given that we
have keychains sandboxes and without GUI?
is this for forward compatibility with macos or something?
PS. Apple please attempt to find a tech writer who can fix the horrendous documentation around security framework. Thanks!
All the available attributes for a certificate are mentioned in the documentation. I believe your attrAccount is an invalid parameter and I feel, but I'm not sure that the valueData might be invalid. So remove at least the attrAccount and possibly the valueData.

`SecKey` object creation for RSA failing, Error Domain Code=-50 "RSA private key creation from data failed swift-iOS

I'm not able to create SecKey object from below private key, I did try many answers available here but nothing is helping.
My swift piece of code is as below:
var error: Unmanaged<CFError>?
guard let keyData = Data(base64Encoded: key) else {
return nil
}
var keyAttributes: CFDictionary {
return [kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecAttrKeySizeInBits: keySize] as CFDictionary
}
guard let secKey = SecKeyCreateWithData(keyData as CFData, keyAttributes, &error) else {
print(error.debugDescription) //Error Domain Code=-50 "RSA private key creation from data failed swift-iOS
return nil
}
The expected result is secKey should have valid value and above guard should not fail.
Note: Public key conversion to the respective secKey is working perfectly fine (the problem is with the only private key while decryption). I have tried removing \r\n from the above key.
After searching a lot, found this Apple thread helpful.
I'm able to manage this ASN.1 parsing using SwiftyRSA library.
let privateKeySwifty = try PrivateKey(pemEncoded: privateKey)
let secPrivateKey = try PrivateKey(reference: privateKeySwifty.reference)
After digging deeper, I can see, there was a need to stripe the header of keyData (ASN.1 Parsing).

Keychain Query Always Returns errSecItemNotFound After Upgrading to iOS 13

I am storing passwords into the iOS keychain and later retrieving them to implement a "remember me" (auto-login) feature on my app.
I implemented my own wrapper around the Security.framework functions (SecItemCopyMatching(), etc.), and it was working like a charm up until iOS 12.
Now I am testing that my app doesn't break with the upcoming iOS 13, and lo and behold:
SecItemCopyMatching() always returns .errSecItemNotFound
...even though I have previously stored the data I am querying.
My wrapper is a class with static properties to conveniently provide the values of the kSecAttrService and kSecAttrAccount when assembling the query dictionaries:
class LocalCredentialStore {
private static let serviceName: String = {
guard let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String else {
return "Unknown App"
}
return name
}()
private static let accountName = "Login Password"
// ...
I am inserting the password into the keychain with code like the following:
/*
- NOTE: protectWithPasscode is currently always FALSE, so the password
can later be retrieved programmatically, i.e. without user interaction.
*/
static func storePassword(_ password: String, protectWithPasscode: Bool, completion: (() -> Void)? = nil, failure: ((Error) -> Void)? = nil) {
// Encode payload:
guard let dataToStore = password.data(using: .utf8) else {
failure?(NSError(localizedDescription: ""))
return
}
// DELETE any previous entry:
self.deleteStoredPassword()
// INSERT new value:
let protection: CFTypeRef = protectWithPasscode ? kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly : kSecAttrAccessibleWhenUnlocked
let flags: SecAccessControlCreateFlags = protectWithPasscode ? .userPresence : []
guard let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
protection,
flags,
nil) else {
failure?(NSError(localizedDescription: ""))
return
}
let insertQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessControl: accessControl,
kSecValueData: dataToStore,
kSecUseAuthenticationUI: kSecUseAuthenticationUIAllow,
kSecAttrService: serviceName, // These two values identify the entry;
kSecAttrAccount: accountName // together they become the primary key in the Database.
]
let resultCode = SecItemAdd(insertQuery as CFDictionary, nil)
guard resultCode == errSecSuccess else {
failure?(NSError(localizedDescription: ""))
return
}
completion?()
}
...and later, I am retrieving the password with:
static func loadPassword(completion: #escaping ((String?) -> Void)) {
// [1] Perform search on background thread:
DispatchQueue.global().async {
let selectQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: serviceName,
kSecAttrAccount: accountName,
kSecReturnData: true,
kSecUseOperationPrompt: "Please authenticate"
]
var extractedData: CFTypeRef?
let result = SecItemCopyMatching(selectQuery, &extractedData)
// [2] Rendez-vous with the caller on the main thread:
DispatchQueue.main.async {
switch result {
case errSecSuccess:
guard let data = extractedData as? Data, let password = String(data: data, encoding: .utf8) else {
return completion(nil)
}
completion(password) // < SUCCESS
case errSecUserCanceled:
completion(nil)
case errSecAuthFailed:
completion(nil)
case errSecItemNotFound:
completion(nil)
default:
completion(nil)
}
}
}
}
(I don't think any of the entries of the dictionaries I use for either call has an inappropriate value... but perhaps I am missing something that just happened to "get a pass" until now)
I have set up a repository with a working project (Xcode 11 beta) that demonstrates the problem.
The password storing always succeeds; The password loading:
Succeeds on Xcode 10 - iOS 12 (and earlier), but
Fails with .errSecItemNotFound on Xcode 11 - iOS 13.
UPDATE: I can not reproduce the issue on the device, only Simulator. On the device, the stored password is retrieved successfully.
Perhaps this is a bug or limitation on the iOS 13 Simulator and/or iOS 13 SDK for the x86 platform.
UPDATE 2: If someone comes up with an alternative approach that somehow works around the issue (whether by design or by taking advantage of some oversight by Apple), I will accept it as an answer.
I've had a similar issue where I was getting errSecItemNotFound with any Keychain-related action but only on a simulator. On real device it was perfect, I've tested with latest Xcodes (beta, GM, stable) on different simulators and the ones that were giving me a hard time were iOS 13 ones.
The problem was that I was using kSecClassKey in query attribute kSecClass, but without the 'required' values (see what classes go with which values here) for generating a primary key:
kSecAttrApplicationLabel
kSecAttrApplicationTag
kSecAttrKeyType
kSecAttrKeySizeInBits
kSecAttrEffectiveKeySize
And what helped was to pick kSecClassGenericPassword for kSecClass and provide the 'required' values for generating a primary key:
kSecAttrAccount
kSecAttrService
See here on more about kSecClass types and what other attributes should go with them.
I came to this conclusion by starting a new iOS 13 project and copying over the Keychain wrapper that was used in our app, as expected that did not work so I've found this lovely guide on using keychain here and tried out their wrapper which no surprise worked, and then went line by line comparing my implementation with theirs.
This issue already reported in radar: http://openradar.appspot.com/7251207
Hope this helps.
After half a day of experimentation I discovered that using a pretty basic instance of kSecClassGenericPassword I had the problem on both the simulator and real hardware. After having a read over of the docs I noticed that kSecAttrSynchronizable has a kSecAttrSynchronizableAny. To accept any value for any other attribute, you simply don't include it in the query. That's a clue.
I found that when I included kSecAttrSynchronizable set to kSecAttrSynchronizableAny the queries all worked. Of course I could also set it to either kCFBooleanTrue (or *False) if I actually do want to filter on that value.
Given that attribute everything seems to work as expected for me. Hopefully this will save some other people a half day of mucking around with test code.
Update
Due to enhanced security requirements from above, I changed the access attribute from kSecAttrAccessibleWhenUnlocked to kSecAttrAccessibleWhenUnlockedThisDeviceOnly (i.e., prevent the password from being copied during device backups).
...And now my code is broken again! This isn't an issue of trying to read the password stored with the attribute set to kSecAttrAccessibleWhenUnlocked using a dictionary that contains kSecAttrAccessibleWhenUnlockedThisDeviceOnly instead, no; I deleted the app and started from scratch, and it still fails.
I have posted a new question (with a link back to this one).
Original Answer:
Thanks to the suggestion by #Edvinas in his answer above, I was able to figure out what was wrong.
As he suggests, I downloaded the Keychain wrapper class used in this Github repository (Project 28), and replaced my code with calls to the main class, and lo and behold - it did work.
Next, I added console logs to compare the query dictionaries used in the Keychain wrapper for storing/retrieving the password (i.e., the arguments to SecItemAdd() and SecItemCopyMatching) against the ones I was using. There were several differences:
The wrapper uses Swift Dictionary ([String, Any]), and my code uses NSDictionary (I must update this. It's 2019 already!).
The wrapper uses the bundle identifier for the value of kSecAttrService, I was using CFBundleName. This shouldn't be an issue, but my bundle name contains Japanese characters...
The wrapper uses CFBoolean values for kSecReturnData, I was using Swift booleans.
The wrapper uses kSecAttrGeneric in addition to kSecAttrAccount and kSecAttrService, my code only uses the latter two.
The wrapper encodes the values of kSecAttrGeneric and kSecAttrAccount as Data, my code was storing the values directly as String.
My insert dictionary uses kSecAttrAccessControl and kSecUseAuthenticationUI, the wrapper doesn't (it uses kSecAttrAccessible with configurable values. In my case, I believe kSecAttrAccessibleWhenUnlocked applies).
My retrieve dictionary uses kSecUseOperationPrompt, the wrapper doesn't
The wrapper specifies kSecMatchLimit to the value kSecMatchLimitOne, my code doesn't.
(Points 6 and 7 are not really necessary, because although I first designed my class with biometric authentication in mind, I am not using it currently.)
...etc.
I matched my dictionaries to those of the wrapper and finally got the copy query to succeed. Then, I removed the differing items until I could pinpoint the cause. It turns out that:
I don't need kSecAttrGeneric (just kSecAttrService and kSecAttrAccount, as mentioned in #Edvinas's answer).
I don't need to data-encode the value of kSecAttrAccount (it may be a good idea, but in my case, it would break previously stored data and complicate migration).
It turns out kSecMatchLimit isn't needed either (perhaps because my code results in a unique value stored/matched?), but I guess I will add it just to be safe (doesn't feel like it would break backward compatibility).
Swift booleans for e.g. kSecReturnData work fine. Assigning the integer 1 breaks it though (although that's how the value is logged on the console).
The (Japanese) bundle name as a value for kSecService is ok too.
...etc.
So in the end, I:
Removed kSecUseAuthenticationUI from the insert dictionary and replaced it with kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked.
Removed kSecUseAuthenticationUI from the insert dictionary.
Removed kSecUseOperationPrompt from the copy dictionary.
...and now my code works. I will have to test whether this load passwords stored using the old code on actual devices (otherwise, my users will lose their saved passwords on the next update).
So this is my final, working code:
import Foundation
import Security
/**
Provides keychain-based support for secure, local storage and retrieval of the
user's password.
*/
class LocalCredentialStore {
private static let serviceName: String = {
guard let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String else {
return "Unknown App"
}
return name
}()
private static let accountName = "Login Password"
/**
Returns `true` if successfully deleted, or no password was stored to begin
with; In case of anomalous result `false` is returned.
*/
#discardableResult static func deleteStoredPassword() -> Bool {
let deleteQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
kSecAttrService: serviceName,
kSecAttrAccount: accountName,
kSecReturnData: false
]
let result = SecItemDelete(deleteQuery as CFDictionary)
switch result {
case errSecSuccess, errSecItemNotFound:
return true
default:
return false
}
}
/**
If a password is already stored, it is silently overwritten.
*/
static func storePassword(_ password: String, protectWithPasscode: Bool, completion: (() -> Void)? = nil, failure: ((Error) -> Void)? = nil) {
// Encode payload:
guard let dataToStore = password.data(using: .utf8) else {
failure?(NSError(localizedDescription: ""))
return
}
// DELETE any previous entry:
self.deleteStoredPassword()
// INSERT new value:
let insertQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
kSecValueData: dataToStore,
kSecAttrService: serviceName, // These two values identify the entry;
kSecAttrAccount: accountName // together they become the primary key in the Database.
]
let resultCode = SecItemAdd(insertQuery as CFDictionary, nil)
guard resultCode == errSecSuccess else {
failure?(NSError(localizedDescription: ""))
return
}
completion?()
}
/**
If a password is stored and can be retrieved successfully, it is passed back as the argument of
`completion`; otherwise, `nil` is passed.
Completion handler is always executed on themain thread.
*/
static func loadPassword(completion: #escaping ((String?) -> Void)) {
// [1] Perform search on background thread:
DispatchQueue.global().async {
let selectQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
kSecAttrService: serviceName,
kSecAttrAccount: accountName,
kSecMatchLimit: kSecMatchLimitOne,
kSecReturnData: true
]
var extractedData: CFTypeRef?
let result = SecItemCopyMatching(selectQuery, &extractedData)
// [2] Rendez-vous with the caller on the main thread:
DispatchQueue.main.async {
switch result {
case errSecSuccess:
guard let data = extractedData as? Data, let password = String(data: data, encoding: .utf8) else {
return completion(nil)
}
completion(password)
case errSecUserCanceled:
completion(nil)
case errSecAuthFailed:
completion(nil)
case errSecItemNotFound:
completion(nil)
default:
completion(nil)
}
}
}
}
}
Final Words Of Wisdom: Unless you have a strong reason not to, just grab the Keychain Wrapper that #Edvinas mentioned in his answer (this repository, project 28)) and move on!
Regarding the issue in kSecClassGenericPassword, I was trying to understand what is the problem and I found a solution for that.
Basically it seems like Apple was fixing an issue with kSecAttrAccessControl, so below iOS version 13 you add keyChain object with kSecAttrAccessControl without biometric identity and above iOS 13 that does not work anymore in a simulator.
So the solution for that is when you want to encrypt the keyChain object with biometric you need to add kSecAttrAccessControl to your query but if you don't need to encrypted by biometric you need to add only kSecAttrAccessible that's the right way to do these.
Examples
Query for biometric encrypt:
guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlocked,
userPresence,
nil) else {
// failed to create accessControl
return
}
var attributes: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
kSecAttrService: "Your service",
kSecAttrAccount: "Your account",
kSecValueData: "data",
kSecAttrAccessControl: accessControl]
Query for regular KeyChain (without biometric):
var attributes: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
kSecAttrService: "Your service",
kSecAttrAccount: "Your account",
kSecValueData: "data",
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]
We had the same issue when generating a key pair - works just fine on devices, but on simulator iOS 13 and above it cannot find the key when we try to retreive it later on.
The solution is in Apple documentation: https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain
When you generate keys yourself, as described in Generating New
Cryptographic Keys, you can store them in the keychain as an implicit
part of that process. If you obtain a key by some other means, you can
still store it in the keychain.
In short, after you create a key with SecKeyCreateRandomKey, you need to save this key in the Keychain using SecItemAdd:
var error: Unmanaged<CFError>?
guard let key = SecKeyCreateRandomKey(createKeyQuery as CFDictionary, &error) else {
// An error occured.
return
}
let saveKeyQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecValueRef as String: key
]
let status = SecItemAdd(saveKeyQuery as CFDictionary, nil)
guard status == errSecSuccess else {
// An error occured.
return
}
// Success!

Swift Keychain - Storing OAuth Credentials: "The specified item already exists in the keychain"

I'm building an iOS app for my website and I'm attempting to use OAuth2 to manage login credentials. On user login, I'm successfully hitting my authentication endpoint with the provided username and password and I'm attempting to store both the Access Token and the Refresh Token in the keychain, so the user doesn't have to provide credentials moving forward.
I'm having trouble storing both refresh token and access token in my keychain, following instructions from these sources:
Adding a Password to the Keychain
Searching for Keychain Items
Updating and Deleting Keychain Items
I'm able to successfully store either the Access Token or the Refresh Token, but no matter which one I store first, when attempting to store the other, I receive the following error message: "The specified item already exists in the keychain."
I added a CheckForExisting function to delete any existing items with the same specifications, but when I attempt to delete the existing keychain item using the same query, I receive a errSecItemNotFound status. So, frustratingly enough, I'm being told that I can't create my item because it already exists, but I can't delete the existing item because no existing item exists.
My hypothesis is that the creation of the Access Token item blocks the creation of the Refresh Token item, so I'm hoping someone can shed some light on the following:
Why is the second item creation being blocked? Does the Keychain have some built in primary key checks that I'm hitting (like can't store more than one kSecClassInternetPassword)?
What's the proper way to differentiate between the two tokens. Right now I'm using kSecAttrLabel, but that's a shot in the dark.
Please note that I'm hoping for an explanation of why my current approach is failing. I absolutely welcome alternative implementations, but I really want to understand what is going on behind the scenes here, so if possible please include an explanation of where an alternative implementation avoids the pitfalls that I seem to have fallen prey to.
Swift4 Code to Store the Tokens:
func StoreTokens(username: String, access_token: String, refresh_token: String) throws {
func CheckForExisting(query: [String: Any]) throws {
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
let error_message = SecCopyErrorMessageString(status, nil)!
throw KeychainError.unhandledError(status: error_message)
}
}
let configuration = ConfigurationDetails()
let server = configuration.server
let access_token = access_token.data(using: String.Encoding.utf8)!
let refresh_token = refresh_token.data(using: String.Encoding.utf8)!
let access_token_query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server,
kSecAttrLabel as String: "AccessToken",
kSecValueData as String: access_token
]
let refresh_token_query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server,
kSecAttrLabel as String: "RefreshToken",
kSecValueData as String: refresh_token
]
try CheckForExisting(query: access_token_query)
let access_status = SecItemAdd(access_token_query as CFDictionary, nil)
guard access_status == errSecSuccess else {
let error_message = SecCopyErrorMessageString(access_status, nil)!
throw KeychainError.unhandledError(status: error_message)
}
try CheckForExisting(query: refresh_token_query)
let refresh_status = SecItemAdd(refresh_token_query as CFDictionary, nil)
guard refresh_status == errSecSuccess else {
let error_message = SecCopyErrorMessageString(refresh_status, nil)!
throw KeychainError.unhandledError(status: error_message)
}
}
According this https://developer.apple.com/documentation/security/errsecduplicateitem looks like the unique key for class kSecClassInternetPassword contains only these properties:
kSecAttrAccount, kSecAttrSecurityDomain, kSecAttrServer, kSecAttrProtocol, kSecAttrAuthenticationType, kSecAttrPort, and kSecAttrPath.
So, kSecAttrLabel is not in the list, and your refresh_token_query duplicates access_token_query.

Resources