mirror of
https://github.com/ProtonMail/ios-mail.git
synced 2026-06-14 09:54:45 +00:00
414 lines
17 KiB
Swift
414 lines
17 KiB
Swift
//
|
|
// Keychain.swift
|
|
//
|
|
// This class has been imported from:
|
|
// https://gitlab.protontech.ch/apple/shared/protoncore/-/blob/cf8c57bfa2207923be6481560f77a94fdb4f6baf/libraries/Keymaker/Sources/Keychain.swift
|
|
//
|
|
|
|
import Foundation
|
|
import Security
|
|
|
|
open class Keychain {
|
|
internal enum Accessibility {
|
|
case afterFirstUnlockThisDeviceOnly
|
|
|
|
var cfString: CFString {
|
|
switch self {
|
|
case .afterFirstUnlockThisDeviceOnly: return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
}
|
|
}
|
|
}
|
|
internal enum AccessControl {
|
|
case none, userPresence
|
|
|
|
var flags: SecAccessControlCreateFlags? {
|
|
switch self {
|
|
case .userPresence: return [.userPresence]
|
|
case .none: return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum AccessError: LocalizedError {
|
|
case readFailed(key: String, error: OSStatus)
|
|
case writeFailed(key: String, error: OSStatus)
|
|
case updateFailed(key: String, error: OSStatus)
|
|
case deleteFailed(key: String, error: OSStatus)
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .readFailed(let key, let code):
|
|
return "Keychain.AccessError.readFailed(\(key), \(code))"
|
|
case .writeFailed(let key, let code):
|
|
return "Keychain.AccessError.writeFailed(\(key), \(code))"
|
|
case .updateFailed(let key, let code):
|
|
return "Keychain.AccessError.updateFailed(\(key), \(code))"
|
|
case .deleteFailed(let key, let code):
|
|
return "Keychain.AccessError.deleteFailed(\(key), \(code))"
|
|
}
|
|
}
|
|
}
|
|
|
|
internal var accessibility: Accessibility
|
|
internal var authenticationPolicy: AccessControl
|
|
internal let accessGroup: String
|
|
internal let service: String
|
|
internal let keychainQueue = DispatchQueue(label: "me.proton.account.keychain.queue", attributes: .concurrent)
|
|
|
|
private let secItemMethodsProvider: SecItemMethodsProvider
|
|
|
|
public init(service: String, accessGroup: String, secItemMethodsProvider: SecItemMethodsProvider? = nil) {
|
|
self.service = service
|
|
self.accessGroup = accessGroup
|
|
|
|
self.secItemMethodsProvider = secItemMethodsProvider ?? SystemSecurityFrameworkSecItemMethodsProvider.instance
|
|
|
|
self.accessibility = .afterFirstUnlockThisDeviceOnly
|
|
self.authenticationPolicy = .none
|
|
}
|
|
|
|
/// Adds or updates the value in the keychain.
|
|
///
|
|
/// This method returns regardless of whether:
|
|
/// * the value was successfully added or updated in the keychain
|
|
/// * keychain update failed because of the keychain access error
|
|
/// - Parameters:
|
|
/// - data: data to be added / updated in keychain
|
|
/// - key: key under which the data should be added / updated
|
|
@available(*, deprecated, message: "Please use the throwing alternative: setOrError(:forKey:) and handle the error")
|
|
public func set(_ data: Data, forKey key: String, attributes: [CFString: Any]? = nil) {
|
|
_ = self.add(data: data, forKey: key, attributes: attributes)
|
|
}
|
|
|
|
/// Adds or updates the value in the keychain.
|
|
///
|
|
/// This method returns regardless of whether:
|
|
/// * the value was successfully added or updated in the keychain
|
|
/// * keychain update failed because of the keychain access error
|
|
/// - Parameters:
|
|
/// - string: string to be added / updated in keychain
|
|
/// - key: key under which the string should be added / updated
|
|
@available(*, deprecated, message: "Please use the throwing alternative: setOrError(:forKey:) and handle the error")
|
|
public func set(_ string: String, forKey key: String, attributes: [CFString: Any]? = nil) {
|
|
_ = self.add(data: string.data(using: .utf8)!, forKey: key, attributes: attributes)
|
|
}
|
|
|
|
/// Adds or updates the value in the keychain.
|
|
///
|
|
/// This method:
|
|
/// * returns if the value was successfully added or updated in the keychain
|
|
/// * throws the error if keychain update failed because of the keychain access error
|
|
/// - Parameters:
|
|
/// - data: data to be added / updated in keychain
|
|
/// - key: key under which the data should be added / updated
|
|
public func setOrError(_ data: Data, forKey key: String, attributes: [CFString: Any]? = nil) throws {
|
|
try self.addOrError(data: data, forKey: key, attributes: attributes)
|
|
}
|
|
|
|
/// Adds or updates the value in the keychain.
|
|
///
|
|
/// This method:
|
|
/// * returns if the value was successfully added or updated in the keychain
|
|
/// * throws the error if keychain update failed because of the keychain access error
|
|
/// - Parameters:
|
|
/// - string: string to be added / updated in keychain
|
|
/// - key: key under which the string should be added / updated
|
|
public func setOrError(_ string: String, forKey key: String, attributes: [CFString: Any]? = nil) throws {
|
|
try self.addOrError(data: string.data(using: .utf8)!, forKey: key, attributes: attributes)
|
|
}
|
|
|
|
/// Fetches the value from the keychain.
|
|
///
|
|
/// This method:
|
|
/// * returns the value if it was found in the keychain,
|
|
/// * returns nil if there was no value in the keychain OR the keychain read failed because of the keychain access
|
|
/// - Parameters:
|
|
/// - forKey: key under which the value is stored in keychain
|
|
@available(*, deprecated, message: "Please use the throwing alternative: dataOrError(forKey:) and handle the error")
|
|
public func data(forKey key: String, attributes: [CFString: Any]? = nil) -> Data? {
|
|
self.getData(forKey: key, attributes: attributes)
|
|
}
|
|
|
|
/// Fetches the value from the keychain.
|
|
///
|
|
/// This method:
|
|
/// * returns the value if it was found in the keychain,
|
|
/// * returns nil if there was no value in the keychain OR the keychain read failed because of the keychain access
|
|
/// - Parameters:
|
|
/// - forKey: key under which the value is stored in keychain
|
|
@available(*, deprecated, message: "Please use the throwing alternative: stringOrError(forKey:) and handle the error")
|
|
public func string(forKey key: String, attributes: [CFString: Any]? = nil) -> String? {
|
|
guard let data = self.getData(forKey: key, attributes: attributes) else {
|
|
return nil
|
|
}
|
|
return String(data: data, encoding: .utf8)
|
|
}
|
|
|
|
/// Fetches the value from the keychain.
|
|
///
|
|
/// This method:
|
|
/// * returns the value if it was found in the keychain,
|
|
/// * returns nil if there was no value in the keychain,
|
|
/// * throws the error if keychain read failed because of the keychain access error
|
|
/// - Parameters:
|
|
/// - forKey: key under which the value is stored in keychain
|
|
public func dataOrError(forKey key: String, attributes: [CFString: Any]? = nil) throws -> Data? {
|
|
try self.getDataOrError(forKey: key, attributes: attributes)
|
|
}
|
|
|
|
/// Fetches the value from the keychain.
|
|
///
|
|
/// This method:
|
|
/// * returns the value if it was found in the keychain,
|
|
/// * returns nil if there was no value in the keychain,
|
|
/// * throws the error if keychain read failed because of the keychain access error
|
|
/// - Parameters:
|
|
/// - forKey: key under which the value is stored in keychain
|
|
public func stringOrError(forKey key: String, attributes: [CFString: Any]? = nil) throws -> String? {
|
|
guard let data = try self.getDataOrError(forKey: key, attributes: attributes) else { return nil }
|
|
return String(data: data, encoding: .utf8)
|
|
}
|
|
|
|
/// Removes the value from the keychain.
|
|
///
|
|
/// This method returns regardless of whether:
|
|
/// * keychain delete succeeded
|
|
/// * there was nothing to remove
|
|
/// * keychain delete failed because of the keychain access error
|
|
/// - Parameters:
|
|
/// - forKey: key under which the value should be removed from the keychain
|
|
@available(*, deprecated, message: "Please use the throwing alternative: removeOrError(forKey:) and handle the error")
|
|
public func remove(forKey key: String) {
|
|
_ = self.remove(key)
|
|
}
|
|
|
|
/// Removes the value from the keychain.
|
|
///
|
|
/// This method:
|
|
/// * returns if keychain delete succeeds or if there was nothing to remove,
|
|
/// * throws if keychain delete failed because of the keychain access error
|
|
/// - Parameters:
|
|
/// - forKey: key under which the value should be removed from the keychain
|
|
public func removeOrError(forKey key: String) throws {
|
|
try self.removeOrError(key)
|
|
}
|
|
|
|
// Private - internal for unit tests
|
|
@available(*, deprecated, message: "Please use the throwing alternative: getDataOrError(forKey:) and handle the error")
|
|
internal func getData(forKey key: String, attributes: [CFString: Any]? = nil) -> Data? {
|
|
try? getDataOrError(forKey: key, attributes: attributes)
|
|
}
|
|
|
|
internal func getDataOrError(forKey key: String, attributes: [CFString: Any]? = nil) throws -> Data? {
|
|
var query: [String: AnyObject] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: self.service as AnyObject,
|
|
kSecAttrAccount as String: key as AnyObject,
|
|
kSecReturnData as String: kCFBooleanTrue,
|
|
kSecMatchLimit as String: kSecMatchLimitOne,
|
|
kSecAttrAccessGroup as String: self.accessGroup as AnyObject,
|
|
kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
|
|
]
|
|
if #available(macOS 10.15, tvOS 13.0, watchOS 6.0, macCatalyst 13.0, *) {
|
|
query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue
|
|
}
|
|
|
|
if let auth = self.authenticationPolicy.flags,
|
|
let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, self.accessibility.cfString, auth, nil)
|
|
{
|
|
query[kSecAttrAccessControl as String] = accessControl
|
|
}
|
|
|
|
if let attributes {
|
|
for (key, value) in attributes {
|
|
query[key as String] = value as AnyObject
|
|
}
|
|
}
|
|
|
|
let secItem = secItemMethodsProvider
|
|
|
|
return try keychainQueue.sync {
|
|
var result: AnyObject?
|
|
let code = withUnsafeMutablePointer(to: &result) {
|
|
secItem.SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
|
|
}
|
|
|
|
guard code == noErr, let data = result as? Data else {
|
|
if code == errSecItemNotFound {
|
|
// data not found in the keychain, return nil
|
|
return nil
|
|
} else {
|
|
// reading from keychain errored out, return the error
|
|
throw Keychain.AccessError.readFailed(key: key, error: code)
|
|
}
|
|
}
|
|
|
|
return data
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
@available(*, deprecated, message: "Please use the throwing alternative: removeOrError(:) and handle the error")
|
|
internal func remove(_ key: String) -> Bool {
|
|
do {
|
|
try removeOrError(key)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
internal func removeOrError(_ key: String) throws {
|
|
var query: [String: AnyObject] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: self.service as AnyObject,
|
|
kSecAttrAccount as String: key as AnyObject,
|
|
kSecAttrAccessGroup as String: self.accessGroup as AnyObject,
|
|
kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
|
|
]
|
|
if #available(macOS 10.15, tvOS 13.0, watchOS 6.0, macCatalyst 13.0, *) {
|
|
query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue
|
|
}
|
|
|
|
let secItem = secItemMethodsProvider
|
|
|
|
return try keychainQueue.sync(flags: .barrier) {
|
|
let code = secItem.SecItemDelete(query as CFDictionary)
|
|
|
|
guard code == noErr || code == errSecItemNotFound else {
|
|
throw Keychain.AccessError.deleteFailed(key: key, error: code)
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(*, deprecated, message: "Please use the throwing alternative: addOrError(data:forKey:) and handle the error")
|
|
internal func add(data value: Data, forKey key: String, attributes: [CFString: Any]? = nil) -> Bool {
|
|
do {
|
|
try addOrError(data: value, forKey: key, attributes: attributes)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
internal func addOrError(data value: Data, forKey key: String, attributes: [CFString: Any]? = nil) throws {
|
|
// search for existing
|
|
var query: [String: AnyObject] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: self.service as AnyObject,
|
|
kSecAttrAccount as String: key as AnyObject,
|
|
kSecAttrAccessGroup as String: self.accessGroup as AnyObject,
|
|
kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
|
|
]
|
|
if #available(macOS 10.15, tvOS 13.0, watchOS 6.0, macCatalyst 13.0, *) {
|
|
query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue
|
|
}
|
|
|
|
if let attributes {
|
|
for (key, value) in attributes {
|
|
query[key as String] = value as AnyObject
|
|
}
|
|
}
|
|
|
|
var queryForSearch = query
|
|
if #unavailable(macOS 11.0, iOS 15.0, macCatalyst 15.0) {
|
|
queryForSearch[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
|
|
}
|
|
|
|
let secItem = secItemMethodsProvider
|
|
|
|
return try keychainQueue.sync(flags: .barrier) {
|
|
let codeExisting = secItem.SecItemCopyMatching(queryForSearch as CFDictionary, nil)
|
|
|
|
// update
|
|
guard codeExisting == errSecItemNotFound else {
|
|
var updateAttributes: [String: AnyObject] = [
|
|
kSecAttrSynchronizable as String: NSNumber(value: false),
|
|
kSecValueData as String: value as AnyObject,
|
|
]
|
|
self.injectAccessControlAttributes(into: &updateAttributes)
|
|
|
|
let codeUpdate = secItem.SecItemUpdate(query as CFDictionary, updateAttributes as CFDictionary)
|
|
guard codeUpdate == noErr else {
|
|
throw Keychain.AccessError.updateFailed(key: key, error: codeUpdate)
|
|
}
|
|
return
|
|
}
|
|
|
|
// add new
|
|
var newAttributes = query
|
|
newAttributes[kSecAttrSynchronizable as String] = NSNumber(value: false)
|
|
newAttributes[kSecValueData as String] = value as AnyObject
|
|
self.injectAccessControlAttributes(into: &newAttributes)
|
|
|
|
let code = secItem.SecItemAdd(newAttributes as CFDictionary, nil)
|
|
guard code == noErr else {
|
|
throw Keychain.AccessError.writeFailed(key: key, error: code)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
private func injectAccessControlAttributes(into attributes: inout [String: AnyObject]) {
|
|
if let auth = self.authenticationPolicy.flags,
|
|
let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, self.accessibility.cfString, auth, nil)
|
|
{
|
|
attributes[kSecAttrAccessControl as String] = accessControl
|
|
} else {
|
|
attributes[kSecAttrAccessible as String] = self.accessibility.cfString
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
public func removeEverything() -> Bool {
|
|
var query: [String: AnyObject] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: self.service as AnyObject,
|
|
kSecAttrAccessGroup as String: self.accessGroup as AnyObject,
|
|
kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
|
|
]
|
|
if #available(macOS 10.15, tvOS 13.0, watchOS 6.0, macCatalyst 13.0, *) {
|
|
query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue
|
|
}
|
|
|
|
let secItem = secItemMethodsProvider
|
|
|
|
return keychainQueue.sync(flags: .barrier) {
|
|
let code = secItem.SecItemDelete(query as CFDictionary)
|
|
|
|
guard code == noErr || code == errSecItemNotFound else {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
public protocol SecItemMethodsProvider {
|
|
func SecItemCopyMatching(_ query: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
|
|
func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
|
|
func SecItemUpdate(_ query: CFDictionary, _ attributesToUpdate: CFDictionary) -> OSStatus
|
|
func SecItemDelete(_ query: CFDictionary) -> OSStatus
|
|
}
|
|
|
|
private enum SystemSecurityFrameworkSecItemMethodsProvider: SecItemMethodsProvider {
|
|
case instance
|
|
|
|
func SecItemCopyMatching(_ query: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus {
|
|
Security.SecItemCopyMatching(query, result)
|
|
}
|
|
|
|
func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus {
|
|
Security.SecItemAdd(attributes, result)
|
|
}
|
|
|
|
func SecItemUpdate(_ query: CFDictionary, _ attributesToUpdate: CFDictionary) -> OSStatus {
|
|
Security.SecItemUpdate(query, attributesToUpdate)
|
|
}
|
|
|
|
func SecItemDelete(_ query: CFDictionary) -> OSStatus {
|
|
Security.SecItemDelete(query)
|
|
}
|
|
}
|