initial commit: Proximity iOS app proposal, full Swift code scaffold, reference backend, testing docs, generated Xcode project

This commit is contained in:
2026-08-12 00:22:01 +00:00
commit 9d1b0f8dd9
52 changed files with 6271 additions and 0 deletions

View 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)
}
}

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