Files
proxy-studio/proxy studio/CredentialStore.swift
T
2026-09-27 15:42:22 +02:00

76 lines
2.7 KiB
Swift

import Foundation
import Security
struct ProxyCredentials: Codable {
let url: String
let email: String
let password: String
}
struct UniFiCredentials: Codable {
let url: String
let apiKey: String
let allowUntrusted: Bool
}
enum CredentialStore {
static let proxyAccount = "nginx-proxy-manager"
static let unifiAccount = "unifi-network"
// Use the macOS login keychain so local, ad hoc signed builds also work
// without a development team's application-identifier entitlement.
private static func query(account: String) -> [String: Any] {
[
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "io.github.nihaiden.ProxyStudio.credentials",
kSecAttrAccount as String: account,
kSecAttrSynchronizable as String: false
]
}
static func save<T: Encodable>(_ credentials: T, account: String) throws {
let data = try JSONEncoder().encode(credentials)
let query = query(account: account)
let attributes = [kSecValueData as String: data]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
if status == errSecItemNotFound {
var item = query
item[kSecValueData as String] = data
item[kSecAttrLabel as String] = "Proxy Studio — " + account
try check(SecItemAdd(item as CFDictionary, nil))
} else {
try check(status)
}
}
static func load<T: Decodable>(_ type: T.Type, account: String) throws -> T? {
var query = query(account: account)
query[kSecReturnData as String] = true
query[kSecMatchLimit as String] = kSecMatchLimitOne
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound { return nil }
try check(status)
guard let data = result as? Data else {
throw ServiceError.message("The saved credentials could not be read. Please connect again.")
}
do {
return try JSONDecoder().decode(type, from: data)
} catch {
throw ServiceError.message("The saved credentials could not be decoded. Please connect again.")
}
}
static func delete(account: String) throws {
let status = SecItemDelete(query(account: account) as CFDictionary)
if status != errSecItemNotFound { try check(status) }
}
private static func check(_ status: OSStatus) throws {
guard status == errSecSuccess else {
let message = SecCopyErrorMessageString(status, nil) as String? ?? "Error \(status)"
throw ServiceError.message("Keychain: \(message)")
}
}
}