Technical Deep Dive

Technical

A detailed look at the cryptographic protocols that make identity verification possible — from key exchange to contacts encryption at rest.

Overview

IM Verify is an identity verification system designed to combat deepfake impersonation attacks. The core insight is that while AI can perfectly replicate someone's voice and face, it cannot replicate a cryptographic secret that was exchanged between two people.

The system works by establishing a shared secret between two people when they exchange contact information. This secret is then used to generate synchronized, time-based verification codes that both parties can compare over any communication channel — phone, video, or messaging.

🛡️ Key Properties

Offline Verification: Verification codes are generated locally on-device. No internet required to verify someone.
Per-Contact Keys: Each relationship has unique cryptographic keys. Your code with Alice is different from your code with Bob.
Time-Based Codes: Codes rotate every 30 seconds, preventing replay attacks.
Device-Bound: Your identity and contacts encryption key are bound to your physical device via the iOS Keychain.
Encrypted at Rest: All contact data is encrypted with AES-256-GCM before being stored.

Threat Model

IM Verify is designed to protect against impersonation attacks where an adversary attempts to convince you they are someone you know and trust.

Attacks We Defend Against

  • Voice cloning: AI-generated audio that perfectly mimics a known person's voice
  • Video deepfakes: Real-time or pre-recorded video impersonation
  • Caller ID spoofing: Falsified phone numbers or identities
  • Account compromise: Attackers using stolen accounts to message as someone else
  • Social engineering: Urgent requests exploiting trust relationships
  • Phone theft/cloning: Device binding detects when a contact's phone changes
⚠️ Important Limitation

This system cannot verify identity if the initial exchange was compromised (e.g., you added an impersonator thinking they were someone else). All exchange methods use a Short Authentication String (SAS) verbal confirmation step to protect against man-in-the-middle attacks — but SAS cannot detect an impersonator you willingly added in person.

Identity & Key Generation

When you first set up IM Verify, the app generates your cryptographic identity entirely on your device.

What Gets Created

  • Ed25519 Key Pair: A Curve25519 signing key pair. The public key is used to derive your fingerprint and for ECDH key agreement during contact exchange. The private key never leaves your device.
  • Fingerprint: The first 16 hex characters (8 bytes) of the SHA-256 hash of your public key, used as a short identifier (e.g., "A1B2C3D4E5F6G7H8").
  • Contacts Encryption Key: A 256-bit AES key generated with SecRandomCopyBytes, used to encrypt all contact data at rest.
  • PIN Hash: Your 6-digit access PIN is hashed with PBKDF2-SHA256 using a high iteration count and a random 16-byte salt. The plaintext PIN is never stored. Verification uses constant-time comparison.

Storage

All identity material is stored in the iOS Keychain with the access level kSecAttrAccessibleWhenUnlockedThisDeviceOnly. This means:

  • Data is encrypted by the device's hardware at rest
  • Data is only accessible when the device is unlocked
  • Data does not transfer via iCloud Keychain or device backup
  • Data is permanently lost if the device is wiped without export
// Identity generation (on first launch) let signingKey = Curve25519.Signing.PrivateKey() let publicKey = signingKey.publicKey let fingerprint = SHA256(publicKey.rawRepresentation) .prefix(8).hex // e.g., "A1B2C3D4E5F6G7H8" // PIN hashing (PBKDF2-SHA256, high iteration count per NIST guidelines) let salt = SecRandomBytes(16) let pinHash = PBKDF2(pin, salt: salt, iterations: N, keyLength: 32) // Contacts encryption key (256-bit random) var keyBytes = [UInt8](repeating: 0, count: 32) SecRandomCopyBytes(kSecRandomDefault, 32, &keyBytes) // Stored in Keychain — never leaves device except during export

Contact Exchange

IM Verify supports three methods for establishing contacts. All methods result in both parties sharing a unique verification seed derived via Elliptic Curve Diffie-Hellman key agreement.

Method 1: QR Code (In-Person)

The highest-trust exchange method. Both parties are physically present and scan each other's QR codes.

// Each QR code contains (pipe-delimited, protocol v5): "5|Alice|A1B2C3D4E5F6G7H8|<base64-ephemeral-key>|123456" // Fields: version | name | fingerprint | ephemeralPublicKey | deviceSecret // The QR payload is plaintext — security comes from the ECDH key // agreement, not from hiding the public key.
QR Code Exchange Flow
Alice
Shows QR
Bob
Scans QR
Bob
Shows QR
Alice
Scans QR
Both
Shared Secret

Method 2: Bluetooth (In-Person)

