initial commit: Proximity iOS app proposal, full Swift code scaffold, reference backend, testing docs, generated Xcode project
This commit is contained in:
57
Proximity/Services/Security/CryptoManager.swift
Normal file
57
Proximity/Services/Security/CryptoManager.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
/// End-to-end encryption for revealed conversations.
|
||||
///
|
||||
/// Uses X25519 key agreement + ChaChaPoly AEAD, the same primitives family
|
||||
/// used by Signal. The server only ever sees ciphertext and opaque key IDs —
|
||||
/// it cannot read message contents.
|
||||
struct CryptoManager {
|
||||
|
||||
// MARK: - Key pair
|
||||
|
||||
func generateKeyPair() -> (privateKey: Curve25519.KeyAgreement.PrivateKey,
|
||||
publicKey: Curve25519.KeyAgreement.PublicKey) {
|
||||
let privateKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
return (privateKey, privateKey.publicKey)
|
||||
}
|
||||
|
||||
// MARK: - Session establishment
|
||||
|
||||
/// Derive a shared symmetric session key from two key pairs.
|
||||
func deriveSessionKey(
|
||||
myPrivate: Curve25519.KeyAgreement.PrivateKey,
|
||||
theirPublic: Curve25519.KeyAgreement.PublicKey
|
||||
) throws -> SymmetricKey {
|
||||
let shared = try myPrivate.sharedSecretFromKeyAgreement(with: theirPublic)
|
||||
// Salt with a fixed app domain string to keep keys per-app.
|
||||
return shared.hkdfDerivedSymmetricKey(
|
||||
using: SHA256.self,
|
||||
salt: Data("proximity.e2e.v1".utf8),
|
||||
sharedInfo: Data(),
|
||||
outputByteCount: 32
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Message encryption
|
||||
|
||||
func encrypt(_ plaintext: Data, using key: SymmetricKey) throws -> Data {
|
||||
let sealed = try ChaChaPoly.seal(plaintext, using: key)
|
||||
return sealed.combined
|
||||
}
|
||||
|
||||
func decrypt(_ combined: Data, using key: SymmetricKey) throws -> Data {
|
||||
let box = try ChaChaPoly.SealedBox(combined: combined)
|
||||
return try ChaChaPoly.open(box, using: key)
|
||||
}
|
||||
|
||||
// MARK: - Signatures (for token authenticity)
|
||||
|
||||
func sign(_ data: Data, with key: Curve25519.Signing.PrivateKey) throws -> Data {
|
||||
try key.signature(for: data)
|
||||
}
|
||||
|
||||
func verify(_ data: Data, signature: Data, with key: Curve25519.Signing.PublicKey) -> Bool {
|
||||
key.isValidSignature(signature, for: data)
|
||||
}
|
||||
}
|
||||
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