190 lines
6.9 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|