Both parties are physically present and exchange keys over Bluetooth Low Energy (BLE). Contact data is encrypted with ChaCha20-Poly1305 (ChaChaPoly).

  • Devices discover each other via BLE advertising with a session code (NATO word + 4 digits)
  • Ephemeral Curve25519 ECDH key exchange establishes a shared session key via HKDF-SHA256
  • Contact data (name, fingerprint, device secret) is encrypted with ChaChaPoly using the session key
  • Both parties confirm a SAS pairing code (NATO word + 4-digit number, ~260,000 combinations) to verify no man-in-the-middle attack occurred
  • Role arbitration is deterministic: lower fingerprint = Central (initiator), higher = Peripheral

Method 3: Online Exchange

For contacts who are not physically present, the app supports an asynchronous online exchange via an invite code system.

Online Exchange Flow
Alice
Creates Invite
Server
Stores E2EE Blob
Bob
Accepts Invite
Both
SAS Verification

The online exchange works as follows:

  1. Alice creates an invite, which generates a random invite code (8 characters) and a random encryption code (8 characters).
  2. Alice's contact payload (name, fingerprint, public key, device secret) is encrypted with AES-256-GCM using a key derived from the encryption code via HKDF-SHA256.
  3. The encrypted payload is uploaded to the server. The server stores only the ciphertext — it never sees the encryption code and cannot decrypt the payload.
  4. Alice shares the invite code and encryption code with Bob through a separate channel (text, email, etc.).
  5. Bob enters the invite code and encryption code in the app, which downloads Alice's encrypted payload and decrypts it locally.
  6. Bob's encrypted payload is uploaded for Alice to retrieve.
  7. Both parties now have each other's contact data and can derive a shared verification seed via ECDH.
// Online exchange encryption let encryptionCode = "K7X2M9PL" // shared out-of-band // Derive encryption key from code using HKDF let key = HKDF<SHA256>.deriveKey( inputKeyMaterial: SymmetricKey(data: encryptionCode.utf8), salt: "itsme-online-exchange-v1", info: "payload-encryption", outputByteCount: 32 ) // Encrypt payload with AES-GCM + AAD let sealed = AES.GCM.seal( payloadJSON, using: key, authenticating: "itsme-online-payload-v1" )

SAS Verification (All Exchange Methods)

Every exchange method — QR, Bluetooth, and online — includes a Short Authentication String (SAS) confirmation step to protect against man-in-the-middle attacks. After the exchange completes:

  1. Both parties see the same derived SAS code (NATO word + 4-digit number, based on the shared secret)
  2. They verbally confirm the SAS code matches — either face-to-face (for in-person exchanges) or over a phone/video call (for online exchanges)
  3. Once confirmed, the contact is marked as verified

This ensures no attacker intercepted or replaced keys during the exchange, regardless of the exchange method used.

Shared Secret Derivation (All Methods)

Regardless of exchange method, the shared verification seed is derived identically:

// ECDH key agreement let sharedSecret = try myPrivateKey.sharedSecretFromKeyAgreement( with: theirPublicKey ) // Sort fingerprints to ensure both sides derive the same seed let sortedFingerprints = [myFingerprint, theirFingerprint].sorted() // Derive verification seed using HKDF let verificationSeed = sharedSecret.hkdfDerivedSymmetricKey( using: SHA256.self, salt: sortedFingerprints.joined(), info: "itsme-verification-seed-v1", outputByteCount: 32 )
🔑 Why Sort Fingerprints?

Both parties must derive the exact same seed from the exact same inputs. By sorting fingerprints alphabetically before using them as the HKDF salt, we ensure both Alice and Bob produce identical output regardless of who initiated the exchange.

Interactive Verification

Verification is an interactive, multi-step process designed to prove both parties hold the same shared secret while defending against replay and pre-recording attacks.

Time Window Synchronization

When verification begins, the app waits until the next unused 30-second time window starts. This is calculated as floor(unixTimestamp / 30). The app tracks which time windows have been used for each contact, ensuring a window is never reused. This prevents an attacker from replaying a previously observed verification session.

The wait also ensures that both parties generate codes at the same moment, eliminating the possibility that an attacker pre-computed or pre-recorded a valid code before the session began.

Session Confirmation

Both devices display a session ID derived from the locked time window and device secrets. This is read aloud to confirm both devices are in sync before proceeding with word selection.

Five-Word Selection

Each person is shown five NATO phonetic words, derived deterministically from the shared secret, the time window, and the selector's fingerprint. Because each person's fingerprint is different, each person sees a different set of five words.

