Revision control

Copy as Markdown

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
open class FxAKeychain {
public private(set) var serviceName: String
public private(set) var accessGroup: String?
static var baseBundleIdentifier: String {
let bundle = Bundle.main
let packageType = bundle.object(forInfoDictionaryKey: "CFBundlePackageType") as? String
let baseBundleIdentifier = bundle.bundleIdentifier!
if packageType == "XPC!" {
let components = baseBundleIdentifier.components(separatedBy: ".")
return components[0 ..< components.count - 1].joined(separator: ".")
}
return baseBundleIdentifier
}
static var shared: FxAKeychain?
static func sharedAppContainerKeychainForFxA(keychainAccessGroup: String?) -> FxAKeychain {
if let s = shared {
return s
}
let wrapper = FxAKeychain(serviceName: baseBundleIdentifier, accessGroup: keychainAccessGroup)
shared = wrapper
return wrapper
}
public init(serviceName: String,
accessGroup: String? = nil)
{
self.serviceName = serviceName
self.accessGroup = accessGroup
}
struct KeychainError: Error {
let errorMessage: String
}
func ensureStringItemAccessibility(
_ accessibility: FxAKeychainItemAccessibility,
forKey key: String
) {
if hasValue(key: key) {
if accessibilityOfKey(key) != accessibility {
FxALog.info("ensureStringItemAccessibility: updating item \(key) with \(accessibility)")
guard let value = getKeyValue(key: key) else {
FxALog.error("ensureStringItemAccessibility: failed to get item \(key)")
return
}
if !removeObject(key: key) {
FxALog.error("ensureStringItemAccessibility: failed to remove item \(key)")
}
if !setKeyValue(value, key: key, accessibility: accessibility) {
FxALog.error("ensureStringItemAccessibility: failed to update item \(key)")
}
}
}
}
open func accessibilityOfKey(_ key: String) -> FxAKeychainItemAccessibility? {
var keychainQueryDictionary = getBaseKeychainQuery(key: key)
// Remove accessibility attribute
keychainQueryDictionary.removeValue(forKey: kSecAttrAccessible as String)
// Limit search results to one
keychainQueryDictionary[kSecMatchLimit as String] = kSecMatchLimitOne
// Specify we want SecAttrAccessible returned
keychainQueryDictionary[kSecReturnAttributes as String] = kCFBooleanTrue
// Search
var result: AnyObject?
let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &result)
guard status == noErr,
let resultsDictionary = result as? [String: AnyObject],
let accessibilityAttrValue = resultsDictionary[kSecAttrAccessible as String] as? String
else {
return nil
}
return FxAKeychainItemAccessibility(rawValue: accessibilityAttrValue)
}
func setKeyValue(_ value: String, key: String, accessibility: FxAKeychainItemAccessibility? = nil) -> Bool {
let accessibilityValue = accessibility != nil ? accessibility : FxAKeychainItemAccessibility.whenUnlocked
return addOrUpdateKeychainKey(value, key: key, accessibility: accessibilityValue)
}
func getKeyValue(key: String, accessibility: FxAKeychainItemAccessibility? = nil) -> String? {
return getDataFromResult(queryKeychainForKey(key: key, accessibility: accessibility))
}
func queryKeychainForKey(key: String,
accessibility: FxAKeychainItemAccessibility? = nil) -> Result<String?, Error>
{
var keychainQueryDictionary = getBaseKeychainQuery(key: key, accessibility: accessibility)
keychainQueryDictionary[kSecMatchLimit as String] = kSecMatchLimitOne
keychainQueryDictionary[kSecReturnData as String] = kCFBooleanTrue
var queryResult: AnyObject?
let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &queryResult)
guard status == noErr else {
let errMsg = SecCopyErrorMessageString(status, nil)
return .failure(KeychainError(errorMessage: errMsg as? String ?? ""))
}
guard let data = queryResult as? Data else {
return .failure(KeychainError(errorMessage: "Unable to encode query result"))
}
return .success(String(data: data, encoding: .utf8))
}
func removeObject(key: String, accessibility: FxAKeychainItemAccessibility? = nil) -> Bool {
let keychainQueryDictionary: [String: Any] = getBaseKeychainQuery(key: key, accessibility: accessibility)
let status = SecItemDelete(keychainQueryDictionary as CFDictionary)
return status == errSecSuccess
}
func hasValue(key: String) -> Bool {
return getDataFromResult(queryKeychainForKey(key: key)) != nil
}
private func getDataFromResult(_ result: Result<String?, Error>) -> String? {
switch result {
case let .success(value):
return value
case .failure:
return nil
}
}
private func addOrUpdateKeychainKey(_ value: String,
key: String,
accessibility: FxAKeychainItemAccessibility? = nil) -> Bool
{
guard let data = value.data(using: .utf8) else { return false }
var addQueryDictionary = getBaseKeychainQuery(key: key)
let accessibilityValue = accessibility != nil ? accessibility : FxAKeychainItemAccessibility.afterFirstUnlock
addQueryDictionary[kSecValueData as String] = data
addQueryDictionary[kSecAttrAccessible as String] = accessibilityValue
let addStatus = SecItemAdd(addQueryDictionary as CFDictionary, nil)
if addStatus == errSecSuccess {
return true
} else if addStatus == errSecDuplicateItem {
let updateStatus = SecItemUpdate(getBaseKeychainQuery(key: key) as CFDictionary,
[kSecValueData: data] as CFDictionary)
return updateStatus == errSecSuccess
} else {
return false
}
}
private func getBaseKeychainQuery(key: String,
accessibility: FxAKeychainItemAccessibility? = nil) -> [String: Any]
{
let encodedIdentifier: Data? = key.data(using: String.Encoding.utf8)
var keychainQueryDictionary: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: serviceName,
kSecAttrSynchronizable as String: false]
keychainQueryDictionary[kSecAttrGeneric as String] = encodedIdentifier
keychainQueryDictionary[kSecAttrAccount as String] = encodedIdentifier
if let accessibility = accessibility {
keychainQueryDictionary[kSecAttrAccessible as String] = accessibility.secItemValue()
}
if let accessGroup = accessGroup {
keychainQueryDictionary[kSecAttrAccessGroup as String] = accessGroup
}
return keychainQueryDictionary
}
}