initial commit: Proximity iOS app proposal, full Swift code scaffold, reference backend, testing docs, generated Xcode project
This commit is contained in:
52
Proximity/Services/Security/TokenManager.swift
Normal file
52
Proximity/Services/Security/TokenManager.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user