Files

190 lines
6.9 KiB
Swift

//
// WalletKeychain.swift
//
//
// Created by Juraldinio on 11/27/22.
//
import LocalAuthentication
final public class WalletKeychain: KeychainProtocol {
private enum KeychainLocals {
public static let password = "PWD"
public static let biometric = "BIO"
}
public static let instance = WalletKeychain()
// MARK: - Init
private init() { }
// MARK: - Interface
public func exist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(KeychainLocals.password)) }
public func bioExist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(KeychainLocals.biometric) ) }
public subscript(biometric key: Key) -> String? {
self.loadBiometricProtected(key: key.with(KeychainLocals.biometric))
.map({ String(data: $0, encoding: .utf8) }) ?? nil
}
public subscript(_ key: Key, password password: String) -> String? {
get { loadPassProtected(key: key.with(KeychainLocals.password), password: password).map { String(data: $0, encoding: .utf8) } ?? nil }
set { update(key, password: password, newValue: newValue) }
}
// MARK: - Private
private func getPwdSecAccessControl() -> SecAccessControl {
var access: SecAccessControl?
var error: Unmanaged<CFError>?
access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .applicationPassword, &error)
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
return access! // swiftlint:disable:this force_unwrapping
}
@discardableResult
private func setPassProtected(key: Key, data: String, password: String) -> Bool {
let context = LAContext()
context.setCredential(password.data(using: .utf8), type: .applicationPassword)
let query = [
kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: key.rawValue,
kSecAttrAccessControl as String: getPwdSecAccessControl(),
kSecValueData as String: (data.data(using: .utf8) ?? Data()) as NSData,
kSecUseAuthenticationContext: context
] as CFDictionary
let status: OSStatus = SecItemAdd(query, nil)
if status == errSecSuccess {
return true
} else if status == errSecDuplicateItem {
if removeProtected(key: key) {
return setPassProtected(key: key, data: data, password: password)
} else {
return false
}
} else {
return false
}
}
private func loadPassProtected(key: Key, password: String) -> Data? {
let context = LAContext()
context.setCredential(password.data(using: .utf8), type: .applicationPassword)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue,
kSecReturnData as String: kCFBooleanTrue!,
kSecAttrAccessControl as String: getPwdSecAccessControl(),
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationContext as String: context,
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
]
var dataTypeRef: AnyObject?
let result = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if result == noErr,
let value = dataTypeRef as? Data {
return value
}
return nil
}
// MARK: - Biometric entries
private func getBiometricSecAccessControl() -> SecAccessControl {
var access: SecAccessControl?
var error: Unmanaged<CFError>?
access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .biometryCurrentSet, &error)
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
return access! // swiftlint:disable:this force_unwrapping
}
@discardableResult
private func setBiometricEntry(key: Key, data: String) -> Bool {
let query = [
kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: key.rawValue,
kSecAttrAccessControl as String: getBiometricSecAccessControl(),
kSecValueData as String: (data.data(using: .utf8) ?? Data()) as NSData,
] as CFDictionary
let status: OSStatus = SecItemAdd(query, nil)
if status == errSecSuccess {
return true
} else if status == errSecDuplicateItem {
if removeProtected(key: key) {
return setBiometricEntry(key: key, data: data)
} else {
return false
}
} else {
return false
}
}
@discardableResult
private func loadBiometricProtected(key: Key) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue,
kSecReturnData as String: kCFBooleanTrue as Any,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseOperationPrompt as String: "Access your data"
]
var dataTypeRef: AnyObject?
return SecItemCopyMatching(query as CFDictionary, &dataTypeRef) == noErr ? dataTypeRef as? Data : nil
}
// MARK: -
private func update(_ key: Key, password: String, newValue: String?) {
//TODO: Refactor later - updation of biometric entry only when it is available and needed
if let value = newValue {
setPassProtected(key: key.with(KeychainLocals.password), data: value, password: password)
setBiometricEntry(key: key.with(KeychainLocals.biometric), data: value)
} else {
removeProtected(key: key.with(KeychainLocals.password))
removeProtected(key: key.with(KeychainLocals.biometric))
}
}
@discardableResult
private func removeProtected(key: Key) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue
]
return SecItemDelete(query as CFDictionary) == noErr
}
private func checkProtectedExist(key: Key) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
]
let status = SecItemCopyMatching(query as CFDictionary, nil)
switch status {
case errSecSuccess, errSecInteractionNotAllowed: return true
case errSecItemNotFound: return false
default: return false
}
}
}