ios_client
This commit is contained in:
132
ios_client 0.8.5/Kecalek/Core/KeychainService.swift
Normal file
132
ios_client 0.8.5/Kecalek/Core/KeychainService.swift
Normal file
@@ -0,0 +1,132 @@
|
||||
import Foundation
|
||||
import Security
|
||||
import LocalAuthentication
|
||||
|
||||
enum KeychainService {
|
||||
private static let service = "com.encryptedchat.credentials"
|
||||
private static let account = "userCredentials"
|
||||
|
||||
struct Credentials: Codable {
|
||||
let email: String
|
||||
let password: String
|
||||
let host: String
|
||||
let port: UInt16
|
||||
}
|
||||
|
||||
/// Check if saved credentials exist without triggering biometric prompt.
|
||||
static func hasSavedCredentials() -> Bool {
|
||||
let context = LAContext()
|
||||
context.interactionNotAllowed = true
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecUseAuthenticationContext as String: context
|
||||
]
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
// errSecInteractionNotAllowed means item exists but needs biometric
|
||||
return status == errSecSuccess || status == errSecInteractionNotAllowed
|
||||
}
|
||||
|
||||
/// Save credentials to Keychain with biometric protection.
|
||||
static func saveCredentials(email: String, password: String, host: String, port: UInt16) throws {
|
||||
// Delete any existing entry first
|
||||
deleteCredentials()
|
||||
|
||||
let credentials = Credentials(email: email, password: password, host: host, port: port)
|
||||
let data = try JSONEncoder().encode(credentials)
|
||||
|
||||
var accessError: Unmanaged<CFError>?
|
||||
guard let accessControl = SecAccessControlCreateWithFlags(
|
||||
kCFAllocatorDefault,
|
||||
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
|
||||
.biometryAny,
|
||||
&accessError
|
||||
) else {
|
||||
throw KeychainError.accessControlCreationFailed
|
||||
}
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account,
|
||||
kSecValueData as String: data,
|
||||
kSecAttrAccessControl as String: accessControl
|
||||
]
|
||||
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.saveFailed(status)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load credentials from Keychain. Triggers biometric prompt.
|
||||
static func loadCredentials() throws -> Credentials {
|
||||
let context = LAContext()
|
||||
context.localizedReason = "Unlock to log in"
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecUseAuthenticationContext as String: context
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
guard status == errSecSuccess, let data = result as? Data else {
|
||||
if status == errSecUserCanceled || status == errSecAuthFailed {
|
||||
throw KeychainError.biometricFailed
|
||||
}
|
||||
throw KeychainError.loadFailed(status)
|
||||
}
|
||||
|
||||
return try JSONDecoder().decode(Credentials.self, from: data)
|
||||
}
|
||||
|
||||
/// Delete stored credentials from Keychain.
|
||||
@discardableResult
|
||||
static func deleteCredentials() -> Bool {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account
|
||||
]
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
return status == errSecSuccess || status == errSecItemNotFound
|
||||
}
|
||||
|
||||
/// Check if biometric authentication is available on this device.
|
||||
static func isBiometricAvailable() -> Bool {
|
||||
let context = LAContext()
|
||||
var error: NSError?
|
||||
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
|
||||
}
|
||||
|
||||
enum KeychainError: LocalizedError {
|
||||
case accessControlCreationFailed
|
||||
case saveFailed(OSStatus)
|
||||
case loadFailed(OSStatus)
|
||||
case biometricFailed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .accessControlCreationFailed:
|
||||
return "Failed to create biometric access control"
|
||||
case .saveFailed(let status):
|
||||
return "Failed to save credentials (error \(status))"
|
||||
case .loadFailed(let status):
|
||||
return "Failed to load credentials (error \(status))"
|
||||
case .biometricFailed:
|
||||
return "Biometric authentication failed or was cancelled"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user