Files
hapi/ios/Packages/HapiKit/Sources/HapiClient/Push/PushDeviceKey.swift
T

51 lines
1.9 KiB
Swift

import Foundation
/// The per-install end-to-end push key: 32 random bytes, generated **once**
/// per install, registered with every paired hub (`pushKey` on
/// `POST /api/devices/register`, base64) and used by the hub to AES-256-GCM
/// encrypt every push envelope (``PushEnvelope``).
///
/// The app stores it in the Keychain (shared access group, so the
/// Notification Service Extension can decrypt); it is regenerated only when
/// lost — a new key simply re-registers on the next registration pass.
public struct PushDeviceKey: Equatable, Sendable {
/// AES-256 key size in bytes (= ``PushEnvelope/keyLength``).
public static let keyLength = 32
/// The raw 32-byte key.
public let data: Data
/// Wraps existing key material; nil unless exactly 32 bytes.
public init?(data: Data) {
guard data.count == Self.keyLength else { return nil }
self.data = data
}
/// Decodes a stored/base64 key; nil when not base64 or not 32 bytes.
public init?(base64: String) {
guard let decoded = Data(base64Encoded: base64) else { return nil }
self.init(data: decoded)
}
/// The registration wire form (`pushKey` field).
public var base64: String {
data.base64EncodedString()
}
/// Draws a fresh key from the system CSPRNG
/// (`SystemRandomNumberGenerator` is cryptographically secure on every
/// supported platform per its documentation).
public static func generate() -> PushDeviceKey {
var generator = SystemRandomNumberGenerator()
var bytes = [UInt8]()
bytes.reserveCapacity(keyLength)
for _ in 0..<(keyLength / 8) {
withUnsafeBytes(of: generator.next().littleEndian) { bytes.append(contentsOf: $0) }
}
guard let key = PushDeviceKey(data: Data(bytes)) else {
preconditionFailure("generated key has fixed length \(keyLength)")
}
return key
}
}