2026-09-14 00:17:07 +03:00
|
|
|
import Foundation
|
|
|
|
|
import Security
|
|
|
|
|
|
|
|
|
|
enum SystemPlayerCredentials {
|
2026-09-14 00:48:25 +03:00
|
|
|
// Derive the signing prefix from our own default Keychain group. Info.plist
|
|
|
|
|
// build substitutions cannot know the team that later re-signs an IPA.
|
|
|
|
|
private static func signingPrefix() throws -> String {
|
|
|
|
|
let marker: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
|
|
|
|
|
kSecAttrService as String: "MusicBridge.signing-prefix.v1", kSecAttrAccount as String: "prefix"]
|
|
|
|
|
let status = SecItemAdd(marker.merging([kSecValueData as String: Data(),
|
|
|
|
|
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]) { _, v in v } as CFDictionary, nil)
|
|
|
|
|
guard status == errSecSuccess || status == errSecDuplicateItem else { throw failure(status) }
|
|
|
|
|
var result: CFTypeRef?
|
|
|
|
|
let read = SecItemCopyMatching(marker.merging([kSecReturnAttributes as String: true,
|
|
|
|
|
kSecMatchLimit as String: kSecMatchLimitOne]) { _, v in v } as CFDictionary, &result)
|
|
|
|
|
guard read == errSecSuccess else { throw failure(read) }
|
|
|
|
|
guard let attributes = result as? [String: Any],
|
|
|
|
|
let group = attributes[kSecAttrAccessGroup as String] as? String,
|
|
|
|
|
let separator = group.firstIndex(of: "."), separator != group.startIndex else {
|
|
|
|
|
throw failure(errSecMissingEntitlement)
|
|
|
|
|
}
|
|
|
|
|
return String(group[...separator])
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 00:17:07 +03:00
|
|
|
private static func query(_ id: String) throws -> [String: Any] {
|
2026-09-14 00:48:25 +03:00
|
|
|
guard let suffix = Bundle.main.object(forInfoDictionaryKey: "MusicBridgeSharedKeychainGroup") as? String,
|
|
|
|
|
suffix == "ru.yukinoki.musicbridge.remote-control" else {
|
2026-09-14 00:17:07 +03:00
|
|
|
throw BridgeError.message("Подпись приложения не настроила общий Keychain для системного пульта.")
|
|
|
|
|
}
|
|
|
|
|
return [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "MusicBridge.system-player.v1",
|
2026-09-14 00:48:25 +03:00
|
|
|
kSecAttrAccount as String: id, kSecAttrAccessGroup as String: try signingPrefix() + suffix]
|
2026-09-14 00:17:07 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func save(_ connection: Connection, id: String) throws {
|
|
|
|
|
let query = try query(id)
|
|
|
|
|
let values: [String: Any] = [kSecValueData as String: try JSONEncoder().encode(connection),
|
|
|
|
|
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]
|
|
|
|
|
var status = SecItemUpdate(query as CFDictionary, values as CFDictionary)
|
|
|
|
|
if status == errSecItemNotFound { status = SecItemAdd(query.merging(values) { _, v in v } as CFDictionary, nil) }
|
|
|
|
|
guard status == errSecSuccess else { throw failure(status) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func load(_ id: String) throws -> Connection {
|
|
|
|
|
var query = try query(id)
|
|
|
|
|
query[kSecReturnData as String] = true; query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
|
|
|
var item: CFTypeRef?
|
|
|
|
|
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
|
|
|
|
guard status == errSecSuccess, let data = item as? Data else { throw failure(status) }
|
|
|
|
|
let saved = try JSONDecoder().decode(Connection.self, from: data)
|
|
|
|
|
guard let token = saved.token, Connection.isFingerprint(token) else { throw BridgeError.unauthorized }
|
|
|
|
|
return try Connection(endpoint: saved.endpoint.absoluteString, fingerprint: saved.fingerprint, token: token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func delete(_ id: String) throws {
|
|
|
|
|
let status = SecItemDelete(try query(id) as CFDictionary)
|
|
|
|
|
guard status == errSecSuccess || status == errSecItemNotFound else { throw failure(status) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func failure(_ status: OSStatus) -> BridgeError {
|
|
|
|
|
.message("Системный пульт не получил доступ к Keychain (\(status)). Проверьте подпись приложения и расширения; обычный плеер остаётся доступен.")
|
|
|
|
|
}
|
|
|
|
|
}
|