mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
109 lines
3.6 KiB
Swift
109 lines
3.6 KiB
Swift
#if BUZZ_PUSH_ENABLED
|
|
import BuzzPushKit
|
|
import Foundation
|
|
import Security
|
|
|
|
/// Keychain-backed endpoint grant storage. The opaque grant is never written to
|
|
/// UserDefaults or logs. Dart can read the closed record through the push bridge.
|
|
final class BuzzPushEndpointGrantKeychainStore: BuzzPushEndpointGrantStore {
|
|
private static let service = "buzz.push.endpoint-grants"
|
|
private static let account = "v1"
|
|
|
|
private let accessGroup: String?
|
|
|
|
init(accessGroup: String?) {
|
|
self.accessGroup = accessGroup
|
|
}
|
|
|
|
func records() throws -> [BuzzPushEndpointGrantRecord] {
|
|
var query = baseQuery()
|
|
query[kSecReturnData as String] = true
|
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
var result: CFTypeRef?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
if status == errSecItemNotFound { return [] }
|
|
guard status == errSecSuccess, let data = result as? Data else {
|
|
throw keychainError(status, operation: "read")
|
|
}
|
|
do {
|
|
return try JSONDecoder().decode([BuzzPushEndpointGrantRecord].self, from: data)
|
|
} catch {
|
|
throw NSError(
|
|
domain: "BuzzPushEndpointGrantStore",
|
|
code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Stored endpoint grants are invalid: \(error)"]
|
|
)
|
|
}
|
|
}
|
|
|
|
func save(_ record: BuzzPushEndpointGrantRecord) throws {
|
|
var all = try records()
|
|
all.removeAll {
|
|
$0.relayOrigin == record.relayOrigin && $0.appProfile == record.appProfile
|
|
}
|
|
all.append(record)
|
|
try replace(all)
|
|
}
|
|
|
|
private func replace(_ records: [BuzzPushEndpointGrantRecord]) throws {
|
|
let data = try JSONEncoder().encode(records)
|
|
let updateStatus = SecItemUpdate(
|
|
baseQuery() as CFDictionary,
|
|
[kSecValueData as String: data] as CFDictionary
|
|
)
|
|
if updateStatus == errSecSuccess { return }
|
|
guard updateStatus == errSecItemNotFound else {
|
|
throw keychainError(updateStatus, operation: "update")
|
|
}
|
|
|
|
var add = baseQuery()
|
|
add[kSecValueData as String] = data
|
|
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
let addStatus = SecItemAdd(add as CFDictionary, nil)
|
|
guard addStatus == errSecSuccess else {
|
|
throw keychainError(addStatus, operation: "add")
|
|
}
|
|
}
|
|
|
|
private func baseQuery() -> [String: Any] {
|
|
var query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: Self.service,
|
|
kSecAttrAccount as String: Self.account,
|
|
]
|
|
if let accessGroup, !accessGroup.isEmpty {
|
|
query[kSecAttrAccessGroup as String] = accessGroup
|
|
}
|
|
return query
|
|
}
|
|
|
|
private func keychainError(_ status: OSStatus, operation: String) -> Error {
|
|
NSError(
|
|
domain: NSOSStatusErrorDomain,
|
|
code: Int(status),
|
|
userInfo: [
|
|
NSLocalizedDescriptionKey:
|
|
"Endpoint grant Keychain \(operation) failed: \(SecCopyErrorMessageString(status, nil) ?? "unknown" as CFString)"
|
|
]
|
|
)
|
|
}
|
|
}
|
|
|
|
extension BuzzPushEndpointGrantRecord {
|
|
var flutterArguments: [String: Any] {
|
|
let arguments: [String: Any] = [
|
|
"relayOrigin": relayOrigin,
|
|
"relayPubkey": relayPubkey,
|
|
"installationId": installationId,
|
|
"endpointGrant": endpointGrant,
|
|
"endpointHash": endpointHash,
|
|
"appProfile": appProfile,
|
|
"endpointEpoch": endpointEpoch,
|
|
"generation": generation,
|
|
"expiresAt": expiresAt,
|
|
]
|
|
return arguments
|
|
}
|
|
}
|
|
#endif
|