// Generate five unique words per selector let optionsKey = HKDF<SHA256>.deriveKey( inputKeyMaterial: SymmetricKey(data: seed), salt: windowData + selectorFingerprint, info: "itsme-word-options-v1" + secretsData, outputByteCount: 32 ) let mac = HMAC<SHA256>.authenticationCode( for: "word-options", using: optionsKey ) // Extract 5 unique NATO indices from HMAC bytes // Words are sorted alphabetically for display

The verification alternates who goes first — which person selects first changes each session. The first person picks one of their five words and says it aloud. Both parties tap that word in their app. Then the second person picks from their own five words, says it aloud, and both tap it.

Why Two Selections Matter

Both word selections are combined into the final code derivation. This means the resulting verification code depends on live, unpredictable input from both parties. An impersonator would need to know the shared secret, the current time window, and correctly predict both word selections to produce a matching code — and the words aren't revealed until each person freely chooses one.

🎲 Combined Selection Space

Each person selects from 5 words out of 26 NATO words. With both parties selecting independently, the combined space is 5 × 5 = 25 possible code outcomes per session. But an attacker doesn't know the five-word sets (which are derived from the secret they don't have), so they can't narrow the possibilities. They would need the shared secret to even see the correct word options.

Final Code Generation

After both selections, the final verification codes are generated:

// Both word selections feed into key derivation let selections = "\(word1):\(word2)" let interactiveKey = HKDF<SHA256>.deriveKey( inputKeyMaterial: SymmetricKey(data: seed), salt: windowData + selections, info: "itsme-interactive-code-v1" + secretsData, outputByteCount: 32 ) // Generate directional codes from the interactive key let myCode = generateCodeFromKey(key: interactiveKey, fingerprint: myFingerprint) let theirCode = generateCodeFromKey(key: interactiveKey, fingerprint: theirFingerprint)

