83 lines
2.8 KiB
Swift
83 lines
2.8 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
/// Simple Keychain wrapper for storing server passwords securely.
|
|
/// Passwords are stored per server UUID so each server has its own entry.
|
|
/// Falls back gracefully — if Keychain is unavailable (e.g. simulator sandbox
|
|
/// edge cases), the caller handles the nil and falls back to UserDefaults.
|
|
enum KeychainHelper {
|
|
|
|
private static let service = "ca.dallasgroot.navidromeplayer"
|
|
|
|
// MARK: - Save
|
|
|
|
/// Store a password for the given server ID. Overwrites any existing entry.
|
|
@discardableResult
|
|
static func savePassword(_ password: String, for serverId: UUID) -> Bool {
|
|
let key = keychainKey(for: serverId)
|
|
guard let data = password.data(using: .utf8) else { return false }
|
|
|
|
// Delete existing first — SecItemAdd fails if the key already exists
|
|
deletePassword(for: serverId)
|
|
|
|
let query: [CFString: Any] = [
|
|
kSecClass: kSecClassGenericPassword,
|
|
kSecAttrService: service,
|
|
kSecAttrAccount: key,
|
|
kSecValueData: data,
|
|
// Accessible after first unlock — allows background audio session
|
|
// to access credentials without requiring the device to be unlocked.
|
|
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
]
|
|
|
|
return SecItemAdd(query as CFDictionary, nil) == errSecSuccess
|
|
}
|
|
|
|
// MARK: - Load
|
|
|
|
/// Retrieve the password for the given server ID. Returns nil if not found.
|
|
static func loadPassword(for serverId: UUID) -> String? {
|
|
let key = keychainKey(for: serverId)
|
|
|
|
let query: [CFString: Any] = [
|
|
kSecClass: kSecClassGenericPassword,
|
|
kSecAttrService: service,
|
|
kSecAttrAccount: key,
|
|
kSecReturnData: true,
|
|
kSecMatchLimit: kSecMatchLimitOne
|
|
]
|
|
|
|
var result: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
|
|
guard status == errSecSuccess,
|
|
let data = result as? Data,
|
|
let password = String(data: data, encoding: .utf8) else {
|
|
return nil
|
|
}
|
|
return password
|
|
}
|
|
|
|
// MARK: - Delete
|
|
|
|
/// Remove the stored password for the given server ID.
|
|
@discardableResult
|
|
static func deletePassword(for serverId: UUID) -> Bool {
|
|
let key = keychainKey(for: serverId)
|
|
|
|
let query: [CFString: Any] = [
|
|
kSecClass: kSecClassGenericPassword,
|
|
kSecAttrService: service,
|
|
kSecAttrAccount: key
|
|
]
|
|
|
|
let status = SecItemDelete(query as CFDictionary)
|
|
return status == errSecSuccess || status == errSecItemNotFound
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private static func keychainKey(for serverId: UUID) -> String {
|
|
"server.\(serverId.uuidString).password"
|
|
}
|
|
}
|