Two words. 10 seconds. Know it's really them.
Deepfake technology has made it trivially easy to clone anyone's voice or face in real time. What used to require a Hollywood studio now takes a laptop and an open-source model.
The consequences are immediate and financial:
- $25 million wired by a Hong Kong finance worker who was convinced by a deepfake CFO on a Zoom call (2024)
- $35 million lost by a UAE bank after a deepfake voice call authorized a transfer
- The FBI publicly warned in 2024 that AI voice cloning is being used to impersonate family members in emergency scam calls
The attacks work because we evolved to trust voices and faces. We never had to verify them before — they were unforgeable. They aren't anymore.
A sophisticated attacker spoofs caller ID or intercepts the callback. This fails against nation-state actors, organized fraud rings, and anyone with $50 worth of VoIP tooling.
Static codewords are a good instinct but fail in practice. A real-time deepfake that intercepts the call can hear Alice say "Jupiter" and relay it to Bob before he notices the delay. A static word can also be extracted through social engineering — one successful impersonation and the attacker knows your word forever.
This is an arms race you will lose. Detectors are always trained on yesterday's deepfakes. The generation models improve faster than the detection models. Building your security on detection is building on sand.
YubiKey solves the right problem — cryptographic authentication — but for the wrong relationship. It authenticates you to a server. It has no concept of authenticating you to a person. There is no product in this category that answers the question: "Is the human on this call who they say they are?"
realxreal gives every contact relationship a pair of rotating codewords backed by a shared cryptographic secret. The words change every minute (configurable). Both parties generate them independently from the same seed, and no server is involved at verification time.
Alice's screen: Bob's screen:
┌─────────────────┐ ┌─────────────────┐
│ YOUR WORD │ │ YOUR WORD │
│ falcon │ │ river │
│ │ │ │
│ BOB'S WORD │ │ ALICE'S WORD │
│ river │ │ falcon │
│ │ │ │
│ ↻ 42s remaining │ │ ↻ 42s remaining │
└─────────────────┘ └─────────────────┘
Alice says "falcon." Bob confirms he sees "falcon" as Alice's word. Bob says "river." Alice confirms she sees "river" as Bob's word. Both verified. Both in the same time window. A relay attacker who intercepts "falcon" still has to produce "river", which they cannot do without the shared seed.
This is mutual authenticationL both parties authenticate to each other simultaneously. No relay attack works. No static word can be stolen. No AI model can predict the next word without the seed.
realxreal implements HOTP (RFC 4226) with HMAC-SHA256 rather than the legacy SHA-1 used by standard authenticator apps. Since realxreal is not bound by TOTP interoperability requirements — both endpoints are realxreal devices — we use the stronger primitive.
// RXRTOTPEngine.swift
static func hotp(seed: Data, counter: UInt64) -> UInt32 {
var c = counter.bigEndian
let counterData = Data(bytes: &c, count: 8)
let key = SymmetricKey(data: seed)
// HMAC-SHA256 produces a 32-byte digest
let mac = HMAC<SHA256>.authenticationCode(for: counterData, using: key)
let bytes = Array(mac)
// Dynamic truncation — offset from last nibble of SHA-256 digest
// Note: bytes[31] not bytes[19] — SHA-256 is 32 bytes, not SHA-1's 20
let offset = Int(bytes[31] & 0x0f)
let value = (UInt32(bytes[offset] & 0x7f) << 24)
| (UInt32(bytes[offset + 1] & 0xff) << 16)
| (UInt32(bytes[offset + 2] & 0xff) << 8)
| UInt32(bytes[offset + 3] & 0xff)
return value
}The 31-bit HOTP value is split into two independent 12-bit indices — one for each party in the relationship:
static func wordPair(seed: Data, period: RXRPeriod, ...) -> (my: String, their: String) {
let val = hotp(seed: seed, counter: counter)
let count = UInt32(RXRWordlist.words.count) // 4096
let myIdx = Int((val & 0x0FFF) % count) // bits 0–11
let theirIdx = Int(((val >> 12) & 0x0FFF) % count) // bits 12–23
return (RXRWordlist.words[myIdx], RXRWordlist.words[theirIdx])
}The 12-bit mask guarantees values 0–4095, making % 4096 a no-op safety net with zero modulo bias. 24 bits are consumed from the 31-bit HOTP value, leaving 7 bits unused — clean extraction with no overlap.
The initiator (whoever generated the invite) and the recipient (whoever scanned/redeemed it) see the same two words but with their labels swapped:
// RXRContactStore.swift
func wordPair(for contact: RXRContact) -> (my: String, their: String)? {
guard let seed = getSeed(for: contact.id) else { return nil }
let pair = RXRTOTPEngine.wordPair(seed: seed, period: contact.period)
return contact.isInitiator
? (my: pair.my, their: pair.their) // Alice: bits 0-11 = mine
: (my: pair.their, their: pair.my) // Bob: bits 12-23 = mine
}This means Alice says "falcon" and Bob says "river" — each sees their own word labeled "YOUR WORD." A relay attacker who intercepts Alice's word still cannot produce Bob's without the shared seed.
Each contact relationship has its own configurable rotation period. Close family might use 30 seconds for maximum security. An occasional business contact might use 1 week:
enum RXRPeriod: TimeInterval, CaseIterable, Codable {
case thirtySeconds = 30
case oneMinute = 60
case oneHour = 3600
case oneDay = 86400
case oneWeek = 604800
case oneMonth = 2592000
}
static func counter(period: RXRPeriod, timeOffset: TimeInterval = 0) -> UInt64 {
let adjusted = Date().timeIntervalSince1970 + timeOffset
return UInt64(adjusted / period.rawValue)
}The timeOffset is calculated on launch by comparing local device time to Firebase server time — correcting for clock drift without capping the correction, since the server clock (backed by Google TrueTime) is authoritative regardless of how wrong the device clock is.
realxreal uses the first 4096 words from the EFF Large Wordlist. This is a public domain list designed specifically for human-readable random word generation.
The EFF list was chosen because:
- Every word is short, unambiguous, and speakable over a phone call
- No homophones (no "bare" / "bear" confusion)
- No offensive or sensitive terms
- No easily confused pairs
- Designed by security researchers for exactly this use case
4096 words was chosen because it is a power of 2 (2¹²), which combined with the 12-bit mask extraction produces zero modulo bias — every word appears with exactly equal probability.
words: 4096 → pairs: 4096 × 4096 = 16,777,216 possible combinations
At 100,000 active simultaneous users, the probability of any two unconnected users sharing the same pair in the same time window is negligible. A future upgrade to three words (bits 0-11, 12-23, 24-35 of a second HOTP call) extends this to 68 billion combinations.
Seeds are generated using CryptoKit.SymmetricKey(size: .bits256) — 256 bits of cryptographically secure random entropy from the Secure Enclave. They are stored in the iOS Keychain with kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly:
- Requires device passcode to be set
- Destroyed if passcode is removed
- Never backed up to iCloud
- Never migrated to another device
// RXRKeychainService.swift
static func save(seed: Data, for contactID: String) -> Bool {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "ai.realxreal.rxr.seeds",
kSecAttrAccount: contactID,
kSecValueData: seed,
kSecAttrAccessible: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
]
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}To avoid Keychain IPC (a round-trip to the securityd daemon) on every UI render, seeds are loaded once at launch into an in-memory dictionary. All word generation reads from this cache — no Keychain access during scrolling or display:
// RXRContactStore.swift
private var seedCache: [String: Data] = [:]
func wordPair(for contact: RXRContact) -> (my: String, their: String)? {
guard let seed = seedCache[contact.id] else { return nil } // O(1) dictionary lookup
// ...
}When Alice sends Bob an invite link, the seed travels encrypted. The decryption key travels exclusively in the URL fragment — which is stripped by every browser and server before the request is transmitted. Firebase receives only ciphertext it cannot decrypt:
https://join.realxreal.app/{token}?from=Alice#{AES-256-GCM-key}
↑ ↑
reaches Firebase never reaches Firebase
(token lookup) (stays in Bob's browser/app)
// RXRInviteCrypto.swift
static func encrypt(senderFirstName: String, ..., seed: Data, period: RXRPeriod) throws -> RXREncryptedInvite {
let token = UUID().uuidString.lowercased() // generated locally, embedded in payload
let payload = RXRInvitePayload(version: "1", token: token, ...)
let key = SymmetricKey(size: .bits256) // ephemeral, per-invite
let sealed = try AES.GCM.seal(try JSONEncoder().encode(payload), using: key)
// Key encoded as Base64URL — safe in URL fragments across all messaging apps
let keyString = key.withUnsafeBytes { Data(Array($0)).base64EncodedString() }
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
return RXREncryptedInvite(
token: token,
ciphertext: sealed.combined!.base64EncodedString(),
encryptionKey: keyString
)
}The token is embedded inside the encrypted payload and verified after decryption — closing the substitution attack vector where a MITM swaps the ciphertext under a known token.
realxreal-iOS/
├── App/
│ ├── realxreal_iOSApp.swift Entry point, Firebase init, NTP sync on launch
│ └── RXRAppState.swift Screen router, deep link queue, loading states
├── Scenes/
│ ├── Onboarding/ Welcome, import contacts, select self, name yourself
│ ├── Home/ Contact list, circular timer, edit profile
│ └── Contact/ Contact sheet, manual add wizard, QR scanner/result
├── Services/
│ ├── RXRTOTPEngine.swift HOTP/TOTP — HMAC-SHA256, word pair extraction
│ ├── RXRWordlist.swift 4096-word EFF list
│ ├── RXRKeychainService.swift Seed storage with WhenPasscodeSet access policy
│ ├── RXRUserProfile.swift Local user identity (encoded in QR/invite links)
│ ├── RXRFirebaseService.swift Cloud Function calls (createInvite, redeemInvite, serverTime)
│ ├── RXRInviteCrypto.swift AES-256-GCM seal/open, Base64URL, token binding
│ ├── RXRInviteService.swift Orchestrates encrypt → upload → URL build
│ ├── RXRDeepLinkHandler.swift Universal Link + custom scheme parsing, clipboard recovery
│ └── RXRTimeService.swift SNTP clock drift correction
├── Stores/
│ └── RXRContactStore.swift Single source of truth, seed cache, role-aware word pairs
└── Theme/
├── DesignTokens.swift Colors, spacing, typography (colorful + mono themes)
├── RXRComponents.swift Reusable SwiftUI components
└── RXRCircularTimer.swift TimelineView-based countdown ring
- Xcode 15+
- iOS 16+ device or simulator
git clone https://github.com/Trust-Worthy/realxreal-iOS.git
cd realxreal-iOS
open realxreal-iOS.xcodeproj⌘+B # Build
⌘+R # Run on simulator or deviceUniversal Links and camera QR scanning require a real device.
realxreal-iOS connects to realxreal-relay — a zero-knowledge Firebase relay that stores encrypted invite payloads temporarily during the seed exchange. The server never has access to decryption keys or plaintext seeds.
Three Cloud Functions:
| Function | Purpose |
|---|---|
createInvite |
Stores encrypted payload, returns token |
redeemInvite |
Returns payload (single-use, 24hr TTL) |
serverTime |
Returns server timestamp for NTP drift correction |
- realxreal-relay — Firebase backend
- realxreal.app — App Store landing page
- realxreal.ai — Brand and protocol
This is an open cryptographic protocol. Contributions welcome — especially:
- Wordlist improvements and localization
- Security audits of the TOTP implementation
- UI accessibility improvements
Please open an issue before submitting a large PR.
GNU Affero General Public License v3.0 — see LICENSE
AGPL-3.0 means:
- You can use, study, modify, and distribute this code freely
- If you run a modified version as a network service, you must release your modifications under the same license
- Proprietary forks that offer realxreal as a closed SaaS are not permitted
The EFF Large Wordlist is used under Creative Commons Attribution 3.0. © 2016 Electronic Frontier Foundation — eff.org