Each party sees two codes: "Your Code" (derived with their own fingerprint) and "Their Code" (derived with the contact's fingerprint). When Alice reads her "Your Code" to Bob, it should match what Bob sees as "Their Code" for Alice — and vice versa.

Interactive Verification Flow
Wait
Fresh Window
Session
Confirm ID
Person A
Picks Word
Person B
Picks Word
Both
Compare Codes

Anti-Replay Properties

  • Fresh time window: Codes can't be pre-recorded because the session waits for an unused window to begin
  • Window tracking: Each time window can only be used once per contact, preventing replay of observed sessions
  • Interactive selections: Both parties contribute unpredictable input, so codes can't be computed in advance
  • Alternating order: Who selects first changes each session, preventing pattern-based attacks

NATO Phonetic Alphabet

Words are chosen from the NATO phonetic alphabet for clarity over voice channels:

ALFA, BRAVO, CHARLIE, DELTA, ECHO, FOXTROT, GOLF, HOTEL, INDIA, JULIET, KILO, LIMA, MIKE, NOVEMBER, OSCAR, PAPA, QUEBEC, ROMEO, SIERRA, TANGO, UNIFORM, VICTOR, WHISKEY, XRAY, YANKEE, ZULU

Online Verify

For contacts who are not in the same location, IM Verify supports Online Verify — a challenge-response protocol over an encrypted WebSocket relay. This allows you to verify someone's identity during a phone call, video call, or even via instant messaging without needing to be physically present.

Online Verify Flow
Both
Join Room
Both
DH + Challenge
Both
Exchange Proof
Both
Confirm SAS
  • Room ID: SHA-256 of the verification seed + current 30-second time window + device secrets. Both phones derive the same room ID independently — only the correct contact pair can join the same room.
  • Forward secrecy: A fresh ephemeral Curve25519 DH key exchange is performed each session. All messages are encrypted with AES-256-GCM using a transport key derived from the DH shared secret + seed + time window.
  • Challenge-response: Each phone generates a random 32-byte challenge. Both challenges are exchanged, sorted, and used to compute an HMAC-SHA256 proof. Proofs are compared with constant-time comparison.
  • SAS confirmation: A Short Authentication String (NATO word + 4-digit number) is derived from both challenges + seed + device secrets. Users confirm this over voice to detect any man-in-the-middle relay by the server.
  • No time window burn: Unlike Voice Verify, Online Verify does not consume a time window — the random 32-byte challenges provide replay protection instead.
🌐 Server-Blind Design

The WebSocket server acts as a dumb relay — it forwards encrypted messages between two clients in the same room. It cannot decrypt messages (no access to the DH private keys or verification seed), determine who is in the room (room IDs are opaque hashes), or tamper with messages (AES-GCM authentication detects modification). The SAS confirmation step catches any active MITM relay attack.

Device Binding & Phone Change Detection

Each contact exchange includes a device secret — a random 6-digit code stored in the iOS Keychain. This secret is unique per contact and serves as a second factor tied to the physical device.

How It Works

  • During exchange, each party shares their device secret alongside their public key
  • Device secrets are stored in the Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly
  • Because the Keychain doesn't transfer via iCloud backup, device secrets are lost when switching phones
  • When someone switches phones — even with a proper identity export — their device secrets are lost and will not match. They will need to share their degradation code with each contact to confirm the phone change is legitimate.

Detection

When verifying, the app checks whether the contact's device secret matches what was originally exchanged. If it doesn't match, the contact is shown in a "degraded" state with a warning that their phone may have changed. This helps detect:

  • Phone theft or cloning attempts
  • Unauthorized device transfers
  • SIM swapping attacks (where the attacker sets up on a new device)

Degradation Process

Device secrets are bound to the physical device via the iOS Keychain (kSecAttrAccessibleWhenUnlockedThisDeviceOnly) and do not transfer — not even via identity export. Any phone change will trigger degradation with each contact. The process:

  1. After switching phones, the device secret mismatch is detected and the contact enters the "degraded" state
  2. The person who changed phones sees a degradation code that they must share with each affected contact (over a call, message, etc.)
  3. The contact enters this code in the app to confirm the phone change is legitimate
  4. Verification continues to work normally — codes are derived from the shared cryptographic seed, not the device secret. The degraded state is a warning indicator only.

Security PIN Update (Recovery)

While verifications work in the degraded state, the contact remains flagged until device secrets are refreshed. When both parties are together in person, they can use Security PIN Update to restore full status:

  • Both phones derive a deterministic BLE session code from the shared verification seed + existing device secrets
  • A fresh ephemeral ECDH key exchange establishes a ChaChaPoly-encrypted channel
  • Each phone generates a new random 6-digit device secret and sends it encrypted
  • A role-ordered commit ensures both phones save atomically: the Central (determined by fingerprint comparison) sends a DONE signal via reliable BLE write, then saves. The Peripheral waits for DONE, then saves.
⚠️ Not Always Malicious

A degraded contact doesn't necessarily mean compromise. The most common cause is someone upgrading their phone — device secrets are always lost on a phone change, even with identity export. The degradation code confirms the change is intentional, verifications continue to work normally, and a Security PIN Update when you're next together in person restores the contact to full status.

Contacts Encryption at Rest

All contact data is encrypted before being written to device storage, protecting it even if the device storage is compromised.

Encryption Scheme

// Encrypt contacts before saving let contactsJSON = try JSONEncoder().encode(contacts) let encryptionKey = KeychainService.loadContactsEncryptionKey() let sealed = try AES.GCM.seal(contactsJSON, using: encryptionKey) // Store nonce + ciphertext + tag as single blob let combined = sealed.nonce + sealed.ciphertext + sealed.tag UserDefaults.set(combined, forKey: "contacts_encrypted")

Key Management

  • The encryption key is a 256-bit AES key generated with SecRandomCopyBytes
  • Stored in the iOS Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly
  • The key does not back up to iCloud — it is bound to the physical device
  • During device transfer, the key is included in the password-protected identity export

What This Means

If your device is backed up to iCloud, the contacts data is included in the backup — but as AES-256-GCM ciphertext. Without the encryption key (which is in the Keychain and doesn't back up), the data is unreadable. On a restored device, the app detects that it has encrypted data but no key, and prompts you to import your identity from your old phone.

Device Transfer

When switching to a new phone, your cryptographic identity and contacts encryption key need to be transferred. The app provides a secure export/import flow.

Export (Old Phone)

  1. Go to Settings → Export Identity (requires PIN verification)
  2. Create a password (minimum 8 characters) to protect the export
  3. A QR code is generated containing your double-encrypted identity

What's in the Export

The export uses a double-wrap encryption scheme with two independent PBKDF2 + AES-GCM layers:

// Export payload (version 5) — double-wrap encryption // Inner layer: PBKDF2 + AES-GCM let innerSalt = SecRandomBytes(16) let innerKey = PBKDF2(password, salt: innerSalt, iterations: N, keyLength: 32) let innerBox = AES.GCM.seal(identityJSON, using: innerKey) // Outer layer: HMAC-derived password + PBKDF2 + AES-GCM let outerPass = HMAC<SHA256>(key: password, message: "im.transfer.seal") let outerSalt = SecRandomBytes(16) let outerKey = PBKDF2(outerPass, salt: outerSalt, iterations: N, keyLength: 32) let outerBox = AES.GCM.seal(innerBox, using: outerKey) // Final blob: [innerSalt 16B] + [outerSalt 16B] + [outerBox]

Import (New Phone)

  1. Install IM Verify on your new phone
  2. The app detects encrypted contacts (from iCloud backup) and prompts for import
  3. Scan the export QR code from your old phone
  4. Enter your password (minimum 8 characters) to decrypt both layers and restore your identity
  5. Contacts are now decryptable and your identity is restored

Import has brute-force protection: 5 failed password attempts trigger a 5-minute lockout.

⚠️ Device Secrets Don't Transfer

The export transfers your identity key and contacts encryption key, but device secrets are not included. They are bound to the old phone's Keychain and cannot be extracted. After importing on a new phone, all your contacts will appear in a "degraded" state. You'll need to share your degradation code with each contact and, when possible, use Security PIN Update in person to restore full status.

Wrong Identity Detection

If you scan an export QR from a different identity (not the one that encrypted the contacts on this device), the app detects the mismatch — the imported encryption key won't be able to decrypt the stored contacts. You'll be warned and given the option to cancel or proceed (which will clear the unreadable contacts).

Device Transfer Flow
Old Phone
Export + Password
QR Code
Double-Wrap AES-GCM
New Phone
Scan + Password
Result
Identity + Key Restored
⚠️ Don't Lose Your Password

If you forget your export password, you'll need to create a new identity and re-add all contacts. There is no recovery mechanism by design — this protects against unauthorized identity theft.

Security Analysis

Why Deepfakes Can't Bypass This

An attacker using AI to impersonate someone faces an impossible challenge: they need to produce a matching verification code, but the code is derived from a shared secret they don't have access to. Even if they perfectly clone the victim's voice and face, they cannot produce the correct code.

Attack Result
Attacker clones voice and asks for code ❌ Attacker can't provide matching code back
Attacker intercepts network traffic ❌ Codes are not transmitted; derived locally
Attacker records and replays old codes ❌ Codes expire after 30 seconds
Attacker intercepts online exchange ❌ Payloads are E2EE; SAS verification catches MITM
Attacker steals victim's phone ⚠️ Requires Face ID / PIN to access app
Attacker clones victim's phone ⚠️ Device binding detects the change
Attacker accesses iCloud backup ❌ Contacts encrypted; key not in backup

Algorithms & Standards

Component Algorithm Standard
Identity Key Pair Ed25519 (Curve25519) RFC 8032 / RFC 7748
Key Agreement ECDH (X25519) RFC 7748
Key Derivation HKDF-SHA256 RFC 5869
Password Hashing PBKDF2-SHA256 (NIST-recommended iteration count) NIST SP 800-132
Verification Codes HMAC-SHA256 RFC 2104
Contacts Encryption AES-256-GCM NIST SP 800-38D
Online Exchange Encryption AES-256-GCM + HKDF NIST SP 800-38D / RFC 5869
BLE Transport Encryption ChaCha20-Poly1305 RFC 8439
Export Encryption Double-wrap PBKDF2 + AES-256-GCM NIST SP 800-132 / NIST SP 800-38D
API Authentication HMAC-SHA256 RFC 2104
Key Storage iOS Keychain Apple Security Framework
Biometric Auth Face ID / Touch ID LocalAuthentication
💻 Open Design

The protocol is intentionally simple and uses well-established cryptographic primitives from Apple's CryptoKit framework. Security comes from the mathematical properties of these algorithms, not from obscurity.

Server Architecture

The server component is minimal by design. It serves as a temporary relay for online contact exchanges and as a WebSocket relay for Online Verify sessions.

What the Server Sees

  • SHA-256 hashes of user fingerprints (not the fingerprints themselves)
  • Encrypted payloads that it cannot decrypt (the encryption key is never sent to the server)
  • Timestamps of when invites are created and accepted
  • IP addresses (as part of normal HTTP communication)

What the Server Cannot Do

  • Read any user's name or identity
  • Decrypt any payload (the encryption code is shared between users out-of-band)
  • Determine who is communicating with whom (only hashed fingerprints are visible)
  • Generate or predict verification codes
  • Tamper with payloads (AES-GCM provides authentication — any modification is detected)

Data Lifecycle

All invite data is automatically deleted after 24 hours or upon successful exchange, whichever comes first. WebSocket relay messages are forwarded in real-time and never stored. The server stores no long-term user data.