Files
Nearfield_Friends/Proximity/Services/Security/TokenManager.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
}
}