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.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 } }