Same here. iPhone 6, iOS 9.2 (13C75), using Alamofire and AlamofireImage. I’m working in Swift, with a Keychain struct I created myself.
After three or four retrievals (it’s pretty consistent), SecItemCopyMatching returns -34018. (See Keychain.password(), below; the error hits at the call to SecItemCopyMatching().) Resetting the password during the run of the app does not help; killing and restarting it clears the error for another three or four cycles, which I don’t have to say is unacceptable. The stored password is still there; Keychain.password() returns it as expected. The bug manifests even if no other SecItem* call is made.
(I see I don’t release returnPointer in that func; I’ll correct that in my own copy. I don’t think it should affect the bug, it’s just an 8-byte leak. I’m leaving the fix out of this listing to preserve my example.)
I use the password by embedding a hash in the body of my transactions. (I use POSTs or PUTs, even in cases that would ordinarily use GETs.) I don’t claim this is what an experienced developer would do; I’m just describing the circumstances. All transactions are performed through Alamofire; this sometimes entails a loop of transactions such as the upload of a few images; the password is retrieved only once for each series.
Quinn’s previous suggestion that this may be an entitlements or signing issue doesn’t seem to hold up here:
- The app runs impeccably in the simulator (which says very little about signing).
- SecItemCopyMatching() does work a few times before failing. (Three times, I think — always the same number.)
- I’ve winnowed out some signing identities that might have conflicted; this came up because…
- … the forced codesign script phase (suggested elsewhere) complained it couldn’t disambiguate the identity CNs. Cutting the certificates down silenced that error but did not stop the -34018.
- I’ve copied and pasted among the bundle ID and keychain-sharing group entitlement.
- I’ve registered the bundle ID specifically with Apple (should not be necessary).
- I’ve forced the provisioning profile and signature.
- I’ve allowed Xcode to repair all the damage I did in those last few steps.
I’m appending my Keychain struct for thoroughness. I DO NOT recommend it for use in others’ work. I have not qualified it as a product. Developers must be particularly on their guard about security practices, and I have no illusions about mine.
//
// Keychain.swift
// Incident
//
// Created by Fritz Anderson on 11/28/15.
// Copyright © 2015 The University of Chicago. All rights reserved.
//
import Foundation
import Security
import CommonCrypto
func hashPassword(password: String) -> NSData {
let hashLength = 256/8
let stringData = password.dataUsingEncoding(NSUTF8StringEncoding)!
let digestBytes = UnsafeMutablePointer.alloc(hashLength)
CC_SHA256(stringData.bytes, UInt32(stringData.length), digestBytes)
return NSData(bytes: digestBytes, length: hashLength)
}
extension NSData {
var hexString: String {
let contents = UnsafeBufferPointer(
start: UnsafePointer(bytes),
count: length)
return contents.reduce("") {
$0 + String($1, radix: 16)
}
}
}
func credentialsDictionary(user: String? = nil,
host: String? = nil) throws -> [String: AnyObject]
{
let userName = user ?? (DefaultsKeys.AccountName.value() as! String)
var retval: [String: AnyObject] = ["account" : userName]
let hostName = host ?? Globals.HostName.value!
var chain = Keychain(user: userName, host: hostName)
if let password = try chain.password() {
let hash = hashPassword(password).hexString
retval["password"] = hash
}
else {
throw KeychainError.NoPasswordDefined(user: userName, host: hostName)
}
return retval
}
func credentialsJSONData(user: String? = nil,
host: String? = nil) throws -> NSData
{
let dict = try credentialsDictionary(user, host: host)
return try NSJSONSerialization.dataWithJSONObject(dict, options: [])
}
public enum KeychainError : ErrorType {
case Duplicate
case OperationOnMissing
case NotAuthorized
case Parameter
case Generic(osError: Int)
case NoPasswordDefined(user: String, host: String)
/// The `KeychainError` case matching an `OSStatus` value.
///
/// - parameters:
/// - status: the `OSStatus` to match
///
/// - returns: Optional `KeychainError`: `nil` if the code was `noErr`; `.Generic` if it was not covered by any of the other cases; or one of those cases if it matches a recognized code. See the source for this `func` for details.
public static func fromOSStatus(status: OSStatus) -> KeychainError? {
switch status {
case noErr: return nil
case errSecDuplicateItem: return .Duplicate
case errSecItemNotFound: return .OperationOnMissing
case errSecAuthFailed: return .NotAuthorized
case -50: return .Parameter
default: return .Generic(osError: Int(status))
}
}
/// A description of the represented error, tagged with an optional context string.
///
/// The optional context string is prepended to the returned description. Example, without a context:
///
/// "There was no such entry."
///
/// If the context is "changing the password":
///
/// "While changing the password: There was no such entry."
///
/// Notice that the context string is wrapped in "While _ : ". Adjust your phrasing accordingly.
///
/// - parameters:
/// - attempting: `nil` (the default) if there is to be no tag; otherwise a string to insert in the description.
///
/// - returns: A description of the error, including the context string if one was supplied.
public func explain(attempting: String? = nil) -> String {
let circumstance: String
if let attempting = attempting {
circumstance = "While \(attempting): "
}
else { circumstance = "" }
let expansion: String
switch self {
case .Duplicate: expansion = "Another entry was already there."
case .OperationOnMissing: expansion = "There was no such entry."
case .NotAuthorized: expansion = "The operation was not authorized."
case .Parameter: expansion = "An internal error (paramErr) occurred.\n\n"
+ "Please report this to me@example.com"
case let .Generic(osError): expansion = "An unexpected error occurred (\(osError))."
case let .NoPasswordDefined(user: user, host: host):
expansion = "No password was defined for \(user) on \(host)."
}
return circumstance + expansion
}
}
/**
Convenient access to the system keychain.
`struct Keychain` simplifies CRUD operations on the system
keychain by wrapping every search and initialization parameter
internally, where client code can’t see.
There is no custom initializer; call `Keychain(user:,host:)`
The client initializes the struct with username and host.
After that,
* `password()` retrieves the password, if the matching
record is in the keychain.
* `setPassword(_:)` creates or updates the keychain record.
* `delete()` removes the record.
Getting or setting the password is always a mutation, because the underlying Security Framework calls set `osStatus`.
- throws: Any of a number of `KeychainError`s.
The `enum` specifies the likeliest errors. Any other is thrown
as `KeychainError.Generic`; examine `osStatus` for the code.
*/
struct Keychain {
// MARK: Publicly-accessible data
/// The login ID for the user, such as `criedel`.
let user: String
/// The name of the host for the user’s account, such as `www.example.com`.
let host: String
/// The error value (or `noErr`) from the last Keychain call.
var osStatus: OSStatus = noErr
init(user: String, host: String) {
self.user = user
self.host = host
}
// MARK: Getter/setter for password
/// The password for the user + host as stored in the keychain.
///
/// The absence of the user + host, or a password for the combination,
/// is one of the expected outcomes; the function will return `nil`
/// in that case.
///
/// This `func` takes care of the **Retrieve** part of the CRUD pattern.
///
/// - returns: the password if the user + host is registered, and a password is set; otherwise `nil`
///
/// - throws: Any of the `KeychainError`s for a misconfigured or forbidden access.
mutating func password() throws -> String? {
let returnPointer = UnsafeMutablePointer.alloc(1)
returnPointer.initialize(nil)
var terms = searchPattern()
terms[kSecReturnData] = true
osStatus = SecItemCopyMatching(terms, returnPointer)
switch osStatus {
case noErr:
if let password = (returnPointer.memory as? NSData),
retval = String(data: password, encoding: NSUTF8StringEncoding)
{ return retval }
else { return nil }
case errSecItemNotFound:
return nil
default:
throw KeychainError.fromOSStatus(osStatus)!
}
}
/// Set a (new) password for the (new) user + host.
///
/// * If the combination is registered, but the password is different, the record is updated.
/// * If they are not registered, the record is created, including the desired password.
///
/// This `func` takes care of both the **Create** and **Update** parts of the CRUD pattern.
///
/// - throws: Any of the `KeychainError` errors thrown by the underlying implementation.
///
/// - parameters:
/// - newPassword: The password to set for the user + host. The `func` will not validate it.
mutating func setPassword(newPassword: String) throws {
if let old = try? password(),
oldPassword = old
{
if newPassword == oldPassword {
// The password is already set. Nothing to do.
return
}
else {
// Item exists but must be updated
try updatePasswordInKeychain(newPassword)
}
}
else {
// nil oldPassword, meaning the item must be created.
try createKeychainItemWithPassword(newPassword)
}
}
/// Remove the user + host combination from the keychain.
///
/// This `func` takes care of the **Delete** part of the CRUD pattern.
///
/// - throws: Any of the `KeychainError` errors thrown by the underlying implementation.
mutating func delete() throws {
osStatus = SecItemDelete(searchPattern())
if let error = KeychainError.fromOSStatus(osStatus) {
throw error
}
}
// MARK: Convenience keychain access
/// A minimal dictionary of search criteria for the user + host
private func searchPattern() -> [NSString: AnyObject] {
let retval: [NSString: AnyObject] = [
// Entry class
kSecClass : kSecClassInternetPassword,
kSecAttrProtocol : kSecAttrProtocolHTTPS,
kSecAttrAuthenticationType
: kSecAttrAuthenticationTypeHTTPBasic,
kSecAttrAccount : user,
kSecAttrServer : host
]
return retval
}
/// Create a keychain item for the user + host, including password.
///
/// It is assumed that no item matching the user + host is in the keychain.
///
/// - parameters:
/// - password: The password to store in the item.
///
/// - throws: Any of the `KeychainError` errors, most likely `.Duplicate`.
private mutating func createKeychainItemWithPassword(password: String) throws {
let passwordData = password.dataUsingEncoding(NSUTF8StringEncoding)!
var values = searchPattern()
values[kSecValueData] = passwordData
// Prevent synchronization with other devices:
values[kSecAttrSynchronizable] = false
// The device has to have been unlocked since last restart, and will not be transferred to another device (as from backup)
values[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
osStatus = SecItemAdd(values, nil)
if let error = KeychainError.fromOSStatus(osStatus) {
throw error
}
}
/// Replace the password for an existing keychain item for user + host.
///
/// It is assumed that an item matching the user + host is in the keychain.
///
/// - parameters:
/// - password: The password to store in the item.
///
/// - throws: Any of the `KeychainError` errors, most likely `.OperationOnMissing`.
private mutating func updatePasswordInKeychain(password: String) throws {
let passwordData = password.dataUsingEncoding(NSUTF8StringEncoding)!
let updateData: [NSString: AnyObject] = [ kSecValueData: passwordData,
// Prevent synchronization with other devices:
kSecAttrSynchronizable: false,
// The device has to have been unlocked since last restart, and will not be transferred to another device (as from backup)
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
]
osStatus = SecItemUpdate(searchPattern(), updateData)
if let error = KeychainError.fromOSStatus(osStatus) {
throw error
}
}
}