Files
voice-cat/clients/apple/iOS/VoiceCatiOS/ServerListStore.swift

93 lines
3.1 KiB
Swift
Raw Normal View History

import Foundation
import Security
final class ServerListStore {
static let shared = ServerListStore()
private let groupId = "group.cat.voice.VoiceCat"
private let keychainService = "cat.voice.VoiceCatiOS"
// MARK: - App Group Container
var appGroupContainer: URL {
guard let url = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: groupId)
else {
// Fall back to app-only support dir if App Groups are unavailable (e.g. simulator
// without entitlements). TOFU pins won't be shared with the broadcast extension,
// but connect + auth still work.
return FileManager.default.urls(for: .applicationSupportDirectory,
in: .userDomainMask).first!
}
return url
}
private var voicecatDir: URL {
let dir = appGroupContainer.appendingPathComponent("voicecat", isDirectory: true)
try? FileManager.default.createDirectory(at: dir,
withIntermediateDirectories: true)
return dir
}
var tofuStorePath: String { voicecatDir.appendingPathComponent("tofu_pins.txt").path }
private var serversURL: URL { voicecatDir.appendingPathComponent("servers.json") }
// MARK: - Server list persistence
func load() -> [SavedServer] {
guard let data = try? Data(contentsOf: serversURL),
let list = try? JSONDecoder().decode([SavedServer].self, from: data)
else { return [] }
return list
}
func save(_ servers: [SavedServer]) {
guard let data = try? JSONEncoder().encode(servers) else { return }
try? data.write(to: serversURL, options: .atomic)
}
// MARK: - Keychain
func savePassword(_ password: String, tag: String) {
let data = Data(password.utf8)
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: keychainService,
kSecAttrAccount: tag,
kSecAttrAccessGroup: groupId,
]
SecItemDelete(query as CFDictionary)
var add = query
add[kSecValueData] = data
SecItemAdd(add as CFDictionary, nil)
}
func loadPassword(tag: String) -> String? {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: keychainService,
kSecAttrAccount: tag,
kSecAttrAccessGroup: groupId,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne,
]
var result: AnyObject?
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
let data = result as? Data
else { return nil }
return String(data: data, encoding: .utf8)
}
func deletePassword(tag: String) {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: keychainService,
kSecAttrAccount: tag,
kSecAttrAccessGroup: groupId,
]
SecItemDelete(query as CFDictionary)
}
}