53 lines
2.1 KiB
Swift
53 lines
2.1 KiB
Swift
import Foundation
|
|
import CryptoKit
|
|
|
|
/// Manages the user's **ephemeral rotating anonymous token**.
|
|
///
|
|
/// This is the heart of the privacy model. While present, the app broadcasts a
|
|
/// token that:
|
|
/// - contains **no identity** (no name, no persistent ID),
|
|
/// - **rotates** on a short interval (e.g. every 5 minutes) so it cannot be
|
|
/// used to track the user over time,
|
|
/// - is only meaningful to the Proximity backend, which maps two tokens that
|
|
/// were physically near each other into an anonymous encounter.
|
|
///
|
|
/// The token is derived from a secret + time window, so it changes
|
|
/// deterministically without needing to re-broadcast a new random value.
|
|
struct TokenManager {
|
|
|
|
/// How long a single token remains valid before rotating.
|
|
static let rotationInterval: TimeInterval = 5 * 60 // 5 minutes
|
|
|
|
private let secret: SymmetricKey
|
|
|
|
init(secret: SymmetricKey = SymmetricKey(size: .bits256)) {
|
|
self.secret = secret
|
|
}
|
|
|
|
/// The current anonymous token for a given time window.
|
|
/// Deterministic per window so both the broadcast and the backend agree.
|
|
func currentToken(at date: Date = Date()) -> String {
|
|
let window = Int(date.timeIntervalSince1970 / Self.rotationInterval)
|
|
return token(forWindow: window)
|
|
}
|
|
|
|
/// The token for a specific rotation window.
|
|
func token(forWindow window: Int) -> String {
|
|
var data = Data()
|
|
data.append(String(window).data(using: .utf8)!)
|
|
let mac = HMAC<SHA256>.authenticationCode(for: data, using: secret)
|
|
return Data(mac).base64EncodedString()
|
|
}
|
|
|
|
/// Whether a received remote token is still within a valid rotation window.
|
|
func isValid(_ token: String, at date: Date = Date()) -> Bool {
|
|
// Tokens are valid for the current window plus one prior window,
|
|
// to tolerate clock skew between devices.
|
|
let current = Int(date.timeIntervalSince1970 / Self.rotationInterval)
|
|
for window in (current - 1)...current {
|
|
if token(forWindow: window) == token { return true }
|
|
}
|
|
return false
|
|
}
|
|
}
|