Vauchi Documentation
Privacy-focused updatable contact cards via in-person exchange. End-to-end encrypted, decentralized.
What is Vauchi?
Vauchi is a contact card that updates automatically. When you change your phone number, everyone who has your card sees the change.
- No sign-up required — Your device is your identity
- No phone number required — Exchange contact cards in person
- End-to-end encrypted — Only you and your contacts can read your data
- Open source — Verify every claim yourself
Quick Links
| 👤 Getting Started | Set up and exchange |
| ❓ FAQ | Common questions |
| 🔒 Security | Data protection |
| 💜 Our Principles | Why we built it |
For Developers
- Contributing Guide — How to contribute to Vauchi
- Architecture Overview — System design and components
- Cryptography Reference — Encryption and key management details
Get the App
- Desktop: Coming soon (macOS, Windows, Linux)
- CLI/TUI: Coming soon
- iOS: Coming soon
- Android: Coming soon
Source Code
Your privacy matters. Vauchi is built to prove it.
For Users
Welcome to Vauchi! This section contains everything you need to get started and make the most of the app.
Getting Started
New to Vauchi? Start here:
- Getting Started Guide — Set up your identity and exchange your first contact
- FAQ — Answers to common questions
Features
Learn what Vauchi can do:
| Feature | Description |
|---|---|
| Contact Exchange | Exchange contact cards in person via QR code |
| Privacy Controls | Control what each contact can see |
| Multi-Device Sync | Use Vauchi on multiple devices |
| Auto Updates | Your contacts always have your latest info |
| Backup & Recovery | Protect and restore your identity |
| Encryption | How your data stays private |
How-To Guides
Step-by-step instructions:
| Guide | What You'll Learn |
|---|---|
| Exchange Contacts | How to add someone using QR codes |
| Set Up Multi-Device | Link Vauchi to another device |
| Recover Your Account | Restore access after losing a device |
| Manage Visibility | Control who sees what |
Need Help?
Getting Started
Welcome to Vauchi! This guide will help you set up your identity and exchange your first contact.
Creating Your Identity
When you first open Vauchi, you'll be asked to create your identity:
- Enter your display name — This is how contacts will see you
- Tap "Create Identity" — Vauchi generates your unique cryptographic identity
Your identity includes:
- A unique public ID (like a fingerprint for your identity)
- Cryptographic keys for secure communication
- Your display name
Your identity keys are stored only on your device. There's no account to log into — your device IS your account.
Understanding the Home Screen
After creating your identity, you'll see:
- Your Card — The contact information you've added
- Fields — Individual pieces of contact info (email, phone, etc.)
- Navigation — Access to Contacts, Exchange, and Settings
Adding Your Contact Information
Your contact card contains fields — pieces of information you want to share with contacts.
To Add a Field
- Tap the + button on the home screen
- Select a field type:
- Email — Email addresses
- Phone — Phone numbers
- Website — URLs and websites
- Address — Physical addresses
- Birthday — Date of birth
- Social — Social media profiles
- Custom — Any other information
- Enter a label (e.g., "Work", "Personal")
- Enter the value (e.g., "john@example.com")
- Tap Add
Your First Exchange
Ready to exchange contacts? Here's the quick version:
- Meet someone in person
- Open the Exchange tab
- Show them your QR code
- Scan their QR code
- Done — you're connected!
For detailed instructions, see the Exchange Contacts guide.
What's Next?
Now that you're set up:
- Control what people see — Hide your home address from work contacts
- Set up multi-device — Use Vauchi on your phone and tablet
- Create a backup — Protect your identity in case you lose your device
Tips for New Users
Security Tips
- Create a backup as soon as you set up
- Verify important contacts in person
- Use a strong backup password (passphrase recommended)
Privacy Tips
- Review visibility settings when adding new fields
- Use specific labels to control field visibility precisely
- Check what contacts can see periodically
Organization Tips
- Use descriptive labels (e.g., "Work Email", "Personal Cell")
- Update outdated information promptly
Need Help?
- Check the FAQ for common questions
- See features for detailed explanations
- Read the how-to guides for step-by-step instructions
Frequently Asked Questions
Answers to common questions about Vauchi.
Privacy & Security
Is my data encrypted?
Yes, comprehensively:
- At rest: All data on your device is encrypted with XChaCha20-Poly1305 using a key stored in your device's platform keychain (iOS Keychain / Android KeyStore)
- In transit: All communication uses end-to-end encryption (X25519 + XChaCha20-Poly1305)
- Backups: Protected with Argon2id key derivation and XChaCha20-Poly1305
Can the relay server read my contacts?
No. The relay server only sees encrypted message envelopes. It cannot:
- Decrypt any message content
- See your contact list
- Read your contact card fields
- Associate your identity with your data
The relay is essentially a "dumb pipe" that passes encrypted blobs between devices.
What data does the relay server store?
Only:
- Encrypted message envelopes (deleted after delivery or 120 days)
- Connection metadata for rate limiting (cryptographic identity hash — deleted after 30 minutes of inactivity)
Is Vauchi truly private?
Yes. Vauchi is designed with privacy as the core principle:
- Local-first architecture (your data lives on your device)
- End-to-end encryption (we can't read your data even if we wanted to)
- No analytics or tracking
- No cloud accounts
- Open source (you can verify our claims)
Identity & Account
What happens if I lose my device?
You have several options:
- Another linked device: Continue using Vauchi normally
- Backup: Restore your identity from an encrypted backup
- Social recovery: Get vouchers from contacts who can verify your identity
- Start fresh: Create a new identity and re-exchange with contacts
How does social recovery work?
Social recovery uses your real-world relationships to verify your identity:
- You create a "recovery claim" on a new device
- You share this claim with trusted contacts
- Each contact creates a "voucher" confirming they recognize you
- After collecting enough vouchers (typically 3), your contacts are migrated to your new identity. Note: social recovery creates a new cryptographic identity — your old signing keys cannot be recovered. Visibility settings may need to be reconfigured.
This prevents both:
- You being locked out (contacts can vouch for you)
- Someone else stealing your identity (they'd need to fool multiple contacts)
Can I change my display name?
Yes, go to Settings and edit your display name. The change syncs to all your devices and contacts see the update.
Do I need an account?
No. Your identity is created on your device. There's nothing to sign up for.
Contacts & Exchange
How do I exchange contacts?
- Meet the person in real life
- Open the Exchange screen
- Show them your QR code to scan
- Scan their QR code
- Done! You're now contacts
Why do I need to meet in person?
In-person exchange ensures:
- You're connecting with who you think you are
- No man-in-the-middle can intercept the exchange
- The trust relationship is established in the real world
Can I exchange contacts remotely?
Not currently. Exchange requires physical proximity — both people must be in the same location so the devices can verify co-presence. This is a deliberate security design to prevent man-in-the-middle attacks.
Can I remove a contact?
Yes:
- Go to Contacts
- Select the contact
- Tap Delete/Remove
- Confirm
This removes them from your device. They still have your data (whatever was visible to them), but won't receive future updates.
Multi-Device
Can I use Vauchi on multiple devices?
Yes! Vauchi supports multi-device sync:
- Set up Vauchi on your first device
- Go to Settings, open the Devices screen, and generate a device link
- Follow the linking process on your second device
- Both devices now share the same identity
How many devices can I link?
Up to 10 devices can be linked to one identity.
How do I migrate to a new phone?
Method 1: Device Linking
- On old phone: Go to Settings, then open the Devices screen and generate a device link
- On new phone: Install Vauchi and join existing identity
- Once synced, you can uninstall from old phone
Method 2: Backup & Restore
- On old phone: Create an encrypted backup
- On new phone: Install Vauchi and restore from backup
Backup & Restore
How do backups work?
- You create a backup with a password you choose
- Vauchi encrypts all your data using that password
- You receive a backup file or code (format varies by platform)
- To restore: backup data + password = your identity
What's included in a backup?
- Your identity (cryptographic keys)
- Your display name
- Device information
Contacts are NOT included in the identity backup. Contact relationships are re-established through the relay when you restore.
I forgot my backup password. Can you recover it?
No. The encryption is designed so that only you can decrypt your backup. This is a security feature, not a bug. Without the password, the backup cannot be decrypted by anyone, including us.
Visibility & Sharing
How do I control what each contact sees?
- Open a contact's detail page
- Scroll to "What They Can See"
- Toggle individual fields on/off
Do contacts know when I hide fields?
They see fields disappear from your card, but don't receive a notification. It appears as if you removed the field.
Can I share different info with different contacts?
Yes! That's the core feature:
- Work contacts: Show work email, hide personal phone
- Family: Show everything
- Acquaintances: Show only basic info
Technical
What's the relay server for?
The relay server:
- Routes encrypted messages between your devices
- Enables real-time sync
- Stores messages temporarily if a device is offline
- Cannot read any message content
Think of it like a post office that handles sealed envelopes.
Does Vauchi work offline?
Partially:
- You can view all your data offline
- You can make changes offline
- Changes sync when you're back online
- You cannot exchange contacts offline (needs camera + network)
What encryption does Vauchi use?
- Signing: Ed25519
- Key Exchange: X25519 (Curve25519)
- Symmetric Encryption: XChaCha20-Poly1305
- Key Derivation: Argon2id (for passwords)
- Forward Secrecy: Double Ratchet protocol
All cryptography uses well-known Rust libraries. Core signing and key-exchange libraries (ed25519-dalek, x25519-dalek) were professionally audited by Trail of Bits. Encryption and KDF libraries (chacha20poly1305, argon2) implement IETF-standardized algorithms.
Is Vauchi open source?
Yes! The complete source code is available at: https://gitlab.com/vauchi
You can:
- Inspect how your data is handled
- Verify our security claims
- Contribute improvements
- Run your own relay server
Troubleshooting
My contacts don't see my updates
- Check your internet connection
- Ensure sync is working (Settings > check last sync time)
- Verify the field is visible to that contact
- Ask them to manually refresh
The QR scanner doesn't work
- Check camera permissions
- Ensure adequate lighting
- Clean your camera lens
- Try adjusting distance to the QR code
- Restart the app
Sync seems stuck
- Check internet connectivity
- Try manual sync (pull to refresh or Settings > Sync)
- Check if the relay server is reachable
- Restart the app
Still Have Questions?
- GitLab Issues: gitlab.com/vauchi/vauchi/-/issues
- Email: support@vauchi.app
Known Issues
This page lists known issues in the current release. Check back after updating to see if your issue has been resolved.
Exchange
- BLE exchange may fail on older Android devices — Bluetooth Low Energy exchange requires Android 12+ with BLE 5.0 support. On older devices, use QR exchange instead.
- Audio proximity verification requires quiet environment — The ultrasonic proximity check can fail in noisy environments. This does not affect the security of the exchange, only the automatic proximity confirmation.
Sync
- iOS background sync not yet available — On iOS, sync only runs when the app is in the foreground. Open the app periodically to receive contact updates. Android background sync works automatically.
Desktop
- Linux Qt: some screens not yet implemented — A few secondary screens (backup scheduling, some settings panels) are still being wired on the Qt frontend.
- Windows: device link dialog not yet available — Device linking on Windows works via QR code but lacks the confirmation dialog.
Reporting Issues
Found something not listed here?
- GitLab Issues: Report a bug
- Email: support@vauchi.app
- Security issues: security@vauchi.app (see our security policy)
!!! warning "Privacy reminder" Never include QR codes, key material, or contact card content in bug reports — these contain cryptographic data.
Contact Exchange
Exchange contact cards securely by scanning QR codes in person.
How It Works
Vauchi uses in-person exchange to establish contact relationships. Both parties must be physically present to complete an exchange.
┌─────┐ ┌─────────┐
│ You │ │ Contact │
└──┬──┘ └────┬────┘
│ │
│ Show QR code │
│───────────────────────▶
│ │
│ Scan QR code │
◀───────────────────────│
│ │
│┌────────────────────┐ │
││ Proximity verified │ │
│└────────────────────┘ │
│ │
│ Scan their QR code │
│───────────────────────▶
│ │
│┌────────────────────┐ │
││ Exchange complete! │ │
│└────────────────────┘ │
│ │
┌──────────────────────────────┐
│ Both have each other's cards │
└──────────────────────────────┘
│ │
┌──┴──┐ ┌────┴────┐
│ You │ │ Contact │
└─────┘ └─────────┘
Why In-Person?
The in-person requirement is a privacy and security feature:
| Threat | How In-Person Prevents It |
|---|---|
| Spam | Can't be added by strangers |
| Impersonation | You verify identity yourself |
| Man-in-the-middle | Direct device communication |
| Screenshot attacks | Proximity verification |
Exchange Methods
QR Code (Primary)
The main method for exchanging contacts:
- Open the Exchange tab
- Show your QR code
- Have the other person scan it
- Scan their QR code
- Exchange complete
QR codes expire after 5 minutes for security.
Proximity Verification
On iOS, Vauchi verifies physical proximity using ultrasonic audio:
- Both phones emit and listen for an audio handshake (18-20 kHz)
- Range: approximately 3 meters
- If verification fails, exchange falls back to manual confirmation
- This prevents screenshot attacks
Android proximity verification is planned.
Troubleshooting Proximity (iOS)
If proximity verification fails:
- Ensure both phones have working speakers/microphones
- Move closer together (within 2-3 meters)
- Reduce background noise
- Disable any audio-blocking apps
- Try again — or confirm manually when prompted
On desktop and CLI/TUI, proximity verification is not available — manual confirmation is required instead.
After Exchange
Once exchange completes:
- The new contact appears in your Contacts list
- You can see their contact card (fields they've shared)
- They can see your contact card (fields you've shared)
- Future updates sync automatically
Security Properties
| Property | Mechanism |
|---|---|
| Proximity required | Ultrasonic audio handshake (iOS); manual confirmation (other platforms) |
| No man-in-the-middle | X3DH key agreement with identity keys |
| Forward secrecy | Ephemeral keys discarded after exchange |
| Replay prevention | One-time token, 5-minute expiry |
| Card authenticity | Ed25519 signature on contact card |
Related
- How to Exchange Contacts — Step-by-step guide
- Privacy Controls — Control what they see
- Encryption — How exchange data is protected
Auto Updates
Your contacts always have your latest information.
How It Works
When you update your contact card, everyone who has your card automatically sees the change. No need to send them the new info — it just appears.
┌─────┐ ┌───────┐ ┌───────────┐ ┌───────────┐
│ You │ │ Relay │ │ Contact 1 │ │ Contact 2 │
└──┬──┘ └───┬───┘ └─────┬─────┘ └─────┬─────┘
│ │ │ │
├───┐ │ │ │
│ │ Change phone number │ │ │
◀───┘ │ │ │
│ │ │ │
│ Send encrypted update │ │ │
│──────────────────────────▶ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Store for offline contacts │
│ ◀───┘ │ │
│ │ │ │
│ │ Deliver when online │ │
│ │────────────────────────▶ │
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Decrypt, update your card
│ │ ◀───┘ │
│ │ │ │
│ │ │ ┌─────────┐
│ │ │ │ Offline │
│ │ │ └─────────┘
│ │ │ │
│ │ Come online │
│ ◀────────────────────────────────────────│
│ │ │ │
│ │ Deliver pending update │
│ │────────────────────────────────────────▶
│ │ │ │
│ │ │ ├───┐
│ │ │ │ │ Decrypt, update your card
│ │ │ ◀───┘
│ │ │ │
│ │ ┌──────────────────────────┐
│ │ │ Both see your new number │
│ │ └──────────────────────────┘
│ │ │ │
┌──┴──┐ ┌───┴───┐ ┌─────┴─────┐ ┌─────┴─────┐
│ You │ │ Relay │ │ Contact 1 │ │ Contact 2 │
└─────┘ └───────┘ └───────────┘ └───────────┘
What Updates
When you change your contact card:
| Action | What Happens |
|---|---|
| Add a field | Visible contacts get notified |
| Edit a field | Contacts see the new value |
| Remove a field | Contacts see it disappear |
| Change visibility | Appears/disappears per contact |
Update Timing
When Online
- Updates deliver within seconds
- Contacts see changes when they open the app
- Real-time sync when both are active
When Offline
- Updates queue on the relay server
- Delivered when the contact comes online
- Messages kept for up to 120 days
Manual Refresh
Contacts can always:
- Pull to refresh their contact list
- Go to Settings > Sync Now
Privacy of Updates
Updates are end-to-end encrypted:
- The relay server cannot read update content
- Each contact receives updates encrypted with their unique key
- Different contacts may see different fields (per visibility settings)
What the Relay Sees
| Sees | Doesn't See |
|---|---|
| Encrypted blob | Field names |
| Recipient ID | Field values |
| Timestamp | Who you are |
| Message size (padded) | What changed |
Visibility and Updates
Updates respect your visibility settings:
- If you hide a field from someone, they don't receive updates for it
- If you show a field to someone, they start receiving updates
- Changes are per-contact, not global
Example
You change your phone number:
| Contact | Visibility | What They See |
|---|---|---|
| Family | Phone visible | New number |
| Work | Phone hidden | Nothing |
| Friend | Phone visible | New number |
Forward Secrecy
Each update uses a unique encryption key:
- Keys are derived via Double Ratchet
- Even if one key is compromised, other updates stay secure
- Past messages can't be decrypted with current keys
Troubleshooting
Contact Doesn't See My Update
- Check visibility — Is the field visible to them?
- Check your connection — Are you online?
- Wait a moment — Updates may take a few seconds
- Ask them to refresh — Pull to refresh or manual sync
Updates Seem Slow
- Check both connections — You and the contact need internet
- Check relay status — Rare server issues may delay delivery
- Try manual sync — Settings > Sync Now
Update Stuck
If an update seems stuck:
- Close and reopen the app
- Check internet connectivity
- Try editing and saving the field again
Related
- Privacy Controls — Control who sees what
- Multi-Device Sync — Updates across your devices
- Encryption — How updates are protected
Privacy Controls
Control exactly what each contact can see on your card.
How It Works
Every field on your contact card can be shown to or hidden from each contact. This gives you fine-grained control over your personal information.
Your Contact Card
┌────────────────────────────────────────────┐
│ Name: Alice Smith │
│ │
│ ┌─────────────────┬─────────┬─────────┐ │
│ │ Field │ Family │ Work │ │
│ ├─────────────────┼─────────┼─────────┤ │
│ │ Personal Email │ ✓ │ ✗ │ │
│ │ Work Email │ ✓ │ ✓ │ │
│ │ Personal Phone │ ✓ │ ✗ │ │
│ │ Work Phone │ ✓ │ ✓ │ │
│ │ Home Address │ ✓ │ ✗ │ │
│ └─────────────────┴─────────┴─────────┘ │
│ │
│ Family sees 5 fields, Work sees 2 fields │
└────────────────────────────────────────────┘
Per-Contact Visibility
You control what each individual contact can see.
To Change What Someone Sees
- Go to Contacts
- Select a contact
- Scroll to "What They Can See"
- Toggle fields on/off
Visibility Options
For each field, you can set:
- Visible — The contact can see this field
- Hidden — The contact cannot see this field
Default Visibility
New fields are visible to everyone by default. You can change this immediately after adding or later.
Labels
Labels help you manage visibility for groups of contacts instead of individuals.
How Labels Work
- Create labels like "Family", "Work", "Friends"
- Assign contacts to labels
- Control visibility per label
- Contacts in multiple labels see the union of visible fields
Example Labels
| Label | What They See |
|---|---|
| Family | Everything |
| Work | Work email, work phone |
| Friends | Personal email, personal phone |
| Acquaintances | Just name |
Bulk Changes
On the home screen, tap the visibility button next to any field to:
- Show to all — Make visible to all contacts
- Hide from all — Hide from all contacts
- Customize — Toggle individual contacts
Privacy Notes
- They don't see changes in real-time — Updates sync when they open the app
- No notifications — Contacts aren't notified when you hide fields
- Looks like removal — Hidden fields appear as if you removed them
- History isn't shared — They only see your current card, not past versions
Common Scenarios
Sharing Business Info
- Create a "Business" label
- Assign professional contacts
- Show: Work email, work phone, LinkedIn
- Hide: Personal phone, home address
Close Friends Only
- Create a "Close Friends" label
- Assign trusted contacts
- Show: Everything including personal details
- Everyone else sees less
Temporary Sharing
- Share field with a contact
- Complete your task
- Hide the field again
- They lose access immediately
Related
- How to Manage Visibility — Step-by-step guide
- Contact Exchange — How contacts are added
- Encryption — How visibility is enforced cryptographically
Encryption
How Vauchi protects your data.
Overview
Everything in Vauchi is end-to-end encrypted. Only you and your contacts can read your data — not us, not the relay server, not anyone else.
What's Encrypted
| Data | Encrypted? | Who Can Read |
|---|---|---|
| Your contact card | Yes | You + your contacts |
| Messages between devices | Yes | Your devices only |
| Backup | Yes | You only (with password) |
| Data at rest (on device) | Yes | You only |
| Data in transit | Yes | You + recipient only |
How It Works
Your Identity
When you create your identity, Vauchi generates:
- A master seed (256 random bits) — the root of all your keys
- A signing key (Ed25519) — proves messages are from you
- An exchange key (X25519) — establishes shared secrets with contacts
These keys never leave your device unencrypted.
Exchanging Contacts
When you exchange with someone:
- You scan their QR code (contains their public key)
- Both devices perform X3DH key agreement
- A shared secret is established that only you two know
- All future communication is encrypted with this secret
┌─────────────────────────┐ ┌────────────┐
│ │ │ │
│ Your Keys │ │ Their Keys │
│ │ │ │
└────────────┬────────────┘ └──────┬─────┘
│ │
│ │
├─────────────────────────┘
│
▼
┌─────────────────────────┐
│ │
│ X3DH Key Agreement │
│ │
└────────────┬────────────┘
│
│
│
│
▼
┌─────────────────────────┐
│ │
│ │
│ Unique encryption key │
│ (known only to you two) │
│ │
└─────────────────────────┘
Updates Between Contacts
When you update your card:
- The update is encrypted with the shared key for each contact
- Different contacts may receive different updates (per visibility)
- Each message uses a unique key (forward secrecy)
- The relay only sees encrypted blobs
Forward Secrecy
Vauchi uses the Double Ratchet protocol (same as Signal):
- Each message uses a unique encryption key
- Keys are derived, used once, then deleted
- Even if one key is compromised, other messages stay secure
- Past messages can't be decrypted with current keys
Encryption Algorithms
| Purpose | Algorithm | Notes |
|---|---|---|
| Signing | Ed25519 | Identity and authenticity |
| Key exchange | X25519 | Shared secrets |
| Symmetric encryption | XChaCha20-Poly1305 | All data |
| Key derivation | HKDF-SHA256 | Derives keys from seeds |
| Password KDF | Argon2id | Protects backups |
All cryptography uses well-known Rust libraries.
Core signing and key-exchange libraries
(ed25519-dalek, x25519-dalek) were
professionally audited by Trail of Bits.
Encryption and KDF libraries
(chacha20poly1305, argon2) implement
IETF-standardized algorithms.
What the Relay Server Sees
The relay server routes messages but cannot read them:
| Relay Sees | Relay Cannot See |
|---|---|
| Encrypted blobs | Message content |
| Recipient ID | Your identity |
| Timestamps | What you changed |
| Message size (padded) | Who you are |
Messages are padded to standardized bucket sizes (256 B, 512 B, 1 KB, 4 KB) to prevent size-based analysis.
Device Security
Your data is protected on your device:
| Platform | Key Storage | Protection |
|---|---|---|
| iOS | Keychain | OS-protected |
| Android | KeyStore | OS-protected |
| macOS | Keychain | OS-protected |
| Windows | Credential Manager | OS-protected |
| Linux | Secret Service | If available |
Backup Security
Backups are encrypted with your password:
- Key derivation: Argon2id (memory-hard, resistant to brute force)
- Encryption: XChaCha20-Poly1305
- Result: Without your password, the backup is useless
We recommend passphrases (4+ random words) for memorable yet secure passwords.
Security Properties
| Property | How Vauchi Achieves It |
|---|---|
| Confidentiality | XChaCha20-Poly1305 |
| Integrity | AEAD authentication tags |
| Authenticity | Ed25519 signatures |
| Forward secrecy | Double Ratchet, one-time keys |
| Break-in recovery | DH ratchet, ephemeral keys |
| Replay prevention | Per-message nonces |
| Traffic analysis | Message padding |
Open Source
All Vauchi code is open source:
- Inspect the encryption implementation yourself
- Verify our security claims
- Report vulnerabilities responsibly
Source: https://gitlab.com/vauchi
Limitations
What encryption doesn't protect:
- Metadata you share: Your name, fields you make visible
- Physical access: Someone with your unlocked device
- Screenshots: If a contact screenshots your card
- Deleted data: Until secure delete completes
Related
- Security Overview — Broader security information
- Cryptography Reference — Technical details
- Privacy Controls — Control what contacts see
Backup & Recovery
Protect your identity and recover access if something goes wrong.
Overview
Vauchi offers two ways to recover your identity:
| Method | When to Use | Requires |
|---|---|---|
| Encrypted Backup | Planned recovery | Backup code + password |
| Social Recovery | Lost all devices | 3+ contacts to vouch |
Encrypted Backup
Creating a Backup
- Go to Settings > Backup
- Tap Export Backup
- Enter a strong password (must pass strength check)
- Confirm the password
- Copy or save the backup code
- Store your backup code securely (password manager, printed copy)
- Remember your backup password — it cannot be recovered
- The backup code + password = your entire identity
What's Included
| Data | Included? |
|---|---|
| Your identity (keys) | Yes |
| Your display name | Yes |
| Device information | Yes |
| Contacts | No* |
*Contact relationships are re-established through the relay when you restore.
Restoring from Backup
- Install Vauchi on a new device
- Choose Restore from Backup
- Paste your backup code
- Enter your backup password
- Your identity is restored
After restoration:
- Your identity is fully restored
- Contacts sync automatically via relay
- You can link additional devices
Backup Security
- Encryption: XChaCha20-Poly1305
- Key derivation: Argon2id (resistant to brute force)
- Without the password: Backup is useless
We recommend passphrases (4+ random words) for memorable yet secure passwords.
Social Recovery
If you lose access to all devices AND don't have a backup, social recovery lets trusted contacts help migrate your contacts to a new identity.
How It Works
┌──────────────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────┐
│ You (New Device) │ │ Contact 1 │ │ Contact 2 │ │ Contact 3 │ │ Relay │
└─────────┬────────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └───┬───┘
│ │ │ │ │
├───┐ │ │ │ │
│ │ Create recovery claim │ │ │ │
◀───┘ │ │ │ │
│ │ │ │ │
│ Meet in person, share claim │ │ │ │
│────────────────────────────────▶ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Verify it's really you │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ Create voucher │ │ │ │
◀────────────────────────────────│ │ │ │
│ │ │ │ │
│ Meet in person, share claim │ │ │
│──────────────────────────────────────────────────▶ │ │
│ │ │ │ │
│ Create voucher │ │ │ │
◀──────────────────────────────────────────────────│ │ │
│ │ │ │ │
│ Meet in person, share claim │ │ │
│──────────────────────────────────────────────────────────────────▶ │
│ │ │ │ │
│ Create voucher │ │ │
◀──────────────────────────────────────────────────────────────────│ │
│ │ │ │ │
│ Submit recovery proof (3 vouchers) │ │
│────────────────────────────────────────────────────────────────────────────────▶
│ │ │ │ │
│ │ │ │ ├───┐
│ │ │ │ │ │ Verify vouchers
│ │ │ │ ◀───┘
│ │ │ │ │
┌────────────────────────────────────┐ │ │ │ │
│ Contacts migrated to new identity! │ │ │ │ │
└────────────────────────────────────┘ │ │ │ │
│ │ │ │ │
┌─────────┴────────┐ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ ┌───┴───┐
│ You (New Device) │ │ Contact 1 │ │ Contact 2 │ │ Contact 3 │ │ Relay │
└──────────────────┘ └───────────┘ └───────────┘ └───────────┘ └───────┘
Starting Recovery
- Install Vauchi on a new device
- Create a new identity
- Go to Settings > Recovery
- Tap Recover Old Identity
- Enter your old public ID
- A recovery claim is generated
Getting Vouchers
For each voucher:
- Meet the contact in person
- Share your recovery claim with them
- They verify it's really you (visual recognition)
- They create a voucher in their app
- They share the voucher with you
Requirements
- You need vouchers from 3 or more contacts
- Each contact must have previously exchanged with your old identity
- This proves your social network recognizes the recovery request
Completing Recovery
Once you have enough vouchers:
- Import all vouchers into your app
- Vauchi submits the recovery proof
- Other contacts verify via mutual connections
- Your identity transitions to the new device
Helping Others Recover
If a contact asks you to vouch for their recovery:
- Go to Settings > Recovery
- Tap Help Someone Recover
- Paste their recovery claim
- Verify their identity (call them, meet in person)
- Create a voucher
- Share the voucher with them
Recovery Best Practices
Before You Need It
- Create a backup as soon as you set up
- Store backup securely (password manager, safe)
- Use a memorable passphrase for the password
- Have 5+ contacts in case some are unavailable
When You Need It
- Try backup restore first (faster, simpler)
- Use social recovery only if backup unavailable
- Meet contacts in person for vouching
- Don't rush — verify everything carefully
Troubleshooting
Forgot Backup Password
Unfortunately, backup passwords cannot be recovered. The encryption is designed so only you can decrypt your backup. Options:
- Use social recovery if available
- Create a new identity and re-exchange with contacts
- Check if you have another linked device still accessible
Not Enough Vouchers
If you can't reach 3 contacts:
- Check if old contacts are still available
- Wait if contacts are temporarily unavailable
- Consider creating a new identity as last resort
Voucher Rejected
Vouchers may be rejected if:
- The voucher is for a different identity
- The voucher is corrupted
- The voucher has expired (90 days)
Ask the contact to create a new voucher.
Related
- How to Recover Your Account — Step-by-step guide
- Multi-Device Sync — Another way to access your identity
- Encryption — How backup encryption works
Multi-Device Sync
Use Vauchi on multiple devices with the same identity.
How It Works
All your devices share the same identity and stay in sync. Changes made on one device appear on all others.
┌─────────────────┐
│ Your Identity │
│ │
│ │
│ ┌─────────────┐ │
│ │ │ │
│ │ Master Seed ├─┼───┬────┐
│ │ │ │ │ │
│ └──────┬──────┘ │ └────┼──────────────┐
│ │ │ │ │
└────────┼────────┘ │ │
│ │ │
│ │ │
│ │ │
┌────────┼─────────────────┼──────────────┼──────┐
│ │ Devices │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌────────┐ ┌─────────┐ │
│ │ │ │ │ │ │ │
│ │ Phone │ │ Tablet │ │ Desktop │ │
│ │ │ │ │ │ │ │
│ └─────────────┘ └────▲───┘ └────▲────┘ │
│ │ │ │
└──────────────────────────┼──────────────┼──────┘
│ │
│ │
│ │
┌─────────────┐ │ │
│ │ │ │
│ R │◄─────────┴──────────────┘
│ │
└─────────────┘
Linking a New Device
Prerequisites
- Your existing device with Vauchi set up
- The new device with Vauchi installed
- Both devices online
Steps
- On your existing device, go to Settings > Devices
- Tap Link New Device
- A QR code appears (valid for 5 minutes)
- On your new device, install Vauchi
- Choose Join Existing Identity
- Scan the QR code (or paste the data string on desktop/CLI)
- Verify the confirmation code matches on both devices
- Confirm to complete linking
Both devices now share your identity and sync automatically.
Confirmation Code
When linking, both devices display a 6-digit code (e.g., 123-456). This code is derived cryptographically from the shared link data — only the two devices involved can compute it. If the codes match, you know the link is authentic.
Device Limits
- Maximum: 10 devices per identity
- Minimum: 1 device (your primary)
If you need to add an 11th device, revoke an existing one first.
Platform Support
| Platform | Link (Generate) | Join (Scan/Paste) | Manage Devices |
|---|---|---|---|
| iOS | Planned | Planned | Planned |
| Android | Planned | Planned | Planned |
| Desktop | Yes | Yes (paste) | Yes |
| TUI | Yes | Planned | Yes |
| CLI | Yes | Yes | Yes |
Managing Devices
Viewing Linked Devices
- Go to Settings > Devices
- See all linked devices
- Your current device is marked
Each device shows:
- Device name
- Platform (iOS, Android, Desktop, CLI, TUI)
- Status (active, revoked)
Revoking a Device
If a device is lost, stolen, or no longer needed:
- Go to Settings > Devices on another device
- Find the device to revoke
- Tap Revoke
- Confirm the action
A revoked device:
- Loses access to your identity immediately
- Cannot send or receive updates
- Cannot be re-linked without starting fresh
How Sync Works
- Changes sync automatically when online
- Sync uses end-to-end encryption
- The relay server cannot read your data
- Offline changes sync when connectivity returns
What Syncs
| Data | Syncs? |
|---|---|
| Your contact card | Yes |
| Your contacts | Yes |
| Visibility settings | Yes |
| App preferences | Yes |
| Device-specific settings | No |
Sync Frequency
- Real-time: When both devices are online
- On app open: Pulls any pending changes
- Manual: Pull to refresh or Settings > Sync Now
Migration
Moving to a New Phone
Option 1: Device Linking (Recommended)
- On old phone: Link the new phone as a device
- Wait for sync to complete
- On old phone: Revoke the old phone (optional)
Option 2: Backup & Restore
- On old phone: Create an encrypted backup
- On new phone: Restore from backup
Device linking is preferred because it preserves device-specific keys and ensures a clean handoff.
Troubleshooting
Sync Not Working
- Check internet connectivity on both devices
- Ensure both devices have the app open
- Try manual sync (Settings > Sync Now)
- Check that the device hasn't been revoked
Device Not Appearing
- Wait a few minutes for sync
- Restart the app on both devices
- Check the link code hasn't expired (5 minutes)
- Try generating a new link code
Security
- Each device has its own keys derived from your master seed
- Revoking a device invalidates its keys immediately
- The relay server never sees plaintext data
- Device-to-device communication is end-to-end encrypted
- Confirmation codes prevent man-in-the-middle attacks during linking
Related
- How to Set Up Multi-Device — Step-by-step guide
- Backup & Recovery — Alternative recovery method
- Encryption — How multi-device encryption works
How to Exchange Contacts
Step-by-step guide for exchanging contact cards with other Vauchi users.
Prerequisites
- Both you and the other person have Vauchi installed
- You're physically together (proximity verification required)
- Both devices have working cameras
QR Code Exchange
Both people show and scan each other's QR codes. This ensures fresh encryption keys are used for every exchange (forward secrecy).
Step 1: Open Exchange
- Open Vauchi
- Tap the Exchange tab at the bottom
Step 2: Show Your QR Code
- Tap Show My QR Code
- A QR code appears on your screen
- Show it to the other person
Step 3: They Scan Your Code
- The other person points their camera at your QR code
- Their device confirms a successful scan
Step 4: Scan Their Code
- Tap Scan QR Code
- Point your camera at their QR code
- Wait for the scan to complete
Step 5: Confirm
Both devices show "Exchange Successful"
You now have each other's contact cards.
Troubleshooting
QR Code Won't Scan
- Lighting: Make sure the QR code is well-lit
- Stability: Hold both devices steady
- Distance: Try moving closer or farther
- Clean lens: Wipe your camera lens
- Refresh: Generate a new QR code (they expire after 5 minutes)
Exchange Keeps Failing
- Check internet connectivity on both devices
- Ensure the QR code hasn't expired (5-minute limit)
- Restart the app on both devices
- Try a fresh QR code
After Exchange
Once exchange completes:
- They appear in your Contacts list
- You can see their card (fields they've shared)
- They can see your card (fields you've shared)
- Future changes sync automatically
Next Steps
Security Notes
- QR codes expire after 5 minutes (replay protection)
- Both parties must scan each other's QR codes (mutual verification)
- Each exchange uses fresh ephemeral keys (forward secrecy)
- Exchange uses encrypted key agreement
- The relay never sees unencrypted data
For more on security, see Encryption.
How to Manage Visibility
Step-by-step guide for controlling what each contact can see.
Overview
Visibility lets you control who sees what on your contact card. Show your work email to colleagues, your personal phone to friends, and hide your home address from everyone else.
Change What One Contact Sees
Step 1: Open Contact
- Go to Contacts
- Tap on the contact you want to modify
Step 2: Find Visibility Settings
- Scroll down to "What They Can See"
- You'll see a list of all your fields
Step 3: Toggle Fields
- Enabled (green): They can see this field
- Disabled (gray): They cannot see this field
Tap any field to toggle it.
Step 4: Confirm
Changes apply immediately. The contact will see the update next time they sync.
Show/Hide a Field for Everyone
Step 1: Go to Your Card
- Go to Home
- Find the field you want to change
Step 2: Open Visibility Menu
- Tap the visibility icon (eye) next to the field
- A menu appears
Step 3: Choose Option
- Show to all: Makes the field visible to all contacts
- Hide from all: Hides the field from all contacts
- Customize: Opens per-contact toggles
Using Labels
Labels help you manage visibility for groups instead of individuals.
Creating a Label
- Go to Settings > Labels
- Tap Add Label
- Enter a name (e.g., "Work", "Family", "Friends")
- Tap Create
Assigning Contacts to Labels
- Open a contact
- Scroll to Labels
- Tap to assign/unassign labels
Setting Visibility by Label
- Go to Home
- Tap the visibility icon next to a field
- Tap Customize
- Switch to the Labels tab
- Toggle labels on/off
Example: Enable "Family" and "Friends", disable "Work" for your personal phone.
Common Configurations
Business Card Mode
Share only professional information:
| Field | Visibility |
|---|---|
| Work Email | All |
| Work Phone | All |
| Personal Email | None |
| Personal Phone | None |
| Home Address | None |
Close Friends
Share everything with trusted contacts:
- Create a "Close Friends" label
- Assign trusted contacts
- Show all fields to "Close Friends"
- Restrict fields for everyone else
Temporary Sharing
Share a field temporarily:
- Show the field to a specific contact
- Complete whatever you needed
- Hide the field again
They lose access immediately when you hide it.
Checking What Someone Sees
Step 1: Open Contact
- Go to Contacts
- Tap on the contact
Step 2: Review Visible Fields
Look at "What They Can See":
- Enabled fields = they see these
- Disabled fields = they don't see these
Summary View
At the bottom of the contact, you'll see:
"Alice can see 3 of your 7 fields"
Default Visibility
For New Fields
When you add a new field, it's visible to everyone by default.
To change this:
- Add the field
- Immediately tap the visibility icon
- Adjust as needed
For New Contacts
When you exchange with someone new, they see all fields that are currently visible to "everyone".
Troubleshooting
Contact Still Sees Hidden Field
- Changes sync when they open the app
- Ask them to refresh their contacts
- Wait a few minutes for sync
Can't Find Visibility Options
- Make sure you're on the contact's detail page
- Scroll down — visibility is below their card info
- If missing, update the app
Label Changes Not Applying
- Make sure contacts are assigned to the label
- Check the label visibility settings
- Try removing and re-adding the label
Tips
Be Intentional
- Review visibility when adding new fields
- Periodically audit what each contact sees
- Use labels to stay organized
Think in Categories
Group contacts by relationship type:
- Work: Professional info only
- Family: Everything
- Acquaintances: Name and email only
Start Restrictive
It's easier to show more later than to hide after sharing.
Privacy Notes
- Hidden fields disappear from their view
- They aren't notified when you hide fields
- They can't see your visibility settings
- Hiding is per-contact — it doesn't delete the field
For more on privacy, see Privacy Controls.
How to Recover Your Account
Step-by-step guide for restoring access to your Vauchi identity.
Choose Your Recovery Method
| Situation | Method | Time Required |
|---|---|---|
| Have backup code + password | Backup Restore | 5 minutes |
| Have another linked device | Device Link | 5 minutes |
| Lost everything | Social Recovery | Hours to days |
Backup Restore
If you have your encrypted backup code and password:
Step 1: Start Fresh
- Install Vauchi on your new device
- On the welcome screen, tap Restore from Backup
Step 2: Enter Backup
- Paste your backup code (the long string of characters)
- Tap Next
Step 3: Enter Password
- Enter your backup password
- Tap Restore
Step 4: Wait for Sync
- Vauchi restores your identity
- Your contacts sync automatically via the relay
- Within minutes, you should see your contacts
Device Link
If you have another device still logged in:
Step 1: On Your Working Device
- Open Vauchi
- Go to Settings > Devices
- Tap Link New Device
- A QR code appears
Step 2: On Your New Device
- Install Vauchi
- Tap Join Existing Identity
- Scan the QR code
Step 3: Revoke Lost Device (Optional)
If your old device was lost or stolen:
- On your working device, go to Settings > Devices
- Find the lost device
- Tap Revoke
Social Recovery
If you've lost all devices and don't have a backup:
Overview
Social recovery uses your real-world relationships to verify your identity. You need vouchers from 3 or more contacts who have previously exchanged with you.
Step 1: Create New Identity
- Install Vauchi on a new device
- Create a new identity (fresh start)
- This gives you a new device to work from
Step 2: Start Recovery
- Go to Settings > Recovery
- Tap Recover Old Identity
- Enter your old public ID (if you know it)
- If you don't know it, ask a contact — they can find it in your contact details
- A recovery claim is generated (valid for 48 hours)
Step 3: Collect Vouchers
For each voucher, you need to meet a contact in person:
- Meet in person (physical presence required)
- Show them your recovery claim
- They open Settings > Recovery > Help Someone Recover
- They paste your claim
- They verify it's really you
- They tap Create Voucher
- They share the voucher with you
Repeat until you have 3 or more vouchers.
Step 4: Submit Recovery
- Go to Settings > Recovery
- Import each voucher you received
- Once you have 3+, tap Complete Recovery
- Vauchi submits your recovery proof
Step 5: Wait for Verification
- The relay verifies your vouchers
- Other contacts may verify via mutual connections
- Once verified, your identity transitions
Step 6: Re-Exchange (If Needed)
Some contacts may need to re-verify you:
- They'll see a notification about your recovery
- Meet them in person to confirm
- Your relationship continues
Helping Someone Else Recover
If a contact asks you to vouch for their recovery:
Step 1: Verify Their Identity
Before creating a voucher:
- Meet in person if possible
- Confirm they are who they claim to be
- Be suspicious of unusual requests
Step 2: Create Voucher
- Go to Settings > Recovery
- Tap Help Someone Recover
- Paste their recovery claim
- Tap Create Voucher
Step 3: Share Voucher
- Copy the voucher
- Send it to them (AirDrop, messaging, etc.)
Troubleshooting
Backup Restore: "Invalid Password"
- Check for typos
- Passwords are case-sensitive
- Try any variations you might have used
If you truly can't remember the password, you'll need to use social recovery.
Backup Restore: "Invalid Backup Code"
- Make sure you copied the entire code
- Check for extra spaces or line breaks
- Try copying again from the original source
Social Recovery: "Not Enough Vouchers"
- You need at least 3 vouchers
- Contact more people who have exchanged with your old identity
- Vouchers must be from different contacts
Social Recovery: "Voucher Rejected"
- The voucher may be for a different identity
- The voucher may have expired (90 days)
- Ask the contact to create a fresh voucher
Can't Remember Old Public ID
- Ask any contact who had your old card
- They can find your ID in your contact details
- Look through old screenshots or notes
Prevention Tips
To avoid needing recovery:
- Create a backup as soon as you set up
- Store backup securely (password manager, safe)
- Link multiple devices (phone + tablet/desktop)
- Remember your password (use a passphrase)
- Have 5+ contacts who could vouch for you
Security Notes
- Social recovery requires in-person verification
- 3 vouchers prevent single-point-of-failure attacks
- Vouchers expire after 90 days
- Recovery is logged for transparency
For more on security, see Backup & Recovery Feature.
How to Set Up Multi-Device
Step-by-step guide for using Vauchi on multiple devices.
Prerequisites
- Your existing device with Vauchi set up
- A new device with Vauchi installed (but not set up)
- Both devices have internet connectivity
Linking a New Device
Step 1: Generate Link Code
On your existing device:
Mobile (iOS/Android):
- Open Vauchi
- Go to Settings (gear icon)
- Tap Devices
- Tap Link New Device
- A QR code appears (valid for 5 minutes)
Desktop:
- Open Vauchi Desktop
- Go to Devices (from the sidebar)
- Click Link New Device
- A QR code and data string appear (valid for 5 minutes)
TUI:
- Open Vauchi TUI
- Press d to go to Devices
- Press l to generate a link
- A QR code and data string appear in an overlay
CLI:
vauchi device link
A QR code and data string are displayed in the terminal.
Step 2: Join on New Device
On your new device:
Mobile (iOS/Android):
- Open Vauchi
- On the welcome screen, tap Join Existing Identity
- Point your camera at the QR code from Step 1
- Verify the confirmation code matches on both devices
- Wait for the linking to complete
Desktop:
- Open Vauchi Desktop
- On the setup screen, click Join Existing Identity
- Paste the data string from the existing device
- Verify the confirmation code matches on both devices
- Click Confirm to complete linking
CLI:
vauchi device join <data-string>
Then on the existing device, pass the encrypted request data from the new device:
vauchi device complete <request-data>
Step 3: Confirm
Both devices should show:
- Your existing device: "Device linked successfully"
- Your new device: "Welcome back, [Your Name]"
Your new device is now synced with your identity.
Verifying Setup
After linking:
On New Device
- Go to Contacts — your contacts should appear
- Go to Home — your contact card should appear
- Go to Settings > Devices — both devices should be listed
On Existing Device
- Go to Settings > Devices
- You should see both devices listed
- Your new device shows its platform and last sync time
Syncing Data
Data syncs automatically:
-
Immediately: When both devices are online
-
On app open: When you open the app
-
Manual: Pull to refresh or Settings > Sync Now
What Syncs
| Data | Syncs? |
|---|---|
| Your contact card | Yes |
| Your contacts | Yes |
| Visibility settings | Yes |
| App preferences | Yes |
Managing Devices
Viewing All Devices
Mobile/Desktop: Go to Settings > Devices to see all linked devices. Your current device is marked.
TUI: Press d to open the Devices screen.
Navigate with j/k or arrow keys. Current
device is marked [this device].
CLI:
vauchi device list
Revoking a Device
If a device is lost, stolen, or no longer needed:
Mobile/Desktop:
- Go to Settings > Devices on another device
- Find the device to revoke
- Tap Revoke
- Confirm by tapping Revoke Device
TUI:
- Press d to open Devices
- Navigate to the device with j/k
- Press r to revoke
- Press y to confirm
CLI:
vauchi device revoke <device-id>
Troubleshooting
Link Code Expired
QR codes are valid for 5 minutes. If expired:
- On your existing device, generate a new link code
- Scan or paste the new code quickly
New Device Not Syncing
- Check internet on both devices
- Wait a few minutes for initial sync
- Pull to refresh on the new device
- Check Settings > Sync for last sync time
"Too Many Devices" Error
You can have up to 10 devices. To add another:
- Go to Settings > Devices
- Revoke a device you no longer use
- Try linking the new device again
Migrating to a New Phone
Option 1: Device Linking (Recommended)
- Keep your old phone accessible
- Follow the steps above to link your new phone
- Wait for sync to complete
- Optionally, revoke your old phone
This is the cleanest migration path.
Option 2: Backup & Restore
If you can't access your old phone:
- Restore from an encrypted backup
- See How to Recover Your Account
Security Notes
- Each device has its own derived keys
- Revoking a device invalidates its keys immediately
- The relay never sees plaintext data
- Link codes expire after 5 minutes
- A 6-digit confirmation code ensures you're linking the right devices
For more on security, see Multi-Device Feature.
About Vauchi
Privacy-focused updatable contact cards via in-person exchange.
What We're Building
Vauchi is a contact card that updates automatically. When you change your phone number, everyone who has your card sees the change — no need to notify them manually.
Unlike traditional contact apps:
- No sign-up required — Your device is your identity
- No phone number required — Exchange cards in person
- End-to-end encrypted — Only you and your contacts can read your data
- Open source — Verify every claim yourself
Why We Built It
Contact information goes stale. You change jobs, move cities, get a new number — and suddenly half your contacts have outdated info. The usual solution is to use a platform (social network, messaging app) as the source of truth, but that means trusting a company with your data.
We believe there's a better way: updatable contact cards that stay private.
Learn More
- Our Principles — The values that guide every decision
- Security — How we protect your data
- Community — How to participate
- Supporters — Those who help make Vauchi possible
Open Source
Vauchi is open source under GPL-3.0-or-later. You can:
- Inspect the code
- Verify security claims
- Contribute improvements
- Run your own relay server
GitLab: https://gitlab.com/vauchi GitHub Mirror: https://github.com/vauchi
Contact
- Issues: GitLab Issues
- Email: hello@vauchi.app
- Security: security@vauchi.app
Vauchi Principles
The single source of truth for core principles and philosophy.
All solutions must be validated against these principles before implementation.
Core Value Statement
Vauchi is built on five interlocking commitments:
1. Privacy is a right, not an option
All design starts with: "How would we build this if users were our only concern?"
- E2E encryption for all communications
- Oblivious privacy-preserving relay (sees only encrypted blobs)
- No tracking, analytics, or telemetry
- User owns and controls their data
2. Trust is earned in person
Human recognition is the security anchor, not passwords or platforms.
- QR exchange with physical proximity verification for full trust; opt-in remote discovery at reduced trust
- No accounts or registration (device IS the identity)
- Social vouching for recovery (people you've actually met)
- No trust-on-first-use for contact verification — you verify contacts in person. Relay server identity is pinned during contact exchange. No platform-mediated relationships
3. Quality comes from rigorous methodology
Confidence through discipline, not hope.
- Test-Driven Development (TDD) is mandatory
- Problem-first workflow with full traceability
- Threat modeling drives security decisions
- No hacks, no tech debt, no ignored tests
4. Simplicity serves the user
Vauchi respects your time and attention — it does one thing well and stays out of your way.
- No engagement tricks, no notifications designed to pull you back
- Clear, minimal interface — useful without a learning curve
- Features earn their place by solving real problems, not adding complexity
- The app is a tool, not a destination
5. Beauty adapts to the user
Simplicity and beauty go hand in hand — and beauty is personal.
- Design that feels good without demanding attention
- Theming and customisation let users make it their own
- Aesthetic choices serve clarity, never compete with it
- A beautiful tool is one that fits the person using it
Principle Categories
Privacy Principles
| Principle | Statement |
|---|---|
| User Ownership | Data stored locally, encrypted, user-controlled |
| Oblivious Relay | Relay sees only encrypted blobs, not content |
| No Harvesting | No analytics, telemetry, or tracking |
| No Sharing | Data never shared with third parties |
| Selective Visibility | Per-field, per-contact visibility control |
Security Principles
| Principle | Statement |
|---|---|
| Proximity Full Trust | QR + BLE/ultrasonic; remote restricted |
| Audited Crypto Only | RustCrypto audited crates; no custom crypto |
| Forward Secrecy | Double Ratchet; past messages safe if keys leak |
| Memory Safety | Rust enforced; no unsafe in crypto paths |
| Defense in Depth | Multiple layers: encryption, signing, verification |
Technical Principles
| Principle | Statement |
|---|---|
| TDD Mandatory | Tidy, Red, Green, Refactor. Test first. No exceptions |
| 90%+ Coverage | For vauchi-core; real crypto in tests (no mocking) |
| Rust Core | Memory safety, no GC, cross-platform compilation |
| Clean Deps | vauchi-core standalone; downstream uses git deps |
| Gherkin Trace | features/*.feature files drive test writing |
UX Principles
| Principle | Statement |
|---|---|
| Complexity Hidden | Users see "scan QR, contact added" |
| In-Person Trust | Human recognition is the security anchor |
| Local-First | Data on device; queues offline, syncs on connect |
| Portable Identity | No lock-in; restore from backup, switch devices |
| Cross-Platform | Same experience on iOS, Android, desktop |
Process Principles
| Principle | Statement |
|---|---|
| Problem-First | Every task starts as a problem |
| Artifacts Accumulate | Investigations and retrospectives attached |
| No Wasted Rejections | Archive rejected solutions with reasoning |
| Small Atomic Commits | After each green, after each refactor |
| Retrospective Required | Learn from every completed problem |
Using These Principles
For Solution Validation
When evaluating a proposed solution, check:
- Does it align with Core Principles? (Privacy, Trust, Quality, Simplicity, Beauty)
- Does it fit the Culture? (Process Principles)
- Is it compatible with Current Implementation? (Technical Principles)
- Does it support existing Features? (UX Principles)
If a solution conflicts with any principle, it must be rejected with documented reasoning.
For Decision Making
When facing a design decision:
- Start with the user's perspective
- Assume adversarial conditions (what could go wrong?)
- Choose the option that best upholds all five core values
- Document the decision and rationale
For New Contributors
Read these principles before contributing. They are non-negotiable. If you disagree with a principle, open a problem record to discuss changing it—don't ignore it.
Amending Principles
Principles can be amended, but only through the Problem Workflow:
- Create a problem record explaining why the principle should change
- Investigate impact across codebase and documentation
- Validate the change against remaining principles
- Implement with full retrospective
Principles are not immutable, but changes must be deliberate and documented.
Security
How Vauchi protects your data.
Security Model
Vauchi is designed with the assumption that everything outside your device is hostile:
- Relay server: Assumed compromised
- Network: Assumed monitored
- Other devices: Verified through in-person exchange
Despite these assumptions, your data stays private because of end-to-end encryption.
How We Protect You
End-to-End Encryption
All communication is encrypted so only you and your contacts can read it:
| Data | Encryption |
|---|---|
| Contact cards | XChaCha20-Poly1305 |
| Messages | XChaCha20-Poly1305 with Double Ratchet |
| Backups | XChaCha20-Poly1305 with Argon2id KDF |
| Local storage | XChaCha20-Poly1305 |
The relay server only sees encrypted blobs. It cannot:
- Read your contacts
- See your card fields
- Decrypt any messages
- Link your identity to your data
In-Person Verification
Contact exchange requires physical presence:
- QR codes contain cryptographic identity
- Proximity verification via ultrasonic audio
- No trust-on-first-use for contact verification — you verify contacts in person. Relay server identity is pinned during contact exchange.
This prevents spam, impersonation, and man-in-the-middle attacks.
Modern Cryptography
Vauchi uses battle-tested cryptographic libraries:
| Purpose | Algorithm | Library |
|---|---|---|
| Signing | Ed25519 | ed25519-dalek |
| Key exchange | X25519 | x25519-dalek |
| Symmetric encryption | XChaCha20-Poly1305 | chacha20poly1305 |
| Password KDF | Argon2id | argon2 |
| Key derivation | HKDF-SHA256 | hkdf |
All libraries are:
- Written in Rust (memory-safe)
- Well-known, widely used in production
Forward Secrecy
Each message uses a unique key derived via Double Ratchet:
- Keys are used once then deleted
- Even if one key is compromised, other messages stay safe
- Past messages can't be decrypted with current keys
Threat Model
| Threat | Mitigation |
|---|---|
| Server compromise | E2E encryption; server can't read data |
| Network surveillance | TLS + Noise NK + E2E; three layers |
| Man-in-the-middle | In-person verification of identity |
| Spam/harvesting | Proximity required; no remote adding |
| Device theft | OS-level key storage, optional biometrics |
| Lost device | Social recovery + encrypted backups |
| Traffic analysis | Padding to standardized bucket sizes |
| Replay attacks | One-time tokens, per-message nonces |
Metadata Visibility
The relay operator can observe communication patterns: which identities communicate, when messages are sent and received, and message frequency. The relay cannot read message content. Delivery jitter reduces timing correlation between senders and recipients. Running your own relay server eliminates third-party metadata exposure.
Best Practices
For Users
- Create a backup — Protect against device loss
- Use a strong backup password — A passphrase (4+ words) is recommended. Store it somewhere safe, separate from your devices
- Verify important contacts — Compare fingerprints in person
- Revoke lost devices immediately — Prevent unauthorized access
- Keep your device secure — Enable lock screen, update OS
- Only link devices you physically control — Each linked device has full access to your identity
For Privacy
- Review visibility settings — Control what each contact sees
- Limit field sharing — Only share what's needed
- Remove old contacts — They keep seeing updates otherwise
For Recovery
Set up social recovery to protect against total device loss:
- Choose diverse guardians — Spread across different social circles (e.g., one family member, one friend, one colleague)
- Don't rely on one group — If all guardians are family, a single household event could make recovery impossible
- Set threshold to at least 3 — Higher thresholds are more secure
- Update guardians when relationships change — Remove guardians you've lost touch with and add new ones
- Review periodically — Check your guardian list once a year
For Backups
- Use a strong passphrase — At least 4 random words or equivalent strength
- Store backups securely — On a USB drive, external storage, or a secure location separate from your devices
- Don't store on cloud services — Backup files are encrypted, but keeping them local is more private
- Create fresh backups — After adding new contacts or linking devices
Security Reporting
Found a security issue? Please report it responsibly:
Email: security@vauchi.app
We will:
- Acknowledge within 48 hours
- Investigate and fix verified issues
- Credit reporters (unless they prefer anonymity)
- Not pursue legal action against good-faith researchers
Open Source
All code is open source and available for inspection:
- GitLab: https://gitlab.com/vauchi
- GitHub Mirror: https://github.com/vauchi
We welcome security reviews and contributions.
Technical Details
For cryptographic implementation details, see:
- Encryption Feature — User-friendly explanation
- Cryptography Reference — Technical specification
Community
Join the Vauchi community.
Get Involved
Report Issues
Found a bug or have a feature request?
- GitLab Issues: https://gitlab.com/vauchi/vauchi/-/issues
Contribute Code
We welcome contributions! See our Contributing Guide for:
- Development setup
- Code guidelines
- Merge request process
Translations
Help translate Vauchi to your language:
- Locale files: https://gitlab.com/vauchi/locales
- Submit merge requests with new translations or fixes
Discussions
Have questions or ideas?
- GitLab Issues: https://gitlab.com/vauchi/vauchi/-/issues (use the
questionoridealabel)
Code of Conduct
Our Commitment
Vauchi is built on trust earned in person. We extend that same spirit to our community: treat others as you would someone you've just met face-to-face.
Expected Behavior
- Be respectful and considerate
- Give and accept constructive feedback graciously
- Focus on what's best for the project and community
- Assume good intent; ask for clarification before assuming malice
Unacceptable Behavior
- Harassment, insults, or personal attacks
- Trolling or inflammatory comments
- Publishing others' private information
- Conduct that would be inappropriate in a professional setting
Scope
This applies to all project spaces: issues, merge requests, discussions, and any public space where you represent Vauchi.
Enforcement
Instances of unacceptable behavior may be reported to: conduct@vauchi.app
Maintainers will review and respond to all complaints. Responses may include:
- Warning
- Temporary ban
- Permanent ban
Attribution
Adapted from the Contributor Covenant, version 2.1.
Contact
- General: hello@vauchi.app
- Security: security@vauchi.app
- Conduct: conduct@vauchi.app
Supporters
Thank you to everyone who supports Vauchi's mission to build privacy-respecting software.
Platinum Sponsors
Become our first Platinum sponsor — GitHub Sponsors
Gold Sponsors
Become our first Gold sponsor — GitHub Sponsors
Silver Sponsors
Become our first Silver sponsor — GitHub Sponsors
Bronze Sponsors
Become our first Bronze sponsor — GitHub Sponsors
Backers
Your name could be here — GitHub Sponsors
Supporters
Your name could be here — GitHub Sponsors
How to Support
- GitHub Sponsors: https://github.com/sponsors/vauchi
- Liberapay: https://liberapay.com/Vauchi/donate
Every contribution helps us build privacy-respecting software without compromising on principles.
Where Funds Go
| Category | Purpose |
|---|---|
| Hardware | Development machines, mobile test devices |
| Infrastructure | Relay server hosting, domain costs |
| Security | External security audits |
| Development | Full-time development toward v1.0 |
Thank you for believing in privacy-first software.
Privacy Policy
Last Updated: February 2026
Overview
Vauchi is a privacy-focused contact card exchange application. This privacy policy explains how we handle your data. The short version: your data stays on your devices, encrypted, and under your control.
Data Collection
What We Collect
On Your Device (Local Storage):
- Your identity (cryptographic keypair, display name)
- Your contact card (fields you choose to add: email, phone, etc.)
- Contacts you've exchanged with (their public cards)
- Visibility rules (which contacts can see which fields)
- Device registry (for multi-device sync)
On Our Relay Server:
- Temporary encrypted envelopes containing contact card updates (deleted after delivery or 120 days)
- Connection metadata (cryptographic identity hash, connection timestamps) for rate limiting — IP addresses are NOT stored or logged
- No envelope content is ever readable by the server
What We Don't Collect
- We do not collect analytics or telemetry
- We do not track your location
- We do not access your device contacts, photos, or other apps
- We do not use advertising identifiers
- We do not sell or share your data with third parties
Data Storage
Local-First Architecture
All your personal data is stored locally on your device:
- Encryption at Rest: Your data is encrypted using XChaCha20-Poly1305 with a key stored in your device's platform keychain (iOS Keychain / Android KeyStore)
- No Cloud Backup by Default: Your data is not automatically backed up to any cloud service
- You Control Exports: You can create encrypted backups manually, protected by a password you choose
Relay Server
The relay server delivers encrypted contact card updates between your devices and your contacts. It operates as a store-and-forward broker:
- Contact card updates are end-to-end encrypted before leaving your device
- The server cannot decrypt any envelope content
- Envelopes are deleted immediately after delivery or after 120 days if undelivered
- Server logs contain only connection metadata, not contact card content
- Rate limiting data (based on cryptographic identity hash, not IP address) is retained for up to 30 minutes of inactivity
Data Sharing
With Your Contacts
When you exchange contact cards with someone:
- You explicitly choose which fields they can see
- You can change visibility settings at any time
- Changes sync automatically to their device
With Third Parties
We do not share your data with any third parties. Period.
With Law Enforcement
If required by law, we can only provide:
- Connection metadata (timestamps only — IP addresses are not stored or logged)
- Encrypted envelopes (which we cannot decrypt)
We cannot provide your contact card content, contact list, IP addresses, or any decrypted data because we do not have access to it.
Data Security
Cryptographic Protections
- Identity Keys: Ed25519 signing keys, never leave your device
- Encryption: X25519 key exchange + XChaCha20-Poly1305 for all contact card updates
- Key Derivation: Argon2id for password-based encryption (backups)
- Forward Secrecy: Double Ratchet protocol ensures each contact card update uses a unique encryption key
Platform Security
- iOS: Keys stored in Keychain
- Android: Keys stored in KeyStore
- Desktop: Keys encrypted with OS-level secure storage
Certificate Pinning
Certificate pinning primitives are implemented to prevent man-in-the-middle attacks against the relay server connection. This feature is not yet active in client apps.
Your Rights
Access Your Data
All your data is stored locally on your device. You can view it directly in the app.
Export Your Data
You can export an encrypted backup of all your data at any time from Settings > Backup.
Delete Your Data
- Account Deletion: Use Settings > Delete Account to initiate deletion. A 7-day grace period allows you to cancel. After 7 days, the app sends a revocation signal to all your contacts (authenticated with your cryptographic identity), requests the relay server to purge all stored data for your account, and permanently deletes all local data (database, keys, and secure storage entries). Your contacts' apps will automatically delete your card upon receiving the revocation.
- Single Contact Removal: You can remove any contact, which deletes their data from your device
- Multi-Device: Account deletion is synchronized across all your linked devices. Initiating deletion on one device starts the grace period on all devices; cancellation from any device cancels on all devices.
Data Portability
Encrypted backups can be imported on any device where you install Vauchi.
Account Recovery
Vauchi has no central account or "forgot password" mechanism. Recovery depends on your situation:
- Linked devices (primary method): If you have multiple linked devices and at least one remains accessible, your identity and all data are already synchronized. No recovery process is needed.
- Social recovery (all devices lost): If all your devices are lost, trusted contacts you previously designated can vouch for your identity, allowing you to migrate your contacts to a new cryptographic identity on a new device. Note: social recovery creates a new identity — your old signing keys cannot be recovered. Trusted contact designations and per-contact visibility labels may need to be reconfigured.
Children's Privacy
Vauchi does not require registration and does not verify the age of its users. Parents and guardians should be aware that Vauchi allows users to share contact information with people they meet in person.
Changes to This Policy
We may update this privacy policy from time to time. We will notify you of significant changes through the app or our website. Continued use of Vauchi after changes constitutes acceptance of the updated policy.
Open Source
Vauchi is open source software. You can inspect exactly how your data is handled by reviewing our source code at: gitlab.com/vauchi
Contact Us
For privacy-related questions or concerns:
- Email: privacy@vauchi.app
- GitLab: gitlab.com/vauchi
Summary
| Question | Answer |
|---|---|
| Store my contacts on servers? | No, only on your device |
| Can you read my card updates? | No, end-to-end encrypted |
| Do you sell my data? | No, never |
| Do you use tracking/analytics? | No |
| Can I delete my data? | Yes, Settings > Delete Account (7-day grace) |
| Data backed up automatically? | No, you control backups |
| What if I lose my device? | Linked device or social recovery |
For Developers
Welcome to Vauchi development! This section contains everything you need to contribute.
Getting Started
New to the project? Start here:
- Contributing Guide — Set up your environment and learn the workflow
- Architecture Overview — Understand how the system works
- GUI Guidelines & UX Guidelines — Design rules for all platforms
- Cryptography Reference — Deep dive into encryption
Documentation
| Document | Description |
|---|---|
| Contributing | Dev workflow, code guidelines, PR process |
| GUI Guidelines | Component design — toasts, editing |
| UX Guidelines | Physical-first, local-first, flow design |
| Architecture | System overview, components, data flow |
| Cryptography | Encryption algorithms, key management, protocols |
| Tech Stack | Languages, frameworks, libraries |
| Diagrams | Sequence diagrams for core flows |
Repository Structure
Vauchi is a multi-repo project under the
vauchi GitLab group:
| Repository | Purpose |
|---|---|
vauchi/ | Orchestrator repo (this documentation) |
core/ | Rust workspace: vauchi-core + UniFFI bindings |
relay/ | WebSocket relay server |
linux-gtk/ | GTK4 Linux desktop app |
linux-qt/ | Qt6 (Widgets) Linux desktop app |
macos/ | macOS native app (SwiftUI) |
windows/ | Windows native app (WinUI3) |
ios/ | SwiftUI app |
android/ | Kotlin/Compose app |
features/ | Gherkin specs |
locales/ | i18n JSON files |
Quick Commands
# Clone and setup workspace
git clone git@gitlab.com:vauchi/vauchi.git
cd vauchi
just setup
# Build everything
just build
# Run all checks
just check
# Run tests
just test
# Show all commands
just help
Key Principles
All development follows our core principles:
- TDD mandatory — Red → Green → Refactor
- 90%+ coverage — For vauchi-core
- Real crypto in tests — No mocking
- Problem-first — Every task starts as a problem record
Getting Help
- Issues: GitLab Issues
- Discussions: GitLab Issues
- Code of Conduct: Community Standards
Architecture Overview
Vauchi is a privacy-focused contact card system. Users exchange contact cards in person via QR code (with NFC and Bluetooth as additional transport options). After exchange, cards update automatically — when you change your phone number, everyone who has your card sees the change.
System Architecture
┌─────────────────────────────────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────┐
│ CLIENTS │ │ RELAY SERVER │
│ │ │ • Store-and-forward encrypted messages │
│ │ │ • No access to plaintext (oblivious) │
│ ┌───────────────────────────┐ ┌─────────┐ ┌─────────┐ ┌──────┐ ┌──────┐ │ │ ┌───────────•─Rate limiting,─quotas,┐GDPR purge────────────┐ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ iOS │ │ Android │ │ Desktop │ │ CLI │ │ TUI │ │ │ │ Blob Storage │ │ Device Sync │ │ Recovery Store │ │
│ │ SwiftUI │ │ Compose │ │ Native │ │ Rust │ │ Rust │ │ │ │ (encrypted) │ │ (per-device) │ │ (90-day TTL) │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └─────────────┬─────────────┘ └────┬────┘ └────┬────┘ └───┬──┘ └───┬──┘ │ │ └──────────────┘ └──────────────┘ └────────────────┘ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ └──────────────────────────────────────────────────────────────┘
│ ├────────────────────────┴───────────────┴──────────────┴────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────┐ │
│ │ │ │
│ │ vauchi-core │ │
│ │ (UniFFI) │ │
│ │ Crypto, storage, protocol │ │
│ │ │ │
│ └─────────────┬─────────────┘ │
│ │ │
└────────OHTTP─encrypted──────────────────────────────────────────────────────────────────┘
│
│
▼
┌───────────────────────────┐
│ │
│ │
│ OHTTP Gateway │
│ (strips client IP) │
│ │
└─────────────┬─────────────┘
│
WebSocket (TLS)
│
│
▼
┌───────────────────────────┐
│ │
│ Relay │
│ │
└───────────────────────────┘
Note: All remote client↔relay traffic flows through an OHTTP gateway per ADR-037 — the relay never sees client IP addresses, and the gateway never sees request content. Sequence diagrams below omit the gateway hop for protocol clarity.
Core Components
vauchi-core
The Rust core library provides all cryptographic and protocol functionality:
| Module | Purpose | Key Files |
|---|---|---|
crypto/ | Encryption, signing, KDF | encryption.rs, signing.rs |
exchange/ | Contact exchange protocol | session.rs, qr.rs, x3dh.rs |
sync/ | Update propagation | device_sync.rs, delta.rs |
recovery/ | Social recovery | mod.rs |
storage/ | Local encrypted database | contacts.rs, identity.rs |
network/ | Relay communication | connection.rs, protocol.rs |
ui/ | Core-driven UI (vauchi-app) | screen.rs, component.rs |
i18n | Internationalization (vauchi-app) | i18n.rs |
vauchi-protocol
Shared protocol message types used by both vauchi-core and the relay:
- Serde-only crate (no crypto, no I/O)
- Defines
MessageEnvelope,MessagePayload, and all variant structs - Provides framing helpers (
encode_message/decode_message) - Ensures wire format consistency between clients and relay
Relay Server
Rust server for message routing (depends on vauchi-protocol for shared types):
- WebSocket-based store-and-forward
- TLS required in production
- No user accounts — just encrypted blobs
- Background cleanup tasks (hourly)
Client Applications
| Platform | Stack | Binding |
|---|---|---|
| iOS | SwiftUI | vauchi-platform-swift (SPM) |
| Android | Kotlin/Compose | Maven AAR from core CI |
| Linux (GTK) | GTK4 (gtk4-rs) | Direct Rust linkage |
| Linux (Qt) | Qt6 (Widgets) | cbindgen C FFI |
| macOS | SwiftUI | UniFFI (shared with iOS) |
| Windows | WinUI3 (C# .NET 8) | C ABI (vauchi-cabi) |
| CLI | Rust | Direct library use |
| TUI | Rust (ratatui) | Direct library use |
Core-Driven UI
Core defines what to show; frontends only decide how to render natively. New workflows are pure Rust — zero frontend code unless a new component type is needed.
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Core (Rust) │ │ Frontend (per platform) │
│ │ │ │
│ │ │ │
│ ┌────────────────────────────────────────────┐ ┌───────────────────────────────────────────┐ ┌───────────────────────────────────────────────┐ ┌────────────────────────────────────────┐ ┌─────────────────────────────────────────────┐ │ │ ┌───────────────────────────────────────────────────────────┐ ┌───────────────────────────────┐ │ ┌──────┐
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ WorkflowEngine trait │ │ │ │ │ │ │ │ │ │ │ │ "Component Library (one native widget per Component) │ │ ScreenRenderer │ │ │ │
│ │ • current_screen() → ScreenModel │ │ ScreenModel { screen_id, title, subtitle, │ │ Component { TextInput, ToggleList, FieldList, │ │ UserAction { TextChanged, ItemToggled, │ │ ActionResult { UpdateScreen, NavigateTo, │ │ │ │ TextInput → TextField / OutlinedTextField / <input> │ │ Maps ScreenModel → native UI │ │ │ Core │
│ │ • handle_action(UserAction) → ActionResult │ │ components, actions, progress } │ │ CardPreview, InfoPanel, Text, Divider, ... } │ │ ActionPressed, ... } │ │ ValidationError, Complete, ShowToast, ... } │ │ │ │ ToggleList → Toggle list / Checkboxes / [x │ │ Sends UserAction back to core │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └────────────────────────────────────────────┘ └───────────────────────────────────────────┘ └───────────────────────────────────────────────┘ └────────────────────────────────────────┘ └─────────────────────────────────────────────┘ │ │ └───────────────────────────────────────────────────────────┘ └───────────────────────────────┘ │ └───┬──┘
│ │ │ │ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│
│
│
┌────────────────────────────────────────────┐ │
│ │ │
│ Frontend │◄─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ScreenModel─(JSON─or─direct)───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │ UserAction (JSON or direct)
└────────────────────────────────────────────┘
Each frontend implements a component library
(one native component per Component variant) and
a ScreenRenderer that maps ScreenModel to
native UI. The component library is built once and
reused across all workflows.
| Component | Linux GTK4 | Linux Qt (Widgets) | macOS/iOS (SwiftUI) | Android (Compose) | Windows (WinUI3) | TUI (Ratatui) | CLI |
|---|---|---|---|---|---|---|---|
| TextInput | gtk::Entry | TextField | TextField | OutlinedTextField | TextBox | Input widget | stdin prompt |
| ToggleList | gtk::CheckButton | CheckBox | List + Toggle | LazyColumn + Checkbox | ToggleSwitch | [x]/[ ] list | numbered choice |
| FieldList | gtk::ListBox | ListView | List + chips | LazyColumn + chips | ListView | Table rows | formatted output |
| CardPreview | gtk::Frame | Frame | Card view | Card composable | Border | Box render | text output |
| InfoPanel | gtk::Box | ColumnLayout | VStack | Column | StackPanel | Block | println sections |
Transport: Rust clients (CLI, TUI, Desktop)
call WorkflowEngine directly. Mobile clients
(iOS, Android) use JSON over UniFFI.
Adding workflows: Implement a new
WorkflowEngine in core. All frontends render it
automatically via the existing component library —
no frontend changes needed.
Adding component types: Define a new Component
variant in core, then implement the corresponding
native widget in each frontend's component library.
This is rare — the vocabulary stabilizes quickly.
Data Flow
1. Contact Exchange (In-Person)
┌───────┐ ┌─────┐
│ Alice │ │ Bob │
└───┬───┘ └──┬──┘
│ │
│ Display QR (identity + key) │
│────────────────────────────────▶
│ │
│ Scan QR, verify proximity │
◀────────────────────────────────│
│ │
│ X3DH key agreement │
│────────────────────────────────▶
│ │
│ Exchange encrypted cards │
◀────────────────────────────────│
│ │
┌──────────────────────────────────┐
│ Both now have each other's cards │
└──────────────────────────────────┘
│ │
┌───┴───┐ ┌──┴──┐
│ Alice │ │ Bob │
└───────┘ └─────┘
2. Card Updates (Remote via Relay)
┌──────────────────────────────────────────┐
│ │
│ Alice updates phone number │
│ │
└─────────────────────┬────────────────────┘
│
│
│
│
▼
┌──────────────────────────────────────────┐
│ │
│ │
│ Encrypt delta with CEK │
│ (per-contact shared key, Double Ratchet) │
│ │
└─────────────────────┬────────────────────┘
│
│
│
│
▼
┌──────────────────────────────────────────┐
│ │
│ │
│ Send to relay │
│ (WebSocket) │
│ │
└─────────────────────┬────────────────────┘
│
│
│
│
▼
┌──────────────────────────────────────────┐
│ │
│ │
│ Relay stores encrypted blob │
│ (indexed by recipient_id) │
│ │
└─────────────────────┬────────────────────┘
│
│
│
│
▼
┌──────────────────────────────────────────┐
│ │
│ │
│ Bob connects │
│ (receives pending messages) │
│ │
└─────────────────────┬────────────────────┘
│
│
│
│
▼
┌──────────────────────────────────────────┐
│ │
│ │
│ Decrypt delta │
│ (update Alice's card locally) │
│ │
└──────────────────────────────────────────┘
3. Multi-Device Sync
All devices under one identity share the same master seed. Device-specific keys are derived via HKDF:
┌─────────────┐ ┌───────────────────────┐
│ │ │ │
│ │ │ │
│ Master Seed ├────►│ Device 1 keys │
│ │ │ (HKDF + device_index) │
│ │ │ │
└──────┬──────┘ └───────────────────────┘
│
│
│
│
│
│ ┌───────────────────────┐
│ │ │
│ │ │
├───────────►│ Device 2 keys │
│ │ (HKDF + device_index) │
│ │ │
│ └───────────────────────┘
│
│
│
│
│
│ ┌───────────────────────┐
│ │ │
│ │ │
└───────────►│ Device 3 keys │
│ (HKDF + device_index) │
│ │
└───────────────────────┘
Device linking uses QR code scan with time-limited token.
4. Recovery (Social Vouching)
When all devices are lost:
- Create new identity
- Generate recovery claim (old_pk → new_pk)
- Meet contacts in person, collect signed vouchers
- When threshold (3) met, upload proof to relay
- Other contacts discover proof, verify via mutual contacts
- Accept/reject identity transition
Security Model
End-to-End Encryption
- All card data encrypted with XChaCha20-Poly1305
- Per-contact keys derived via X3DH + Double Ratchet
- Forward secrecy: each message uses unique key
- Relay sees only encrypted blobs
Key Hierarchy
┌───────────────────────────────────────────┐
│ │
│ │
│ Master Seed │
│ (256-bit, generated at identity creation) │
│ │
└───────────────────────────────────────────┘
│
│
├─────────────────────────────────────────┬──────────────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────────────────────────────────┐ ┌───────────────────────────┐ ┌────────────────────────────┐
│ │ │ │ │ │
│ │ │ │ │ │
│ Identity Signing Key │ │ Exchange Key │ │ SMK (Shredding Master Key) │
│ (Ed25519, raw seed) │ │ (X25519, HKDF derived) │ │ (HKDF derived) │
│ │ │ │ │ │
└───────────────────────────────────────────┘ └───────────────────────────┘ └────────────────────────────┘
│
│
┌─────────────────────────────────────────┬──────────────────────────────────┤
│ │ │
▼ ▼ ▼
┌───────────────────────────────────────────┐ ┌───────────────────────────┐ ┌────────────────────────────┐
│ │ │ │ │ │
│ │ │ │ │ │
│ SEK │ │ FKEK │ │ Per-Contact CEK │
│ (Storage Encryption Key) │ │ (File Key Encryption Key) │ │ (random 256-bit) │
│ │ │ │ │ │
└───────────────────────────────────────────┘ └───────────────────────────┘ └────────────────────────────┘
Physical Verification
Contact exchange requires in-person presence:
- QR + ultrasonic audio verification (18-20 kHz) — implemented on iOS, planned for Android
- NFC Active tap (planned — centimeters range)
- BLE with RSSI proximity check (planned — GATT transport)
Repository Structure
vauchi/ ← Orchestrator repo
├── core/ ← vauchi-core + vauchi-platform + vauchi-protocol
├── relay/ ← WebSocket relay server (uses vauchi-protocol)
├── linux-gtk/ ← GTK4 Linux desktop app
├── linux-qt/ ← Qt6 (Widgets) Linux desktop app
├── macos/ ← macOS native app (SwiftUI)
├── windows/ ← Windows native app (WinUI3)
├── ios/ ← SwiftUI app
├── android/ ← Kotlin/Compose app
├── cli/ ← Command-line interface
├── tui/ ← Terminal UI
├── features/ ← Gherkin specs
├── locales/ ← i18n JSON files
├── ohttp-relay/ ← OHTTP relay proxy
├── themes/ ← Design tokens
├── e2e/ ← End-to-end tests
└── docs/ ← Documentation
Related Documentation
- GUI Guidelines — Component design rules (toasts, inline editing, confirmations)
- UX Interaction Guidelines — Interaction philosophy (physical-first, local-first, flow design)
- Crypto Reference — Cryptographic operations
- Tech Stack — Technology choices
- Diagrams — Sequence diagrams
Cryptography Reference
Concise reference for all cryptographic operations in Vauchi.
Algorithms
| Purpose | Algorithm | Library | Notes |
|---|---|---|---|
| Signing | Ed25519 | ed25519-dalek | Identity, registry |
| Key Exchange | X25519 | x25519-dalek | X3DH + identity binding |
| Sym. Encrypt | XChaCha20-Poly1305 | chacha20poly1305 | 192-bit nonce |
| Forward Secrecy | Double Ratchet | hkdf + hmac | Chain limit 2000 |
| Key Derivation | HKDF-SHA256 | hkdf | RFC 5869 |
| Password KDF | Argon2id | argon2 | m=64MB, t=3, p=4 |
| CSPRNG | OsRng | rand | OS entropy |
| TLS | TLS 1.2/1.3 | rustls (aws-lc-rs) | Relay only |
Key Types
Identity Keys
| Key | Type | Size | Purpose |
|---|---|---|---|
| Master Seed | Symmetric | 256-bit | Root of all keys |
| Signing Key | Ed25519 | 32+64 bytes | Identity, signatures |
| Exchange Key | X25519 | 32 bytes | Key agreement |
Storage Keys (Shredding Hierarchy)
┌──────────────────────────────────────────────┐
│ │
│ Master Seed (256-bit) │
│ │
└──────────────────────────────────────────────┘
│
│
├─────────────────────────────────────────────────────┬─────────────────────────────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────┐
│ │ │ │ │ │
│ │ │ │ │ │
│ Identity Signing Key │ │ Exchange Key │ │ SMK (Shredding Master Key) │
│ raw seed (Ed25519 requirement) │ │ HKDF(seed, "Vauchi_Exchange_Seed_v2") │ │ HKDF(seed, "Vauchi_Shred_Key_v2") │
│ │ │ │ │ │
└──────────────────────────────────────────────┘ └─────────────────────────────────────────────────┘ └─────────────────────────────────────────────┘
│
│
┌─────────────────────────────────────────────────────┬─────────────────────────────────────────────────────┤
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────┐
│ │ │ │ │ │
│ SEK (Storage Encryption Key) │ │ FKEK (File Key Encryption Key) │ │ Per-Contact CEK │
│ HKDF(SMK, "Vauchi_Storage_Key_v2") │ │ HKDF(SMK, "Vauchi_FileKey_Key_v2") │ │ random 256-bit per contact │
│ encrypts all local SQLite data │ │ encrypts file key storage │ │ encrypts individual contact's card data │
│ │ │ │ │ │
└──────────────────────────────────────────────┘ └─────────────────────────────────────────────────┘ └─────────────────────────────────────────────┘
HKDF Convention: Master seed as IKM, no salt,
domain string as info. All derivations use
HKDF::derive_key(None, &seed, info).
HKDF Context Strings:
| Context | Usage |
|---|---|
Vauchi_Exchange_Seed_v2 | Exchange key derivation from master seed |
Vauchi_Shred_Key_v2 | SMK derivation from master seed |
Vauchi_Storage_Key_v2 | SEK derivation from SMK |
Vauchi_FileKey_Key_v2 | FKEK derivation from SMK |
vauchi-x3dh-symmetric-v2 | X3DH transcript binding (4-key HKDF info) |
vauchi-x3dh-key-v2 | X3DH key agreement derivation |
Vauchi_Root_Ratchet | DH ratchet root key step |
Vauchi_Message_Key | Symmetric ratchet message key |
Vauchi_Chain_Key | Symmetric ratchet chain key advance |
Vauchi_AnonymousSender_v2 | Anonymous sender ID derivation |
Vauchi_Mailbox_v1 | Contact mailbox token (daily rotation, SP-33) |
Vauchi_DeviceSync | Device-to-device encryption key derivation |
Vauchi_DeviceSync_v1 | Device sync self-token (daily rotation, SP-33) |
Ratchet Keys
| Key | Type | Lifecycle |
|---|---|---|
| Root Key | 32 bytes | Updated on DH ratchet |
| Chain Key | 32 bytes | Advances with each message |
| Message Key | 32 bytes | Single-use, deleted after |
Ciphertext Format
algorithm_tag (1 byte) || nonce || ciphertext || tag
| Tag | Algorithm | Nonce | Notes |
|---|---|---|---|
0x01 | AES-256-GCM | 12 bytes | Removed — no longer supported |
0x02 | XChaCha20-Poly1305 | 24 bytes | Default since v0.1.2 |
0x03 | XChaCha20-Poly1305 + AD | 24 bytes | Double Ratchet (header-bound) |
Tag 0x03 binds message header as AEAD associated
data to prevent relay manipulation.
Message Padding
All messages padded to fixed buckets before encryption:
| Bucket | Size | Typical Content |
|---|---|---|
| Small | 256 B | ACK, presence, revocation |
| Medium-Small | 512 B | Short card deltas, single-field updates |
| Medium | 1 KB | Card deltas, small updates |
| Large | 4 KB | Media references, large payloads |
Messages > 4 KB: rounded to next 256-byte boundary.
Format: [4-byte BE length prefix] [plaintext] [random padding]
X3DH Key Agreement
Full X3DH with identity binding (no signed pre-keys):
QR / Mutual Exchange (Symmetric)
Both sides:
ephemeral ← generate X25519 keypair
shared_bytes ← DH(our_ephemeral_secret, their_ephemeral_public)
// Transcript binding: all four public keys sorted lexicographically
// and appended to info, preventing identity misbinding attacks
info ← "vauchi-x3dh-symmetric-v2" || sort(id_lo, id_hi) || sort(eph_lo, eph_hi)
shared ← HKDF(ikm=shared_bytes, salt=None, info=info)
NFC/BLE Exchange
Same as Mutual QR — fresh ephemeral keys on both sides, HKDF-derived shared secret.
Double Ratchet
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ DOUBLE RATCHET │
│ │
│ │
│ ┌──────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ DH RATCHET │ │ SYMMETRIC RATCHET │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ ┌──────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────┐ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ our_dh_secret × their_dh_public │ │ │ │ chain_key │ │ DH ├─┼──────┐ │
│ │ │ │ │ │ │ │ │ │ │ │ │
│ │ └─────────────────┬────────────────┘ │ │ └─────────────────────────────────────────────────┘ └───────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ ├──────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ ▼ │ ▼ │
│ │ ┌──────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────┐ │ ┌────┐ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ HKDF(root_key, shared_secret, │ │ │ │ HKDF(chain_key, "Vauchi_Message_Key") │ │ HKDF(chain_key, "Vauchi_Chain_Key") │ │ │ SR │ │
│ │ │ "Vauchi_Root_Ratchet") │ │ │ │ → message_key (single use) │ │ → next_chain_key │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ └─────────────────┬────────────────┘ │ │ └─────────────────────────────────────────────────┘ └───────────────────────────────────────────────┘ │ └────┘ │
│ │ │ │ │ │ │
│ │ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ "[new_root_key, new_chain_key │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Limits:
- Max chain generations: 2000
- Max skipped keys stored: 1000
- Message key deleted immediately after use
Ratchet Message (Authenticated, Not Encrypted Header)
#![allow(unused)] fn main() { RatchetMessage { dh_public: [u8; 32], // Current DH public key dh_generation: u32, // DH ratchet step counter message_index: u32, // Message index in current chain previous_chain_length: u32, // Messages sent in previous chain ciphertext: Vec<u8>, // Encrypted payload } }
Header (44 bytes) bound as AEAD associated data (tag 0x03).
Backup Format
v2 (Current)
[0x02] || salt(16) || ciphertext
- Key derivation: Argon2id (m=64MB, t=3, p=4)
- Cipher: XChaCha20-Poly1305
- Plaintext:
display_name_len(4) || display_name || master_seed(32) || device_index(4) || device_name_len(4) || device_name
v1 (Removed)
salt(16) || nonce(12) || ciphertext || tag(16)
- Key derivation: PBKDF2-HMAC-SHA256
- Cipher: AES-256-GCM
- Status: Removed from codebase. Documented for format reference only.
Transport Encryption (Noise NK)
Client-to-relay communication uses a Noise NK inner transport layer as defense-in-depth inside TLS.
Pattern
Noise_NK_25519_ChaChaPoly_BLAKE2s
NK means the relay's static public key is known
to the client before the handshake (distributed via
the /info HTTP endpoint as base64url). The client
does not authenticate to the relay (anonymous
initiator).
Handshake
Pre-message: <- s (relay's static public key, known to client)
Message 1: -> e, es (client sends ephemeral, DH with relay static)
Message 2: <- e, ee (relay sends ephemeral, DH between ephemerals)
After Message 2, both sides derive symmetric keys for bidirectional encryption.
v2 Framing
v2 (Noise-encrypted) connections are identified by a 3-byte magic prefix:
0x00 'V' '2' || 48-byte NK handshake message
All connections use the 3-byte 0x00 V 2 prefix
followed by the NK handshake. After the handshake
completes, all subsequent WebSocket frames are
Noise-encrypted.
Why NK?
| Property | Benefit |
|---|---|
| No client auth | Relay cannot link connections to identities |
| Forward secrecy | Past sessions cannot be decrypted |
| Relay auth | Client verifies relay via static key |
| Defense-in-depth | Routing metadata encrypted if TLS fails |
Configuration
| Variable | Default | Description |
|---|---|---|
RELAY_REQUIRE_NOISE | false | Removed — NK always on |
The relay's Noise keypair is auto-generated on first
start and persisted to
{data_dir}/relay_noise_key.bin.
Security Properties
| Property | Mechanism |
|---|---|
| Confidentiality | XChaCha20-Poly1305 encryption |
| Integrity | AEAD authentication tag |
| Authenticity | Ed25519 signatures |
| Forward Secrecy | Double Ratchet, message keys deleted |
| Break-in Recovery | DH ratchet with ephemeral keys |
| No Nonce Reuse | Random 24-byte nonces |
| Memory Safety | zeroize on drop for all keys |
| Traffic Analysis Prevention | Standardized bucket-size message padding |
| Replay Prevention | Double Ratchet counters |
| Transport Encryption | Noise NK inside TLS (defense-in-depth) |
Source Files
| Module | Path |
|---|---|
| Key Derivation | core/vauchi-core/src/crypto/kdf.rs |
| Signing | core/vauchi-core/src/crypto/signing.rs |
| Encryption | core/vauchi-core/src/crypto/encryption.rs |
| Double Ratchet | core/vauchi-core/src/crypto/ratchet.rs |
| Chain Key | core/vauchi-core/src/crypto/chain.rs |
| CEK | core/vauchi-core/src/crypto/cek.rs |
| Shredding | core/vauchi-core/src/crypto/shredding.rs |
| Password KDF | core/vauchi-core/src/crypto/password_kdf.rs |
| X3DH | core/vauchi-core/src/exchange/x3dh.rs |
| X3DH Session (Symmetric) | core/vauchi-core/src/exchange/session.rs |
| Padding | core/vauchi-core/src/crypto/padding.rs |
Related Documentation
- Architecture Overview — System design
- Encryption Feature — User-friendly explanation
- Security — Security model overview
Threat Model
Formal threat analysis of the Vauchi system using the STRIDE framework.
System Context
Vauchi is a contact card exchange system, not a messaging app:
- Traffic pattern: Generated when users physically meet (exchange) or update contact info (small delta to all contacts)
- Data type: Contact cards (name, phone, email, address, social handles)
- Exchange model: In-person only via QR code, NFC, or BLE
- Update frequency: Infrequent (users rarely change phone numbers or emails)
- Update size: Small (delta of changed fields, typically < 1 KB)
This context shapes the threat model: low traffic volume and infrequent updates reduce the value of traffic analysis compared to messaging apps.
Trust Boundaries
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ CLIENT DEVICE │
│ │
│ │
│ ┌───────────────────────────────────────────────────────┐ ┌────────────────────────┐ ┌───────────────────┐ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ vauchi-core ├┄┄┐ │ Local DB │ │ Platform Keychain │ │
│ │ (crypto, protocol) │ ┆ │ (encrypted at rest) │ │ │ │
│ │ │ ┆ │ │ │ │ │
│ └───────────────────────────┬───────────────────────────┘ ┆ └────────────────────────┘ └───────────────────┘ │
│ │ ┆ │
└────────────────────TLS─+─E2E┼encrypted─────────────────────┆───────────────────────────────────────────────────────┘
│ └┄┄┄┄┄┄┄┄┄┄┄optional
│ ┆
▼ ▼
┌───────────────────────────────────────────────────────┐ ┌────────────────────────┐
│ │ │ │
│ SELF-HOSTED REVERSE PROXY (nginx/caddy) │ │ │
│ • Strips all client-identifying headers │◄via┄┤ Optional: SOCKS5 proxy │
│ • Relay never sees client IP addresses │ │ (Tor, VPN, etc.) │
│ │ │ │
└───────────────────────────┬───────────────────────────┘ └────────────────────────┘
│
Internal│network
│
│
▼
┌───────────────────────────────────────────────────────┐
│ │
│ │
│ OHTTP LAYER (RFC 9458) — optional path │
│ • OHTTP relay: sees client IP, cannot read content │
│ • Gateway: decrypts content, sees only OHTTP relay IP │
│ • No single hop sees both client identity and request │
│ │
└───────────────────────────┬───────────────────────────┘
│
│
│
│
▼
┌───────────────────────────────────────────────────────┐
│ │
│ │
│ RELAY SERVER │
│ • Assumed compromised (oblivious design) │
│ • Sees only encrypted blobs, never client IPs │
│ • No user accounts, no decryption keys │
│ • Store-and-forward with TTL │
│ • Timing obfuscation: sync jitter, payload padding │
│ │
└───────────────────────────────────────────────────────┘
| Boundary | Protection | Trust Level |
|---|---|---|
| Client ↔ Rev. Proxy | TLS | Trusted (self-hosted) |
| Rev. Proxy ↔ Relay | Internal net | Untrusted (no client IPs) |
| Client ↔ OHTTP ↔ GW | OHTTP (9458) | No hop sees both ID + content |
| Client ↔ Client | E2E (X3DH + DR) | Verified in person |
| Device ↔ Device | HKDF device keys | Trusted (same seed) |
| Contact ↔ Contact | Per-contact CEK | Verified at exchange |
Assets
| Asset | Sensitivity | Description |
|---|---|---|
| Contact card data | High | Personal info (phone, email, address) |
| Identity keys | Critical | Ed25519 signing + X25519 exchange keys |
| Master seed | Critical | 256-bit root secret for all key derivation |
| Social graph | High | Who knows whom (contact relationships) |
| Update metadata | Medium | When someone changed their info |
Adversary Model
| Adversary | Capability | Motivation |
|---|---|---|
| Passive observer | Encrypted traffic only | Surveillance |
| Malicious relay | Blobs + timing, no IPs | Data harvesting |
| OHTTP relay | Client IP, no content | Correlation |
| Compromised device | Full device access | Targeted attack |
| Physical attacker | Steals/seizes device | Law enforcement |
| Malicious contact | Legitimate contact | Stalking |
STRIDE Analysis
Spoofing
Threat: Attacker impersonates a contact or creates a fake identity.
Mitigations:
- Ed25519 identity keys bound to each user
- Full-trust contact exchange requires physical presence (QR, NFC, or BLE with proximity check)
- Opt-in remote discovery establishes reduced-trust contacts (Tier 0/1) with restricted visibility
- No trust-on-first-use for contact verification: you verify who you connect with in person
- Device registry is cryptographically signed; unauthorized devices cannot be added
Residual risk: Social engineering (convincing someone to scan a QR code under false pretenses).
Tampering
Threat: Relay or network attacker modifies messages in transit.
Mitigations:
- AEAD encryption (XChaCha20-Poly1305) detects any modification via authentication tag
- Ed25519 signatures verify sender authenticity
- Double Ratchet counters and per-message nonces detect replay and reordering
- Device registry version numbers prevent rollback
Residual risk: None. Tampering is cryptographically detected and rejected.
Repudiation
Threat: User denies having sent an update.
Design decision: Repudiation is a privacy feature, not a bug. Vauchi intentionally does not provide non-repudiation for contact card updates. Users should be able to update or remove their information without permanent proof of past states.
Information Disclosure
Threat: Unauthorized access to contact card data.
Mitigations:
- All data E2E encrypted with XChaCha20-Poly1305 before leaving the device
- Per-contact encryption keys (CEK) derived via X3DH + Double Ratchet
- Forward secrecy: each message uses a unique key, deleted after use
- Relay stores only encrypted blobs (oblivious privacy-preserving design)
- Local storage encrypted with device-derived keys (SEK from HKDF key hierarchy)
- Sensitive key material zeroized on drop (
zeroizecrate) - Message padding to fixed buckets (256 B, 512 B, 1 KB, 4 KB) prevents size-based inference
Residual risk: Metadata (connection timing, recipient pseudonyms) visible to relay. Mitigated by the four-layer privacy architecture: reverse proxy (strips client IPs), OHTTP (cryptographic content protection), timing obfuscation (sync jitter + padding), and optional SOCKS5 proxy support.
Denial of Service
Threat: Attacker disrupts relay availability.
Mitigations:
- Token-bucket rate limiting (60 msgs/min per client, configurable)
- Stricter recovery rate limit (10 queries/min, anti-enumeration)
- Connection limit (max 1000 concurrent, RAII guard)
- Multi-layer timeout protection: handshake timeout, idle timeout (5 min)
- Message size limit (1 MB)
- Automatic message expiration (120-day TTL; recovery store: 90-day TTL)
- Federation support for relay redundancy
Residual risk: Sustained DDoS from many source IPs can overwhelm a single relay.
Elevation of Privilege
Threat: Attacker gains unauthorized capabilities.
Mitigations:
- No user accounts on relay: no admin interface, no privilege levels
- Client capabilities limited to own identity (can only decrypt own messages)
- Master seed required for all identity operations
- Device linking requires physical QR scan + identity key signature verification
Residual risk: Compromised device with master seed has full identity control. Mitigated by device revocation broadcast.
Key Security Properties
| Property | Mechanism |
|---|---|
| Confidentiality | XChaCha20-Poly1305 E2E encryption |
| Integrity | AEAD authentication tag + Ed25519 signatures |
| Forward secrecy | Double Ratchet with ephemeral DH keys |
| Break-in recovery | DH ratchet step generates new key material |
| Oblivious relay | Encrypted blobs only, no keys |
| Physical verification | QR + audio / NFC / BLE (full); SAS (video) |
| Traffic analysis resist. | Bucket padding + routing tokens + jitter |
| IP privacy | Reverse proxy + OHTTP (RFC 9458) |
| Memory safety | Rust (no unsafe in crypto paths) + zeroize on drop |
| Replay prevention | Double Ratchet counters + version numbers |
Attack Scenarios
Relay Compromise
If an attacker gains full control of a relay:
- Cannot read contact card data (E2E encrypted)
- Cannot forge messages (AEAD + signatures)
- Can see pseudonymous recipient IDs and message timing
- Can drop or delay messages (detected by delivery acknowledgments)
- Cannot see client IP addresses (reverse proxy strips headers; OHTTP encrypts requests to gateway)
- Can observe connection timing patterns (mitigated by timing obfuscation: 30s-5min post-exchange jitter, ±15% sync interval jitter)
Device Theft
If an attacker steals a device:
- Master seed encrypted with user password via Argon2id (m=64 MB, t=3, p=4)
- Platform-native key storage (macOS Keychain, iOS Keychain, Android KeyStore)
- All key material zeroized on drop
- Weak passwords can be brute-forced (Argon2id raises cost significantly)
- Recommendation: Use a strong backup password, enable OS-level device encryption
Recovery Impersonation
If an attacker tries to abuse social recovery:
- Must meet K contacts in person (default threshold: 3)
- Each voucher signs with their identity key
- Vouchers validated against victim's known contact list
- Conflicting claims detected and flagged by relay
- Voucher timestamps prevent replay (48-hour window)
Federation Security
When relays federate (forward blobs to peer relays for redundancy), additional trust boundaries apply.
What a federated relay CAN see:
- Recipient routing IDs (pseudonymous public key hashes)
- Blob sizes (padded to fixed buckets: 256, 512, 1024, 4096 bytes)
- Message creation timestamps and hop count
- Network topology via gossip protocol
What a federated relay CANNOT see:
- Message content (E2E encrypted)
- Sender identity (not included in federation protocol)
- Real identity behind routing IDs
- Client IP addresses (federation is relay-to-relay; clients are behind reverse proxy + OHTTP)
Guarantees:
- mTLS authentication prevents unauthorized peers
- Hop count limit prevents amplification attacks
- SHA-256 integrity verification on all federated blobs
- DNS rebinding protection: explicit resolution + SSRF validation before connecting to peer relays
Device Linking (STRIDE)
| Category | Threat | Mitigation |
|---|---|---|
| Spoofing | Scan link QR | 5-min expiry, proximity |
| Tampering | Modified QR | Signed by identity key |
| Info Disclosure | Seed intercepted | Ephemeral key encryption |
| DoS | Excess linking | Max 10 devices |
| Elev. of Privilege | Unauth device | Signed registry + version |
Known limitation: Currently, the full master seed is shared during device linking (not per-device subkeys). A compromised linked device gains full identity control. Per-device subkey isolation is planned for 1.0 release.
Remote Discovery (Opt-In)
When remote discovery is enabled, users can generate discovery tokens that allow contacts to be established without physical proximity. This introduces a new attack surface that is mitigated by graduated trust tiers.
Trust Tiers
| Tier | Name | Established Via | Capabilities |
|---|---|---|---|
| 0 | Pending | Token-based contact request | View-only, expires after 30 days |
| 1 | Accepted | Recipient accepts request | Everyone-visibility fields only |
| 2 | VideoVerified | SAS + liveness (video) | Label-based visibility |
| 3 | InPerson | Physical QR/NFC/BLE | Full access, recovery |
Recovery and facilitated introductions remain gated to Tier 3 (InPerson) only.
Attack Surface
| Threat | Mitigation |
|---|---|
| Token spam | Expiry, one-time use, rate limit |
| Phishing token | Tier 0 only, no sensitive fields |
| Social eng. to T1 | Everyone fields only |
| Video MITM | SAS 6-digit code mismatch |
| Deepfake/replay | Finger-count liveness challenge |
| Sender leak | Sealed sender, ephemeral IDs |
| Recipient leak | Daily-rotating mailbox tokens |
| Token replay | Ed25519 sig + expiry + one-time |
| Mailbox correlation | Daily rotation per-contact pair |
Sealed Sender Properties
Sealed sender is always enabled — it improves metadata protection for all users, not just remote contacts. With sealed sender delivery, the relay sees:
- Session ID: Ephemeral, per-connection (no identity key)
- Mailbox token: Opaque, daily rotation, derived from shared key
- Encrypted blob: AEAD ciphertext, padded to fixed buckets
The relay cannot determine: who sent a message, who the real recipient is (beyond the opaque token), or whether two sessions belong to the same user across reconnections.
Residual Risks
| Risk | Sev. | Notes |
|---|---|---|
| Weak remote trust | Low | Tier gating limits to Everyone |
| Untrusted token channels | Med | User responsibility |
| Compromised video platform | Low | SAS binding is independent |
| Mailbox token correlation | Low | Brief overlap, mitigated by OHTTP |
Known Limitations
| Limitation | Impact | Mitigation |
|---|---|---|
| Relay sees metadata | Graph inference | Sealed sender, 4-layer arch |
| Device compromise | Full seed exposed | Subkeys (planned), passwords |
| Linked device = full seed | Full identity | Subkeys (planned) |
| Single relay SPOF | No sync if down | Federation (planned) |
| Blocked contacts keep data | Can't unsend | By design |
| Recovery leaks voucher PKs | Partial graph | Accepted tradeoff |
| No guardian diversity | Same circle | UX warnings (planned) |
| No key transparency | Not auditable | Signed key log (future) |
| Remote trust weaker | No in-person | Tier gating; upgrade path |
| Push leaks metadata | Delivery timing | Empty push + fetch |
Core-UI Trust Boundary
The vauchi-core library is consumed by multiple
UI layers (macOS/SwiftUI, Linux-GTK, Linux-Qt, CLI,
TUI, mobile Swift/Kotlin via UniFFI). The trust
relationship between core and its callers:
What core trusts from UI:
- UI provides correct file paths for storage
- UI invokes lifecycle methods in the correct order (init before use)
- UI does not hold references to sensitive data after core has zeroized them
What core does NOT trust from UI:
- Input lengths: Core enforces maximum lengths at its API boundary (display name: 100 chars, field value: 1000 chars, field label: 64 chars, card size: 64 KB, avatar: 256 KB)
- Field values: Core validates phone/email format, URL safety, and rejects malformed data
- Contact IDs: Core verifies contact existence before processing updates
- Ratchet messages: Core validates signatures, AEAD tags, and replay nonces before accepting peer data
- File content: Core verifies checksums on all content fetched from remote servers
A compromised or buggy UI layer cannot cause vauchi-core to:
- Decrypt data for an unauthorized recipient
- Produce unsigned or weakly signed messages
- Skip replay detection or signature verification
- Bypass visibility label enforcement
UniFFI surface note: The UniFFI binding layer does not add rate limiting at the API boundary. Rate limiting for relay operations is enforced server-side. UI layers should implement their own rate limiting for user-facing operations to prevent accidental rapid-fire calls.
Relay Metadata Exposure Analysis
While the relay sees only encrypted blobs, it observes metadata that can reveal the social graph:
What the relay sees:
| Datum | Visibility | Example |
|---|---|---|
| Sender (session ID) | Per-connection | Ephemeral, no identity key |
| Recipient (mailbox) | Daily rotation | HKDF-derived, opaque |
| Message timing | Obfuscated | Jittered 30s-5min / ±15% |
| Message size | Approximate | Padded to buckets |
| Connection freq. | Approximate | Jittered intervals |
What the relay CANNOT see:
- Client IP addresses (stripped by self-hosted reverse proxy; OHTTP provides additional cryptographic separation)
- Message content (E2E encrypted, AEAD)
- Contact card fields (encrypted under per-contact CEK)
- Sender identity beyond pseudonymous routing ID
- Which specific contact fields changed
Risk assessment for Vauchi:
The social graph inference risk is lower than for messaging apps because:
- Updates are infrequent (users rarely change contact info)
- Updates are small and padded to fixed buckets (256 B, 512 B, 1 KB, 4 KB)
- All locale files are downloaded in bulk to prevent language inference
- Routing IDs are pseudonymous and session-scoped
- Relay never sees client IPs (reverse proxy strips headers; OHTTP provides cryptographic separation)
- Timing obfuscation (sync jitter, post-exchange delay) prevents correlation of connection patterns
Current mitigations (four-layer privacy architecture):
- Self-hosted reverse proxy (nginx/caddy) — strips all client-identifying headers before forwarding to relay. The relay provably never touches client IP addresses.
- OHTTP (RFC 9458) — cryptographic content protection. The OHTTP relay sees the client IP but cannot read request content (encrypted to gateway). The gateway decrypts but only sees the OHTTP relay's internal IP. No single hop sees both client identity and request content.
- Timing obfuscation — post-exchange sync jitter (30s-5min random delay), sync interval jitter (+-15%), payload padding to bucket sizes. Applied to all users by default.
- Generic SOCKS5 proxy support — optional, user-configured. Route through Tor, VPN, or any SOCKS5 proxy for ISP-level hiding.
Additional mitigations: routing token rotation,
suppress_presence flag, standardized bucket-size
message padding.
Why OHTTP over Tor: Tor's 90-120s mobile bootstrap latency is fatal for adoption. Tor traffic is banned or flagged in countries that need privacy most (China, Iran, Russia). OHTTP traffic is indistinguishable from normal HTTPS — no special protocol signatures. The four-layer architecture provides equivalent metadata protection for Vauchi's threat model without the usability and censorship-resistance penalties.
Future considerations: Private Information Retrieval (PIR) could eliminate recipient pseudonym visibility entirely. Cover traffic patterns could further reduce timing correlation. These are not currently prioritized given Vauchi's low-frequency traffic pattern, but remain on the architectural roadmap for high-threat deployments.
Key Transparency
Current Model
Vauchi uses a trust-on-exchange model: during the initial in-person exchange (QR/NFC/BLE), both parties verify each other's Ed25519 identity keys directly. This provides strong initial authentication.
After the exchange, contacts receive updates via the Double Ratchet:
- If a user adds a new device, it derives keys from the same master seed
- Contacts receive a
DeviceRegistryupdate signed by the existing identity key - The ratchet provides forward secrecy and break-in recovery
Gap
There is no append-only, auditable log of a user's key history. A sophisticated attacker who compromises both a relay and a user's device could theoretically:
- Generate a new identity key for the victim
- Distribute it to the victim's contacts via the compromised relay
- Contacts would have no way to verify this is consistent with the victim's key history
Accepted Tradeoff
This attack requires simultaneous compromise of both the relay and the target device, which is beyond Vauchi's primary threat model (relay-only compromise). The in-person exchange provides a strong root of trust that subsequent key changes cannot easily override without raising suspicion (contacts would see unexpected re-exchange requests).
Future Direction
A lightweight key transparency mechanism could provide additional assurance:
- Signed key history: Each user maintains a signed append-only log of their key changes. Contacts can audit this log to detect unauthorized key modifications.
- Cross-contact verification: Contacts can compare their view of a user's key history with other contacts to detect divergence (split-world attack detection).
This is not currently implemented but is documented as a future enhancement for high-assurance deployments. A full CONIKS-style transparency log is not necessary given Vauchi's decentralized architecture and low update frequency.
Push Notification Constraints
Push notifications (APNs for iOS, FCM for Android) are not currently implemented. If they are added in the future, the following constraint is mandatory:
Threat: Naive push notifications would expose message receipt timing to Apple/Google. Even though the relay sees only encrypted blobs, a push notification would link a specific device token (tied to a real Apple/Google account) to the exact moment a Vauchi message arrives. This undermines the oblivious privacy-preserving relay design.
Required pattern: Empty push + app-side fetch.
- The relay sends a push notification with no payload and no sender information — only a "wake up" signal
- The app receives the push, connects to the relay independently over TLS
- The app fetches pending messages using its normal encrypted channel
- Apple/Google learn only that the app was woken up, not who sent a message or what it contains
This pattern is used by Signal and other privacy-focused applications. Any implementation of push notifications in Vauchi must follow this pattern. Direct payload delivery via push is explicitly prohibited.
Cryptographic Primitives
| Purpose | Algorithm | Library |
|---|---|---|
| Signing | Ed25519 | ed25519-dalek |
| Key exchange | X25519 | x25519-dalek |
| Symmetric encryption | XChaCha20-Poly1305 | chacha20poly1305 |
| Password KDF | Argon2id | argon2 |
| Key derivation | HKDF-SHA256 | hkdf |
| CSPRNG | OsRng | rand |
| TLS | TLS 1.2/1.3 | rustls (aws-lc-rs backend) |
For the full cryptographic specification, see the Cryptography Reference.
Comparison with Messaging Apps
| Aspect | Vauchi | Messaging Apps |
|---|---|---|
| Traffic volume | Very low | High (continuous) |
| Timing analysis | Low (jittered) | High |
| Social graph | Low (no IPs) | High |
| Metadata | Recipient ID only | Sender + recip + IPs |
| IP privacy | Proxy + OHTTP | Often server sees IPs |
| Forward secrecy | Yes (DR) | Varies |
| Relay knowledge | Encrypted blobs | Often plaintext |
| Recovery | Social vouching | Phone/cloud backup |
Vauchi's infrequent, small updates significantly reduce the value of traffic analysis compared to messaging apps.
Security Reporting
Found a vulnerability? Please report it responsibly:
Email: security@vauchi.app
We acknowledge reports within 48 hours and do not pursue legal action against good-faith researchers.
Related Documentation
- Security Overview — User-friendly security explanation
- Cryptography Reference — Full cryptographic specification
- Architecture Overview — System design
Contributing to Vauchi
Quick Start
# Clone and setup workspace
git clone git@gitlab.com:vauchi/vauchi.git
cd vauchi
just setup
# Build and test
just build
just check
Development Workflow
All repos have protected main branches — no direct
pushes, merge via MR only, force push disabled.
Step 1: Create a branch
git checkout -b feature/my-feature
Branch naming: {type}/{short-description}
| Type | Use Case |
|---|---|
feature/ | New functionality |
bugfix/ | Bug fixes |
refactor/ | Code restructuring |
tidy/ | Structural-only cleanup (Tidy First) |
investigation/ | Research/exploration |
For multi-repo features, use the same branch name in every affected repo:
just git branch feature/remote-content-updates features core docs
Step 2: Do the work — commit often
- If changing code: strict TDD. Tidy -> Red ->
Green -> Refactor. No exceptions.
- Tidy first — small structural improvement if
the code is hard to change (own
tidy:commit) - Write failing test first (Red)
- Write minimal code to pass (Green)
- Refactor
- Tests must trace to
features/*.featurescenarios
- Tidy first — small structural improvement if
the code is hard to change (own
- Commit atomically — each commit should be a single logical change.
- Run
just checkbefore pushing (formats, lints, tests). - See Principles for core values.
Commit message format:
{type}: {Short description}
{Longer description if needed}
Types: feat, fix, refactor, tidy, docs, test, chore
Use imperative mood: "Add feature" not "Added feature". Keep first line under 72 characters.
Use just commit for interactive commits across all repos with changes.
Step 3: Create a Merge Request
# Push all repos with changes (runs pre-push checks)
just git push-all
# Create MR for a specific repo
just gitlab mr-create core
CI must pass (security scans + tests) before merge.
If the work spans multiple repos, each MR must list the related MRs it depends on:
## Summary
- Bullet points of changes
## Related MRs
- vauchi/features!42 - Gherkin specs
- vauchi/core!78 - Implementation
## Test Plan
- [ ] Tests pass
- [ ] Manual testing done
## Checklist
- [ ] Follows TDD
- [ ] Documentation updated
- [ ] Feature file updated (if applicable)
- [ ] Follows [GUI Guidelines](gui-guidelines.md)
and [UX Guidelines](ux-guidelines.md)
(if UI changes)
Use GitLab MR reference format: {group}/{project}!{mr_number}
Code Guidelines
Rust
- Use RustCrypto crates: audited (
ed25519-dalek,x25519-dalek— Trail of Bits), IETF-standardized (chacha20poly1305,argon2), well-established (sha2,hmac,hkdf);aws-lc-rsfor TLS only (via rustls) - Never mock crypto in tests
- 90%+ test coverage for vauchi-core
- Use
Result/Option, fail fast
Structure
crate/
├── src/ # Production code only
└── tests/ # Integration tests only
Mobile Bindings Workflow
UniFFI generates Swift/Kotlin bindings from Rust.
When modifying vauchi-platform or vauchi-core:
When to Regenerate Bindings
Regenerate bindings when you:
- Add/remove/rename exported types or functions
- Change function signatures
- Add new
#[uniffi::export]or#[derive(uniffi::*)]annotations
How to Regenerate
cd core
# IMPORTANT: Build without symbol stripping to preserve metadata
RUSTFLAGS="-Cstrip=none" cargo build -p vauchi-platform --release
# Regenerate for both platforms (macOS required for iOS)
./scripts/build-bindings.sh
# Or regenerate Android only (works on Linux)
./scripts/build-bindings.sh --android
# Validate bindings have all expected types
./scripts/validate-bindings.sh
Common Issues
Empty or incomplete bindings:
- Cause: Library built with symbol stripping (default in release)
- Fix: Use
RUSTFLAGS="-Cstrip=none"when building
Missing types in generated code:
- Run
validate-bindings.shto check expected types - Regenerate with the steps above
Repository Organisation
This is a multi-repo project under the
vauchi GitLab group.
The root repo (vauchi/vauchi) is the workspace
orchestrator — just setup clones all sub-repos as
sibling directories. Each subdirectory is its own
Git repo at gitlab.com/vauchi/<name>.
vauchi/ ← root repo (justfile, CI config)
│
├── core/ ← Rust workspace: vauchi-core + vauchi-platform (UniFFI)
├── relay/ ← WebSocket relay server (standalone Rust)
├── cli/ ← Command-line interface
├── tui/ ← Terminal user interface
│
├── android/ ← Kotlin/Compose native app
├── ios/ ← SwiftUI native app
├── macos/ ← SwiftUI macOS app
├── linux-gtk/ ← GTK4 + libadwaita Linux app
├── linux-qt/ ← Qt6 Linux app
├── windows/ ← WinUI 3 Windows app
├── web-demo/ ← SolidJS + WASM demo app
├── vauchi-platform-swift/ ← Generated Swift bindings + XCFramework (SPM distribution)
│
├── e2e/ ← End-to-end tests
├── features/ ← Gherkin specs (shared across all platforms)
├── locales/ ← i18n locale files
├── themes/ ← Design tokens
├── ohttp-relay/ ← OHTTP relay proxy
│
├── docs/ ← Public documentation (this site)
├── scripts/ ← Dev tools, hooks, utilities
├── website/ ← Landing page source
└── assets/ ← Brand assets, logos
Platform bindings (vauchi-platform-swift/) are
not manually edited — core/ CI generates
UniFFI bindings and pushes artifacts to this repo
when merging to main. Android bindings are
distributed as a Maven AAR published by core CI.
Release Workflow
Vauchi uses a 3-tier versioning system. Each tier triggers a different CI pipeline scope:
| Tier | Tag Format | What Runs | Publishes? |
|---|---|---|---|
| Dev | v0.2.3-dev.N | Lint + test only | No |
| RC | v0.2.3-rc.N | Lint + test + coverage + mutation + security | No |
| PROD | v0.2.3 | Full release: build, package, publish, deploy | Yes |
Creating releases
just release-dev [repo] # Fast feedback — default: core. E.g., just release-dev cli
just release-rc [repo] # Full quality gate. E.g., just release-rc relay
just release-prod [repo] # Full release. E.g., just release-prod core
just release-history [repo] # Show promotion chain. E.g., just release-history tui
Typical flow
- Merge feature MRs to
main just release-dev— verify basic CI passes (~2 min)just release-rc— full quality gate with coverage + mutation (~15 min)just release-prod— publish to package registry, trigger mobile binding distribution
Dev and RC tags never publish artifacts, trigger mobile repos, or create GitLab releases. Only PROD tags do.
Useful Commands
just help # Show all commands
just check-annotations # Check test coverage vs features
just relay # Start local relay for testing
just run cli # Run CLI
just git sync # Fetch all + pull where on main
Getting Help
- Review existing issues
- Ask in GitLab Issues
License
By contributing, you agree that your contributions will be licensed under GPL-3.0-or-later.
Technology Stack
Core Library (Shared)
| Component | Technology | Notes |
|---|---|---|
| Language | Rust | Memory safety, cross-platform |
| Crypto | ed25519-dalek, x25519-dalek, chacha20poly1305, argon2 | Audited (ed25519/x25519: Trail of Bits) + IETF-standardized (chacha20/argon2) |
| Storage | SQLite | Encrypted with XChaCha20-Poly1305 |
| Serialization | serde + JSON | Protocol messages |
| FFI | UniFFI | Swift/Kotlin bindings |
Mobile Apps
iOS
| Component | Technology |
|---|---|
| UI Framework | SwiftUI |
| Language | Swift |
| Bindings | UniFFI (SPM package) |
| Min iOS | 15.0 |
Android
| Component | Technology |
|---|---|
| UI Framework | Jetpack Compose |
| Language | Kotlin |
| Bindings | UniFFI (Gradle dependency) |
| Min SDK | 26 (Android 8.0) |
Desktop Apps (Native)
| Platform | Framework | Language | Bindings |
|---|---|---|---|
| macOS | SwiftUI | Swift | UniFFI (SPM) |
| Linux (GTK) | GTK4 + libadwaita | Rust | Direct (same process) |
| Linux (Qt) | Qt 6 (Widgets) | C++ | C ABI (vauchi-cabi) |
| Windows | WinUI 3 | C# (.NET 8) | C ABI (vauchi-cabi) |
Web Demo
| Component | Technology | Notes |
|---|---|---|
| Framework | SolidJS | TypeScript, WASM bridge |
| Core | vauchi-core (WASM) | wasm32-unknown-unknown target |
| Crypto | Pure RustCrypto (WASM) | No WebCrypto bridge needed (SP-30) |
CLI & TUI
| Component | Technology |
|---|---|
| CLI | Rust (clap) |
| TUI | Rust (ratatui) |
Relay Server
| Component | Technology | Notes |
|---|---|---|
| Language | Rust | Standalone binary |
| WebSocket | tokio-tungstenite | Async runtime |
| TLS | rustls | Certificate handling |
| Storage | In-memory + disk | Encrypted blobs only |
Development Tools
| Tool | Purpose |
|---|---|
| Just | Task runner |
| Cargo | Rust package manager |
| npm/pnpm | JavaScript dependencies |
| Docker | Containerization |
| GitLab CI | Continuous integration |
Performance Targets
| Operation | Target |
|---|---|
| Contact exchange | < 3 seconds |
| Update propagation | < 30 seconds (when online) |
| Local operations | < 100ms |
| App startup | < 2 seconds |
Data Limits
| Limit | Value |
|---|---|
| Max contact card size | 64KB (encrypted) |
| Max contacts per user | 10,000 |
| Max fields per card | 200 |
| Max linked devices | 10 |
Repository Dependencies
vauchi-core (standalone, no workspace deps)
↑ (git dependency)
cli/, tui/, e2e/, macos/, windows/, linux-gtk/, linux-qt/, web-demo/
vauchi-platform (UniFFI bindings, in core/ workspace)
↑ (via generated binding repos)
android/ ← Maven AAR from core CI
ios/ ← vauchi-platform-swift (SPM)
vauchi-cabi (C ABI exports, in core/ workspace)
↑ (cbindgen)
linux-qt/, windows/
relay/ (standalone, uses vauchi-protocol for shared types only)
Downstream repos use git dependencies with branch-based pinning (branch = "main").
Local development uses .cargo/config.toml path overrides.
Related Documentation
- Architecture Overview — System design
- Contributing Guide — Development setup
- Crypto Reference — Cryptographic details
TDD Rules
Three Laws
- No production code without a failing test
- Write only enough test to fail
- Write only enough code to pass
Tidy-Red-Green-Refactor-Commit
TIDY → Small structural improvement → COMMIT (no behavior change)
RED → Write failing test
GREEN → Minimal code to pass → COMMIT (tests green)
REFACTOR → Improve design → COMMIT (tests still green)
Inspired by Kent Beck's Tidy First?: make the change easy, then make the easy change.
TIDY is an optional pre-step before starting a Red-Green cycle. A tidying is a
small, structural-only change that makes the upcoming work easier — guard clauses,
extract helper, rename for clarity, reorder for readability, delete dead code.
Tidyings never change behavior and always get their own commit (tidy: type).
When to tidy:
| Timing | Guidance |
|---|---|
| Tidy First | Default. The code you're about to change is hard to read or extend |
| Tidy After | You understand the shape of the change better after implementing |
| Tidy Later | Batch structural cleanup into a separate branch/MR |
| Never | Code works, won't change again, and is readable enough |
Commit early, commit often:
- Commit tidyings separately before the RED step (
tidy:commit) - Commit immediately after GREEN (tests pass for the first time)
- Commit after each REFACTOR cycle (if tests still pass)
- Small, atomic commits make rollback and review easier
- Never commit with failing tests
Tidying Catalog
Small, safe, structural-only changes (from Tidy First?):
| Tidying | What it does |
|---|---|
| Guard Clauses | Replace nested if/match with early returns |
| Dead Code | Delete unreachable or unused code |
| Normalize Symmetries | Make similar code use consistent patterns |
| New Interface, Old Implementation | Wrap before replacing internals |
| Reading Order | Reorder declarations top-down |
| Cohesion Order | Group related items together |
| Move Declaration and Initialization Together | Close the gap between let and first use |
| Explaining Variables | Name intermediate results |
| Explaining Constants | Replace magic numbers with named constants |
| Explicit Parameters | Pass values instead of relying on ambient state |
| Chunk Statements | Add blank lines between logical blocks |
| Extract Helper | Pull reusable logic into a function |
| One Pile | Inline before re-extracting with better structure |
| Explaining Comments | Add "why" comments where intent isn't obvious |
| Delete Redundant Comments | Remove comments that repeat the code |
Test Types
| Type | Scope | Speed | Coverage |
|---|---|---|---|
| Unit | Single function | < 100ms | 90% min |
| Integration | Multiple components | < 5s | Critical paths |
| E2E | Full system | < 60s | All Gherkin scenarios |
Naming
test_<function>_<scenario>_<expected>
Examples:
- test_encrypt_valid_key_returns_ciphertext
- test_decrypt_wrong_key_fails
Critical Rules
Crypto - Never mock. Test with real crypto:
- Roundtrip (encrypt/decrypt, sign/verify)
- Wrong key rejection
- Tampered data rejection
Gherkin - Every scenario in features/ must have a test.
Coverage - 90% minimum for vauchi-core.
Forbidden
- Writing code before tests
- Mocking crypto operations
#[ignore]without tracking issue- Flaky/non-deterministic tests
- Hardcoded test secrets
Mocking Strategy
| Component | Approach |
|---|---|
| Crypto | Real (never mock) |
| Network | Mock transport |
| Storage | In-memory DB |
| Time | Mockable clock |
PR Checklist
-
Structural tidyings in separate
tidy:commits (if any) - Tests written before code
- All Gherkin scenarios covered
- No ignored tests
- Coverage ≥ 90%
- Crypto has security tests
GUI Design Guidelines
Cross-platform design rules for all vauchi client interfaces — smartphones, dumb-phones, smartwatches, tablets, laptops, desktops (Windows, macOS, Linux, Android, iOS).
These rules enforce Principle 4: Simplicity serves the user — vauchi stays out of your way. All GUI contributions must follow these guidelines. For interaction-level guidelines (flows, physical device usage, navigation philosophy), see the sibling document UX Interaction Guidelines.
Problem Statement
Modal dialogs and confirmation popups interrupt user flow. Every "Are you sure?" dialog forces the user to stop, read, and click — even for routine, reversible actions like deleting a contact or hiding a field. Users develop dialog fatigue: they stop reading and click through reflexively, which defeats the safety purpose entirely.
The fix is not fewer safety nets — it's better ones. Modern interfaces (Gmail, Slack, iOS Mail) prove that act-then-undo is both safer and faster than ask-then-act. Users stay in flow, and recovery from mistakes is immediate.
The Rules
UI-01: No Dialogs for Reversible Actions
If an action can be undone, do it immediately. Show a non-blocking toast/snackbar with an Undo button (5-second window). No confirmation dialog.
Examples:
| Action | Wrong | Right |
|---|---|---|
| Delete contact | "Are you sure?" modal | Toast: "Deleted. Undo" |
| Hide field | Confirmation dialog | Toast: "Hidden. Undo" |
| Unlink device | "Confirm unlink?" popup | Toast: "Unlinked. Undo" |
Why: Users perform reversible actions frequently. Interrupting each one trains them to click "OK" without reading — making real confirmations invisible.
UI-02: Confirm Only Irrevocable Actions
Reserve confirmation for actions that cannot be undone: permanent identity wipe, recovery phrase reset, key deletion. These are rare by design.
When confirmation is needed, use inline confirmation — expand the action area in place with a clear warning and explicit confirm/cancel buttons. Do not launch a modal overlay.
[ Delete Identity ]
↓ click
┌─────────────────────────────────────────┐
│ This permanently deletes your identity │
│ and all contacts. This cannot be undone.│
│ │
│ [ Cancel ] [ Delete Forever ] │
└─────────────────────────────────────────┘
Why: Inline confirmation keeps context visible. Modal dialogs obscure what the user was looking at, forcing them to hold the context in memory.
UI-03: Inline Over Overlay
Editing, status messages, errors, and form validation appear inline — in the context where the action happened. Never launch a modal to show a single text field, a status message, or a validation error.
| Use case | Wrong | Right |
|---|---|---|
| Edit contact name | Modal with text input | Name becomes editable in place |
| Validation error | Alert popup | Red border + inline message |
| Success message | "Saved!" modal | Brief inline indicator or toast |
| Error message | Error dialog | Inline error banner at the relevant location |
Why: Overlays break spatial context. Users lose their place and must re-orient after dismissing the dialog.
UI-04: Progressive Disclosure
Show only what the user needs now. Advanced options, secondary actions, and details expand in-place on demand.
- Default views show primary content only
- "Show more", accordions, and expandable sections reveal detail
- Settings pages use sections, not nested dialogs
- Help text appears on hover/focus, not in separate windows
Why: Front-loading complexity overwhelms users and obscures the primary action. Let them drill in when they choose to.
UI-05: Follow Platform Conventions
Each platform adapts these rules using native idioms. Users expect their platform's patterns — don't invent new ones.
| Concept | Linux GTK4 | Linux Qt (Widgets) | Windows (WinUI3) | macOS (SwiftUI) | Android (Compose) | iOS (SwiftUI) | watchOS / Wear OS | KaiOS (Web) |
|---|---|---|---|---|---|---|---|---|
| Toast/Undo | adw::Toast | Custom QWidget overlay | InfoBar | Custom overlay | SnackbarHost | Custom overlay | Haptic + brief text | Soft-key toast |
| Inline confirm | In-place gtk::Box | Inline QHBoxLayout | Inline StackPanel | Inline VStack | Inline Row | Swipe + confirm | Crown press-hold | Confirm soft-key |
| Inline edit | gtk::Entry swap | QLineEdit swap | TextBox swap | TextField swap | TextField swap | TextField swap | Voice or companion | D-pad select |
| Navigation | adw::NavigationView | QStackedWidget | NavigationView | NavigationStack | NavHost / M3 | NavigationStack | Vertical page list | Soft-key tabs |
| Loading | gtk::Spinner | QProgressBar | ProgressRing | ProgressView | CircularProgress | ProgressView | Dots animation | Inline text |
When platform convention conflicts with these rules: Platform convention wins for interaction patterns (gestures, navigation, system dialogs). These rules win for information architecture (what triggers a dialog vs. inline action).
UI-06: One Primary Action per Screen
Each screen has one primary action, visually dominant. Secondary actions are visually subordinate (smaller, muted, or behind a menu).
- Primary action: filled/accent button, prominent placement
- Secondary actions: outlined or text buttons, grouped away from primary
- Destructive actions: never the primary visual element — require deliberate reach (end of list, behind a menu, or expandable section)
Why: When everything is prominent, nothing is. Users hesitate when faced with multiple equally weighted choices.
UI-07: Immediate Feedback
Every user action gets visible feedback within 100ms.
- Tap/click: visual state change (pressed state, color shift)
- Submit: loading indicator or optimistic UI update
- Error: inline message at the point of failure
- Success: brief visual confirmation (checkmark, state change) — not a dialog
If an operation takes longer than 300ms, show a loading state. If longer than 2 seconds, show a progress indicator with context ("Syncing contacts...").
Why: Delayed feedback makes users tap again, double-submit, or assume the app is broken.
UI-08: Escape Hatches Are Visible
Every state must have a clear, visible way back.
- Back buttons are always present in navigation stacks
- Cancel is always available during multi-step flows
- Undo appears immediately after destructive actions (see UI-01)
- No state requires a gesture (swipe, long-press) as the only way out — always provide a visible alternative
Why: Hidden escape hatches create anxiety. Users avoid taking actions when they're unsure they can get back.
Applying the Rules
For New Screens
Before implementing a new screen, answer:
- What is the one primary action on this screen? (UI-06)
- Are any actions truly irrevocable? List them. Everything else gets undo. (UI-01, UI-02)
- Can all editing happen inline? (UI-03)
- What is the minimum the user needs to see on first load? (UI-04)
For Existing Screens
When modifying an existing screen, check whether it violates any rule. If it does, fix the violation in a separate commit — don't mix guideline fixes with feature work.
For Code Review
Reviewers should check:
- No new modal dialogs for reversible actions
- Confirmation only for irrevocable actions, done inline
- Error and status messages appear inline
- Primary action is visually clear
- Every action has visible feedback
- Undo is available for destructive but reversible actions
Decision Record
These guidelines were adopted on 2026-02-24 based on:
- Analysis of current dialog patterns across desktop, Android, and iOS implementations
- NNG: Modal & Nonmodal Dialogs
- NNG: Confirmation Dialogs
- Apple HIG: Modality
- Material Design: Dialogs
- IxDF: Progressive Disclosure
- UX Planet: Confirmation Dialogs
UX Interaction Guidelines
Cross-platform interaction rules for all vauchi clients — smartphones, dumb-phones, smartwatches, tablets, laptops, desktops (Windows, macOS, Linux, Android, iOS).
These rules enforce Principle 4: Simplicity serves the user and Principle 2: Trust is earned in person. For component-level behavior (toasts, inline editing, confirmations), see the sibling document GUI Design Guidelines.
Philosophy
Four commitments shape every interaction in vauchi:
- Don't make me think — Every screen is self-explanatory. If a user pauses to figure out what to do, the design has failed.
- Keep the user informed — Always show what's happening, what just happened, and what comes next.
- Success paths are straight lines — Primary flows have no forks, no optional detours, no "you can also..." on the main screen.
- Simple views, few transitions — Each view does one thing well. Don't bounce users between screens when content can update in place.
These are not aspirations — they are constraints. Designs that violate them must be reworked.
The Rules
UX-01: Physical Device First
Prefer real-world device interaction over typing, pasting, or link sharing. Vauchi's trust model is built on physical proximity — the UX must reflect that.
| Scenario | Wrong | Right |
|---|---|---|
| Contact exchange (phone ↔ phone) | Copy a code, paste in app | Hold phones together, scan QR |
| Contact exchange (phone ↔ laptop) | Type a code on laptop | Point phone camera at laptop screen QR |
| NFC-capable devices | Share a link | Tap devices together |
| BLE proximity | Manual pairing flow | Automatic discovery, confirm on both devices |
On devices without cameras or NFC (some desktops, dumb-phones without camera, CLI): fall back to the simplest available method (display QR for the other device to scan, or paste a one-time code). The device with more hardware capabilities drives the interaction.
On smartwatches: the watch displays a QR code; the other person's phone scans it. Watches don't drive complex flows — they companion with the paired phone for setup and editing.
Why: Physical actions are faster, harder to phish, and align with Principle 2 (trust is earned in person). Copy-paste is error-prone and trains users to move secrets through clipboards.
UX-02: Zero-Instruction Screens
If a screen needs written instructions to be understood, redesign it. Labels, icons, layout, and context must be self-evident.
- Button labels state the action: "Add Contact", not "Submit"
- Icons have text labels on first encounter (icon-only after familiarity)
- Empty states explain what goes here and how to start: "No contacts yet. Scan a QR code to add one."
- Error states say what went wrong and what to do (see UX-07)
Exceptions: Security-critical screens (identity wipe, recovery) may include a short warning. This is a safety message, not an instruction.
Why: Users scan, they don't read. Instructions are ignored or cause hesitation. Self-evident design eliminates both problems.
UX-03: Show State, Not Chrome
Screen real estate belongs to the user's data and current status. Decorative elements, branding, and navigation chrome are subordinate.
- Contact list shows contacts, not app branding
- Exchange screen shows the camera viewfinder or QR code — not a toolbar and a sidebar
- Status indicators (syncing, connected, offline) are compact and contextual
- On small screens (phones, watches), chrome compresses or hides entirely during focused tasks
Why: Users open vauchi to do something with their contacts, not to admire the app. Every pixel of chrome competes with the content they came for.
UX-04: One Happy Path
Primary flows (onboarding, exchange, editing a contact) have exactly one path forward. Alternatives, advanced options, and edge cases are hidden until needed.
- Onboarding: one screen at a time, one action per screen, linear progression
- Exchange: scan → confirm → done. No "choose your method" screen unless the device supports multiple methods
- Settings: grouped by topic, expanded on demand (progressive disclosure per UI-04)
When multiple hardware methods exist (QR + NFC + BLE on a phone): auto-select the best available method. Show alternatives only on failure or explicit user request — not as a decision tree at the start.
Why: Every fork in the path is a decision point. Decisions cost attention. One clear path is faster and less stressful than three options with no clear winner.
UX-05: Progress Is Always Visible
Multi-step flows show where the user is, where they've been, and how many steps remain.
- Step indicators: "Step 2 of 4" or a progress bar — not just the current screen
- Completed steps show a checkmark or similar confirmation
- The final step clearly signals completion ("Your identity is ready", not just returning to a blank home screen)
- Long operations (sync, key generation) show a progress indicator with context: "Generating keys..." not just a spinner
On desktop and laptop: larger screens can show a step sidebar or breadcrumb trail. On phones, a compact progress bar or step counter at the top suffices.
Why: Users abandon flows when they can't tell how much is left. Visible progress reduces anxiety and drop-off.
UX-06: Transitions Are Earned
Don't navigate to a new screen when content can update in place. Screen changes are only for genuinely new contexts.
| Situation | Wrong | Right |
|---|---|---|
| Edit a contact field | Navigate to edit screen | Field becomes editable in place (UI-03) |
| Show exchange result | New "success" screen | Contact appears in list, toast confirms (UI-01) |
| Toggle a setting | Navigate to sub-page | Toggle updates in place |
| View contact details | New screen | Expand contact card in list (phone), or side panel (desktop/laptop) |
When a transition is appropriate: navigating to a genuinely different context (contact list → exchange camera), entering a multi-step flow (settings → identity wipe confirmation), or switching between top-level sections.
Desktop/laptop consideration: larger screens should use split views, side panels, and in-place expansion more aggressively. A phone might navigate to a contact detail screen; a laptop should show it alongside the list.
Why: Every screen transition resets the user's spatial context. Frequent transitions create disorientation and make the app feel heavier than it is.
UX-07: Errors Name the Fix
Every error message tells the user what happened and what they can do about it. Generic errors are forbidden.
| Wrong | Right |
|---|---|
| "Something went wrong" | "Camera permission denied. Open Settings → Privacy → Camera to allow vauchi." |
| "Network error" | "Can't reach the relay. Your changes are saved and will sync when you're back online." |
| "Invalid QR" | "This QR code isn't a vauchi contact card. Ask the other person to open vauchi and show their code." |
| "Exchange failed" | "Couldn't complete the exchange. Move closer and try again." |
Structure: [What happened]. [What to do about it]. — two sentences, no jargon.
Offline context: When offline, never show errors for things that will work later. Instead, show status: "Saved locally. Will sync when connected."
Why: An error without a fix is a dead end. Users can't solve "something went wrong" — they can solve "move closer and try again."
UX-08: Offline and In-Person First
QR exchange, contact viewing, and card editing work without network connectivity. The app never blocks on a network call for local operations.
- Exchange: QR generation and scanning are fully local. BLE/NFC are local. No server round-trip needed.
- Contact list and card details: always available from local storage
- Editing own card: immediate, local. Sync happens when connectivity returns.
- Sync status: shown as a subtle indicator, never as a blocking state
What requires connectivity: relay sync (pushing updates to contacts), recovery voucher upload, relay registration. These are background operations — never in the critical path of a user action.
On laptops and desktops: the same rules apply. A desktop user editing their card on a train without WiFi should have the same experience as someone on a connected phone.
Why: Vauchi's trust model is in-person. Two people standing next to each other should never see "Connecting..." when exchanging contacts. Local-first is both a UX and a security principle.
UX-09: Hardware Guides the Flow
When the device's hardware is active (camera, NFC, BLE), the hardware's output IS the primary UI. Don't cover it with chrome.
- Camera (QR scan): viewfinder fills the available space. A subtle frame or overlay guides alignment — nothing more.
- NFC: the system's NFC animation (iOS tap indicator, Android NFC dialog) is the feedback. The app shows a brief "Hold near the other device" prompt, then gets out of the way.
- BLE discovery: show a compact list of nearby devices as they appear. No full-screen takeover.
On devices without hardware (desktop without camera, CLI): clearly communicate the fallback. "Display this QR code on your screen — the other person scans it with their phone."
Desktop with camera: the same camera-fills-the-space rule applies. A laptop scanning a phone's QR should show the webcam feed prominently, not buried in a small widget.
Why: Hardware feedback is immediate and real. Overlaying it with app UI creates competition for attention. Let the camera be the camera.
UX-10: Reachability Drives Layout
Primary actions sit where the user can reach them without effort. On touch devices, this is the thumb zone. On desktop and laptop, this is the main content area and keyboard shortcuts.
Smartphones and tablets:
- Primary actions: bottom of screen, center or dominant-hand side
- Navigation: bottom tab bar or swipe gestures
- Destructive/rare actions: top of screen or behind a menu — require deliberate reach
- Minimum tap targets: 44×44pt (iOS) / 48×48dp (Android)
Smartwatches:
- One primary action per screen — crown or single tap to confirm
- Scrollable vertical list for navigation — no side menus or tabs
- Minimal text — icons and short labels only
- Destructive actions require crown press-and-hold or companion app
Dumb-phones (KaiOS):
- D-pad navigation — primary action on center/select key
- Soft keys at bottom for contextual actions (left = back, right = options)
- No gestures — every action reachable via key presses
Desktop and laptop (Windows, macOS, Linux):
- Primary actions: prominent buttons in the main content area, keyboard shortcuts for frequent actions
- Navigation: sidebar or top bar — persistent, not hidden behind a hamburger menu
- Destructive/rare actions: behind a menu or at the end of a settings list
- Keyboard accessibility: every action reachable without a mouse
Why: Frequent actions that are hard to reach feel heavy. Destructive actions that are easy to reach cause accidents. Layout should match action frequency and risk.
Applying the Rules
For New Flows
Before designing a new user flow, answer:
- Can this be done with a physical device action instead of typing? (UX-01)
- Does every screen explain itself without instructions? (UX-02)
- Is there one clear path forward? (UX-04)
- Does the user always know where they are in the flow? (UX-05)
- Can any screen transition be replaced with an in-place update? (UX-06)
- Does it work offline? If not, why not? (UX-08)
For Existing Flows
When modifying an existing flow, check whether it violates any rule. Fix violations in a separate commit — don't mix UX fixes with feature work (same as GUI guidelines).
For Code Review
Reviewers should check:
- Physical interaction preferred over manual input where hardware allows
- No screens require reading instructions to understand
- Primary flow has one path, no unnecessary forks
- Multi-step flows show progress
- Screen transitions only for genuinely new contexts
- Errors include what happened and what to do
- Core operations work offline
- Hardware UI (camera, NFC) not obscured by app chrome
- Primary actions in easy-reach zones per platform
Platform Adaptation
These rules apply to all platforms, but implementation adapts:
| Concept | Smartphone | Dumb-phone (KaiOS) | Smartwatch | Tablet | Laptop/Desktop | CLI/TUI |
|---|---|---|---|---|---|---|
| QR exchange | Camera viewfinder | Camera viewfinder | Display QR (no camera) | Camera viewfinder | Webcam or display QR for phone to scan | Display QR in terminal (ASCII/sixel) |
| NFC/BLE | Native hardware | NFC if available | NFC tap | Native hardware | USB NFC reader (if available) | Not applicable — QR fallback |
| Progress | Top progress bar | Step counter | Minimal step dots | Top progress bar | Step sidebar or breadcrumb | Step counter: [2/4] |
| Reachability | Thumb zone (bottom) | D-pad center/select | Crown/single button | Thumb zone (bottom) | Main content area + shortcuts | Command-line arguments |
| Inline editing | Tap to edit | Select to edit | Not applicable — voice or companion app | Tap to edit | Click to edit, Enter to save | Not applicable — command-based |
| Split views | Full-screen navigation | Full-screen navigation | Single view only | Side panel + list | Side panel + list | Not applicable |
Relationship to Other Documents
- GUI Design Guidelines: Component-level behavior — how toasts, inline confirmations, and inline editing work. UX guidelines say when to use them; GUI guidelines say how they behave.
- Principles: Philosophical foundation. UX guidelines are the practical application of Principles 2 (trust in person) and 4 (simplicity serves the user).
- ADR-022 (Core-driven UI): UX guidelines inform what
WorkflowEngineimplementations should produce; ADR-022 defines the mechanism. See the internal Architecture Decision Records for details.
Decision Record
These guidelines were adopted on 2026-03-10 based on:
- Analysis of vauchi's physical-first trust model and multi-platform architecture
- Steve Krug: Don't Make Me Think — self-evident design, scanning over reading
- Nielsen Norman Group: Visibility of System Status — keep users informed
- Interaction Design Foundation: Progressive Disclosure — one happy path
- Apple HIG: Layout — thumb zone and reachability
- Google Material Design: Navigation — transition frequency
- Smashing Magazine: Privacy UX — privacy-first interaction patterns
Diagrams
Sequence diagrams for core Vauchi flows.
Available Diagrams
| Diagram | Description |
|---|---|
| Contact Exchange | In-person QR code exchange |
| Device Linking | Multi-device setup |
| Sync Updates | How card updates propagate |
| Contact Recovery | Social recovery flow |
| Message Delivery | End-to-end message delivery flow |
| Crypto Hierarchy | Key derivation and storage hierarchy |
Reading These Diagrams
All diagrams use Mermaid sequence diagram notation:
- Solid arrows (
->>) = Synchronous request - Dashed arrows (
-->>) = Asynchronous/response - Notes = Context or explanation
- Participants = Entities involved in the flow
Interaction Types
Each diagram indicates the interaction type:
| Icon | Type | Meaning |
|---|---|---|
| 🤝 | IN-PERSON | Physical proximity required |
| ☁️ | REMOTE | Via relay server |
| 🔒 | ENCRYPTED | End-to-end encrypted |
Related Documentation
- Architecture Overview — System design
- Crypto Reference — Cryptographic details
Contact Exchange Sequence
Interaction Type: 🤝 IN-PERSON (Proximity Required)
Two users exchange contact cards by scanning QR codes while physically present together. Proximity is verified via ultrasonic audio handshake to prevent remote/screenshot attacks.
Participants
- Alice - User initiating exchange (displays QR)
- Alice's Device - Mobile/Desktop running Vauchi
- Bob - User completing exchange (scans QR)
- Bob's Device - Mobile/Desktop running Vauchi
- Relay - WebSocket relay server (fallback only)
Sequence Diagram
┌───────┐ ┌────────────────┐ ┌──────────────┐ ┌─────┐ ┌───────┐
│ Alice │ │ Alice's Device │ │ Bob's Device │ │ Bob │ │ Relay │
└───┬───┘ └────────┬───────┘ └───────┬──────┘ └──┬──┘ └───┬───┘
│ │ │ │ │
│ Tap "Share Contact" │ │ │ │
│────────────────────────▶ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Generate ephemeral X25519 keypair │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Create exchange token (expires 5 min) │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Generate audio challenge seed │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Encode QR: [public_key, token, audio_challenge] │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ Display QR code │ │ │ │
◀────────────────────────│ │ │ │
│ │ │ │ │
│ │ │ Open camera, scan QR │ │
│ │ ◀─────────────────────────│ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Decode QR data │ │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Validate token not expired │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Extract Alice's public key │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ┌───────────────────────────────────────────┐ │ │ │
│ │ │ PROXIMITY VERIFICATION (Ultrasonic Audio) │ │ │ │
│ │ └───────────────────────────────────────────┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Emit ultrasonic challenge (18-20 kHz) │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Detect ultrasonic challenge │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Sign challenge with Bob's key │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Emit ultrasonic response │
│ │ ◀───┘ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Detect and verify response │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Confirm proximity │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Confirm proximity │ │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ┌────────────────────┐ │ │ │
│ │ │ X3DH KEY AGREEMENT │ │ │ │
│ │ └────────────────────┘ │ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Generate ephemeral X25519 keypair
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ Send: [Bob's identity key, ephemeral key] │ │ │
│ ◀────────────────────────────────────────────────│ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ X3DH: Derive shared secret │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ │ Send: [Alice's identity key, ephemeral key] │ │ │
│ │────────────────────────────────────────────────▶ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ X3DH: Derive shared secret │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ┌───────────────────────────────────┐ │ │ │
│ │ │ Both have identical shared secret │ │ │ │
│ │ └───────────────────────────────────┘ │ │ │
│ │ │ │ │
│ │ ┌───────────────────────┐ │ │ │
│ │ │ CONTACT CARD EXCHANGE │ │ │ │
│ │ └───────────────────────┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Encrypt Alice's card with shared secret │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ │ Send encrypted contact card │ │ │
│ │────────────────────────────────────────────────▶ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Decrypt Alice's card│ │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Store Alice as contact │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Encrypt Bob's card with shared secret
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ Send encrypted contact card │ │ │
│ ◀────────────────────────────────────────────────│ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Decrypt Bob's card │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Store Bob as contact │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ Exchange Successful │ │ │ │
◀────────────────────────│ │ │ │
│ │ │ │ │
│ │ │ Exchange Successful │ │
│ │ │─────────────────────────▶ │
│ │ │ │ │
│ ┌───────────────────────────────────────────────────┐ │ │
│ ││Alice and Bob now have each other's contact cards │ │ │
│ └───────────────────────────────────────────────────┘ │ │
│ │ │ │ │
┌───┴───┐ ┌────────┴───────┐ ┌───────┴──────┐ ┌──┴──┐ ┌───┴───┐
│ Alice │ │ Alice's Device │ │ Bob's Device │ │ Bob │ │ Relay │
└───────┘ └────────────────┘ └──────────────┘ └─────┘ └───────┘
Data Exchanged
QR Code Contents
Binary format (v3) with WBEX magic
bytes:
WBEX (4 bytes magic)
version (1 byte) = 0x03
Ed25519 public key (32 bytes)
X25519 exchange key (32 bytes)
exchange token (32 bytes)
audio_challenge seed (16 bytes)
creation timestamp (8 bytes)
name_len (2 bytes, big-endian)
name (N bytes, UTF-8)
flags (1 byte, bitfield)
[if bit 0] relay_url_len (2 bytes) + relay_url (M bytes)
[if bit 1] relay_noise_pubkey (32 bytes)
signature (64 bytes, Ed25519 over all preceding fields)
Minimum size (empty name, no relay fields): 192 bytes.
Contact Card (Encrypted)
{
"display_name": "Alice Smith",
"fields": [
{"type": "phone", "label": "Mobile", "value": "+1-555-1234"},
{"type": "email", "label": "Personal", "value": "alice@example.com"}
],
"signature": "Ed25519 signature of card"
}
Security Properties
| Property | Mechanism |
|---|---|
| Proximity | Ultrasonic audio (18-20 kHz) |
| No MITM | X3DH with identity keys |
| Forward Secrecy | Ephemeral keys discarded |
| Replay Prevention | One-time token, 5-min expiry |
| Card Authenticity | Ed25519 signature |
Failure Scenarios
Proximity Verification Fails
┌────────────────┐ ┌──────────────┐
│ Alice's Device │ │ Bob's Device │
└────────┬───────┘ └───────┬──────┘
│ │
├───┐ │
│ │ Emit ultrasonic challenge
◀───┘ │
│ │
│ ├╌╌╌┐
│ │ │ No ultrasonic detected (too far)
│ ◀╌╌╌┘
│ │
│ ├───┐
│ │ │ Proximity verification FAILED
│ ◀───┘
│ │
│ ├───┐
│ │ │ Proximity verification failed
│ ◀───┘
│ │
┌───────────────────────────────────────┐
│ Exchange blocked - no cards exchanged │
└───────────────────────────────────────┘
│ │
┌────────┴───────┐ ┌───────┴──────┐
│ Alice's Device │ │ Bob's Device │
└────────────────┘ └──────────────┘
QR Code Expired
┌────────────────┐ ┌──────────────┐
│ Alice's Device │ │ Bob's Device │
└────────┬───────┘ └───────┬──────┘
│ │
│ ├───┐
│ │ │ Decode QR, check expiry
│ ◀───┘
│ │
│ ├───┐
│ │ │ Token expired
│ ◀───┘
│ │
│ ├───┐
│ │ │ QR code expired
│ ◀───┘
│ │
┌────────────────────────────┐
│ Alice must generate new QR │
└────────────────────────────┘
│ │
┌────────┴───────┐ ┌───────┴──────┐
│ Alice's Device │ │ Bob's Device │
└────────────────┘ └──────────────┘
Platform Variations
| Platform | Proximity Method | Fallback |
|---|---|---|
| iOS ↔ iOS | Ultrasonic | Manual confirm |
| Android ↔ Android | Ultrasonic | Manual confirm |
| iOS ↔ Android | Ultrasonic | Manual confirm |
| Desktop ↔ Mobile | N/A (no mic) | Manual confirm |
| Desktop ↔ Desktop | N/A | Manual confirm |
Related Features
- Device Linking - Similar QR flow for linking devices
- Sync Updates - How card updates propagate after exchange
Sync Updates Sequence
Interaction Type: ☁️ REMOTE (Via Relay)
Contact card changes propagate automatically to contacts via the relay network. All data is end-to-end encrypted - relays only see encrypted blobs.
Participants
- Alice - User updating their contact card
- Alice's Device - Device where change is made
- Alice's Other Device - Another linked device
- Relay - WebSocket relay server
- Bob's Device - Contact receiving the update
- Bob - Contact who will see the update
Sequence Diagram
┌───────┐ ┌────────────────┐ ┌────────────────┐ ┌───────┐ ┌──────────────┐ ┌─────┐
│ Alice │ │ Alice Device 1 │ │ Alice Device 2 │ │ Relay │ │ Bob's Device │ │ Bob │
└───┬───┘ └────────┬───────┘ └────────┬───────┘ └───┬───┘ └───────┬──────┘ └──┬──┘
│ │ │ │ │ │
│ Update phone: "555-1111" → "555-2222" │ │ │ │ │
│──────────────────────────────────────────▶ │ │ │ │
│ │ │ │ │ │
│ ├───┐ │ │ │ │
│ │ │ Update local contact card │ │ │
│ ◀───┘ │ │ │ │
│ │ │ │ │ │
│ ├───┐ │ │ │ │
│ │ │ Increment card version │ │ │
│ ◀───┘ │ │ │ │
│ │ │ │ │ │
│ ├───┐ │ │ │ │
│ │ │ Sign card with identity key │ │ │
│ ◀───┘ │ │ │ │
│ │ │ │ │ │
│ │ ┌─────────────────────┐ │ │ │
│ │ │ PUSH TO OWN DEVICES │ │ │ │
│ │ └─────────────────────┘ │ │ │
│ │ │ │ │ │
│ ├───┐ │ │ │ │
│ │ │ Encrypt update for Device 2 │ │ │
│ ◀───┘ │ │ │ │
│ │ │ │ │ │
│ │ Push encrypted update (for AD2) │ │ │
│ │─────────────────────────────────────────────────▶ │ │
│ │ │ │ │ │
│ │ │ Forward encrypted update │ │ │
│ │ ◀╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌│ │ │
│ │ │ │ │ │
│ │ ├───┐ │ │ │
│ │ │ │ Decrypt update │ │ │
│ │ ◀───┘ │ │ │
│ │ │ │ │ │
│ │ ├───┐ │ │ │
│ │ │ │ Apply change locally │ │ │
│ │ ◀───┘ │ │ │
│ │ │ │ │ │
│ │ ├───┐ │ │ │
│ │ │ │ Verify signature │ │ │
│ │ ◀───┘ │ │ │
│ │ │ │ │ │
│ │ ┌──────────────────┐ │ │ │
│ │ │ PUSH TO CONTACTS │ │ │ │
│ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ ├───┐ │ │ │ │
│ │ │ Check visibility rules │ │ │
│ ◀───┘ │ │ │ │
│ │ │ │ │ │
│ │ ┌─────────────────────┐ │ │ │
│ │ │ Phone visible to: │ │ │ │
│ │ │ - Bob │ │ │ │ │
│ │ │ - Carol │ │ │ │ │
│ │ │ - Dave (restricted) │ │ │ │
│ │ └─────────────────────┘ │ │ │
│ │ │ │ │ │
│ ├───┐ │ │ │ │
│ │ │ Encrypt delta for Bob (shared key) │ │ │
│ ◀───┘ │ │ │ │
│ │ │ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │ │
│ │ │ Delta: {field: "phone", value: "555-2222"} │ │ │ │
│ │ └────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ Push encrypted delta (for Bob) │ │ │
│ │─────────────────────────────────────────────────▶ │ │
│ │ │ │ │ │
│ │ │ ┌alt [Bob is Online]─────────────────────────────────────────────────────────────────┐
│ │ │ │ │ │ │ │
│ │ │ │ │ Forward encrypted delta │ │ │
│ │ │ │ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ ├───┐ │ │
│ │ │ │ │ │ │ Decrypt with Alice-Bob shared key │ │
│ │ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ ├───┐ │ │
│ │ │ │ │ │ │ Verify signature │ │
│ │ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ ├───┐ │ │
│ │ │ │ │ │ │ Update Alice's contact card locally │ │
│ │ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ Notification: "Alice updated contact info" │ │
│ │ │ │ │ │───────────────────────────────────────────────▶ │
│ │ │ │ │ │ │ │
│ │ │ ├[Bob is Offline]╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ │ │ │ │ │ │ │
│ │ │ │ ├───┐ │ │ │
│ │ │ │ │ │ Queue message for Bob │ │ │
│ │ │ │ ◀───┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ ┌───────────────────────────┐ │ │
│ │ │ │ │ │ Queued until Bob connects││ │ │
│ │ │ │ │ └───────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────────────┘
│ │ │ │ │ │
│ │ │ ┌opt [Bob was Offline]───────────────────────────────────────────────────────────────┐
│ │ │ │ │ │ │ │
│ │ │ │ │ Connect to relay │ │ │
│ │ │ │ ◀────────────────────────────│ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ Deliver queued messages │ │ │
│ │ │ │ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ ├───┐ │ │
│ │ │ │ │ │ │ Process Alice's update │ │
│ │ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ ├───┐ │ │
│ │ │ │ │ │ │ Update local contact card │ │
│ │ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ Alice updated contact info │ │
│ │ │ │ │ │───────────────────────────────────────────────▶ │
│ │ │ │ │ │ │ │
│ │ │ ┌───────────────────────────────────────┐ │ │ │
│ │ │ │ Bob now sees Alice's new phone number │ │ │ │
│ │ │ └───────────────────────────────────────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────────────┘
│ │ │ │ │ │
┌───┴───┐ ┌────────┴───────┐ ┌────────┴───────┐ ┌───┴───┐ ┌───────┴──────┐ ┌──┴──┐
│ Alice │ │ Alice Device 1 │ │ Alice Device 2 │ │ Relay │ │ Bob's Device │ │ Bob │
└───────┘ └────────────────┘ └────────────────┘ └───────┘ └──────────────┘ └─────┘
Visibility-Aware Sync
┌────────────────┐ ┌───────┐ ┌──────────────┐ ┌────────────────┐ ┌───────────────┐
│ Alice's Device │ │ Relay │ │ Bob's Device │ │ Carol's Device │ │ Dave's Device │
└────────┬───────┘ └───┬───┘ └───────┬──────┘ └────────┬───────┘ └───────┬───────┘
│ │ │ │ │
├───┐ │ │ │ │
│ │ Check visibility rules │ │ │ │
◀───┘ │ │ │ │
│ │ │ │ │
│ ┌───────────────────────────┐ │ │ │ │
│ │ Visibility: │ │ │ │ │
│ │ Bob: [name, phone, email] │ │ │ │ │
│ │ Carol: [name, email] │ │ │ │ │
│ │ Dave: [name only] │ │ │ │ │
│ └───────────────────────────┘ │ │ │ │
│ │ │ │ │
┌par [Send to contacts based on visibility]──────────────────────────────────────────────────────────────────────┐
│ │ │ │ │ │ │
│ │ To Bob: {phone: "555-2222"} │ │ │ │ │
│ │────────────────────────────────▶ │ │ │ │
│ │ │ │ │ │ │
│ │ │ Forward (Bob can see phone) │ │ │ │
│ │ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ │ │ │
│ │ │ │ │ │ │
│ │ ┌──────────────────────────────┐ │ │ │ │
│ │ │ Carol cannot see phone field │ │ │ │ │
│ │ └──────────────────────────────┘ │ │ │ │
│ │ │ │ │ │ │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ │ │ │ │ │ │
│ │ No update sent (field not visible) │ │ │ │
│ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ │ │
│ │ │ │ │ │ │
│ │ │ ┌─────────────────────────────┐ │ │ │
│ │ │ │ Dave cannot see phone field││ │ │ │
│ │ │ └─────────────────────────────┘ │ │ │
│ │ │ │ │ │ │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ │ │ │ │ │ │
│ │ │ No update sent (field not visible) │ │ │
│ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ │
│ │ │ │ │ │ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │ │ │ │
┌────────┴───────┐ ┌───┴───┐ ┌───────┴──────┐ ┌────────┴───────┐ ┌───────┴───────┐
│ Alice's Device │ │ Relay │ │ Bob's Device │ │ Carol's Device │ │ Dave's Device │
└────────────────┘ └───────┘ └──────────────┘ └────────────────┘ └───────────────┘
Offline Queue Handling
┌────────────────┐ ┌───────┐ ┌──────────────┐
│ Alice's Device │ │ Relay │ │ Bob's Device │
└────────┬───────┘ └───┬───┘ └───────┬──────┘
│ │ │
├───┐ │ │
│ │ Update 1: Change phone │
◀───┘ │ │
│ │ │
│ Queue for Bob │ │
│──────────────────▶ │
│ │ │
│ ├───┐ │
│ │ │ Store encrypted message
│ ◀───┘ │
│ │ │
├───┐ │ │
│ │ Update 2: Change email │
◀───┘ │ │
│ │ │
│ Queue for Bob │ │
│──────────────────▶ │
│ │ │
│ ├───┐ │
│ │ │ Store encrypted message
│ ◀───┘ │
│ │ │
├───┐ │ │
│ │ Update 3: Change phone again │
◀───┘ │ │
│ │ │
│ Queue for Bob │ │
│──────────────────▶ │
│ │ │
│ ├───┐ │
│ │ │ Store encrypted message
│ ◀───┘ │
│ │ │
│ │ Connect (back online) │
│ ◀──────────────────────────│
│ │ │
│ │ Deliver update 1 │
│ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶
│ │ │
│ │ Deliver update 2 │
│ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶
│ │ │
│ │ Deliver update 3 │
│ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶
│ │ │
│ │ ├───┐
│ │ │ │ Apply all updates in order
│ │ ◀───┘
│ │ │
│ │┌───────────────────────────────────────────────────┐
│ ││ Bob sees latest values after applying all updates │
│ │└───────────────────────────────────────────────────┘
│ │ │
┌────────┴───────┐ ┌───┴───┐ ┌───────┴──────┐
│ Alice's Device │ │ Relay │ │ Bob's Device │
└────────────────┘ └───────┘ └──────────────┘
Data Exchanged
Update Delta (Encrypted)
{
"type": "card_update",
"from": "Alice's public key",
"version": 6,
"timestamp": "2026-01-21T12:00:00Z",
"changes": [
{
"op": "update",
"path": "/fields/phone",
"value": "555-2222"
}
],
"signature": "Ed25519 signature"
}
Security Properties
| Property | Mechanism |
|---|---|
| End-to-End Encryption | Updates encrypted with per-contact shared keys |
| Relay Blindness | Relay sees only encrypted blobs, no metadata |
| Update Authenticity | Ed25519 signature on all updates |
| Replay Prevention | Monotonic version numbers + timestamps |
| Visibility Enforcement | Only visible fields sent to each contact |
Related Features
- Contact Exchange - How shared keys are established
- Device Linking - How devices sync with each other
Message Delivery Flow
Interaction Type: 🌐 REMOTE (Via Relay)
End-to-end message delivery from card update to acknowledgment.
Participants
- Alice - User sending card update
- Alice's Device - Source device
- Relay - WebSocket relay server
- Bob's Device - Recipient device
- Bob - Contact receiving update
Message Sizes & Frequency
| Message Type | Payload | Padded Size | Frequency |
|---|---|---|---|
| Card delta | 50-200 B | 256 B | 1-5/month |
| Full card | 500 B-2 KB | 1-4 KB | Initial only |
| Ack | 32-64 B | 256 B | Per message |
| Device sync | 100-500 B | 256 B-1 KB | Real-time |
Complete Delivery Flow
┌───────┐ ┌───────────────────┐ ┌───────────┐ ┌─────────────────┐ ┌─────┐
│ Alice │ │ Alice's Device 📱 │ │ Relay 🖥️ │ │ Bob's Device 📱 │ │ Bob │
└───┬───┘ └─────────┬─────────┘ └─────┬─────┘ └────────┬────────┘ └──┬──┘
│ │ │ │ │
│ Edit phone: "555-1111" → "555-2222" │ │ │ │
│────────────────────────────────────────▶ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Update local card │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Create CardDelta │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ │ ┌──────────────────────────────────────┐ │ │ │
│ │ │ Delta: ~100 bytes │ │ │ │
│ │ │ {"field":"phone","value":"555-2222"} │ │ │ │
│ │ └──────────────────────────────────────┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Check visibility: Bob can see phone? ✓ │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Ratchet send chain forward │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ │ ┌─────────────────────┐ │ │ │
│ │ │ Chain gen 42 → 43 │ │ │ │
│ │ │ Message key derived │ │ │ │
│ │ └─────────────────────┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Encrypt delta (XChaCha20-Poly1305) │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Pad to 256 bytes (bucket) │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Generate CEK, sign payload │ │ │
│ ◀───┘ │ │ │
│ │ │ │ │
│ │ ┌───────────────────┐ │ │ │
│ │ │ Final: ~256 bytes │ │ │ │
│ │ │ (v0x02 format) │ │ │ │
│ │ └───────────────────┘ │ │ │
│ │ │ │ │
│ │ EncryptedUpdate(recipient=Bob, blob=...) │ │ │
│ │─────────────────────────────────────────────▶ │ │
│ │ │ │ │
│ │ ┌───────────────────────────────┐ │ │ │
│ │ │ WebSocket frame: │ │ │ │
│ │ │ 4-byte length + JSON envelope │ │ │ │
│ │ └───────────────────────────────┘ │ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Validate recipient_id format │ │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Check quota (blobs < 1000, storage < 50MB)│ │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ ├───┐ │ │
│ │ │ │ Store blob indexed by recipient_id │ │
│ │ ◀───┘ │ │
│ │ │ │ │
│ │ Acknowledgment(status=Stored) │ │ │
│ ◀╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌│ │ │
│ │ │ │ │
│ │ │ ┌──────────────────────────────┐ │ │
│ │ │ │ Blob stored with 120-day TTL │ │ │
│ │ │ └──────────────────────────────┘ │ │
│ │ │ │ │
│ ┌alt [Bob is Online (Connected to Relay)]─────────────────────────────────────────────────────────────────────────────────────────────┐
│ │ │ │ │ │ │
│ │ │ │ Forward EncryptedUpdate │ │ │
│ │ │ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ │ │
│ │ │ │ │ │ │
│ │ │ │ ├───┐ │ │
│ │ │ │ │ │ Receive encrypted blob │ │
│ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │
│ │ │ │ ├───┐ │ │
│ │ │ │ │ │ Resolve anonymous sender ID │
│ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │
│ │ │ │ │ ┌────────────────────┐ │ │
│ │ │ │ │ │ Try each contact's │ │ │
│ │ │ │ │ │ shared key against │ │ │
│ │ │ │ │ │ anonymous_id │ │ │
│ │ │ │ │ └────────────────────┘ │ │
│ │ │ │ │ │ │
│ │ │ │ ├───┐ │ │
│ │ │ │ │ │ Found: Alice │ │
│ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │
│ │ │ │ ├───┐ │ │
│ │ │ │ │ │ Derive message key (chain gen │3)
│ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │
│ │ │ │ ├───┐ │ │
│ │ │ │ │ │ Decrypt payload │ │
│ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │
│ │ │ │ ├───┐ │ │
│ │ │ │ │ │ Remove padding │ │
│ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │
│ │ │ │ ├───┐ │ │
│ │ │ │ │ │ Verify Ed25519 signature │ │
│ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │
│ │ │ │ ├───┐ │ │
│ │ │ │ │ │ Update Alice's card locally │
│ │ │ │ ◀───┘ │ │
│ │ │ │ │ │ │
│ │ │ │ │ Alice updated contact info │ │
│ │ │ │ │───────────────────────────────▶ │
│ │ │ │ │ │ │
│ │ │ │ Acknowledgment(status=ReceivedByRecipient) │ │ │
│ │ │ ◀───────────────────────────────────────────────│ │ │
│ │ │ │ │ │ │
│ │ │ Forward Acknowledgment │ │ │ │
│ │ ◀╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌│ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌─────────────────────────┐ │ │ │ │
│ │ │ │ Sender notified if │ │ │ │ │
│ │ │ │ suppress_presence=false │ │ │ │ │
│ │ │ └─────────────────────────┘ │ │ │ │
│ │ │ │ │ │ │
│ │ ├───┐ │ │ │ │
│ │ │ │ Mark update as delivered │ │ │ │
│ │ ◀───┘ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ ┌───────────────────────┐ │ │ │
│ │ │ │ │ Blob queued for later │ │ │ │
│ │ │ │ └───────────────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌opt [Bob comes online later]───────────────────────────┐ │ │
│ │ │ │ │ │ │ │ │
│ ├[Bob is Offline]╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ │ │ │ │ │ │ │ │
│ │ │ │ │ Connect (Handshake) │ │ │ │
│ │ │ │ ◀───────────────────────────────────────────────│ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ Deliver queued blobs │ │ │ │
│ │ │ │ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ ├───│ │ │
│ │ │ │ │ │ │ Process all pending updates │
│ │ │ │ │ ◀───│ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ ├───│ │ │
│ │ │ │ │ │ │ Updates applied in order │ │
│ │ │ │ │ ◀───│ │ │
│ │ │ │ │ │ │ │ │
│ │ │ ┌────────────────────────────────┐ │ │ │ │
│ │ │ │ Bob now sees Alice's new phone │ │ │ │ │
│ │ │ └────────────────────────────────┘ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │ │ │ │
┌───┴───┐ ┌─────────┴─────────┐ ┌─────┴─────┐ ┌────────┴────────┐ ┌──┴──┐
│ Alice │ │ Alice's Device 📱 │ │ Relay 🖥️ │ │ Bob's Device 📱 │ │ Bob │
└───────┘ └───────────────────┘ └───────────┘ └─────────────────┘ └─────┘
Double Ratchet Message Flow
┌─────────────────┐ ┌───────────────┐
│ Alice's Ratchet │ │ Bob's Ratchet │
└────────┬────────┘ └───────┬───────┘
│ │
┌rect [rgb(240, 248, 255)]───────────────────────────┐
│ │ │ │
│ ├───┐ │ │
│ │ │ Ratchet send chain: gen 0 → 1 │ │
│ ◀───┘ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Derive message key (gen 0) │ │
│ ◀───┘ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Encrypt with message key │ │
│ ◀───┘ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Delete message key │ │
│ ◀───┘ │ │
│ │ │ │
│ │ [DH_pub, gen=0, idx=0] + ciphertext │ │
│ │────────────────────────────────────────────▶ │
│ │ │ │
│ │ ┌─────────────────────┐
│ │ │ RECEIVE (Message 1) │
│ │ └─────────────────────┘
│ │ │ │
└────────────────────────────────────────────────────┘
│ │
│ ┌rect [r┐
│ │ │ │
│ │ ├───│
│ │ │ │ Verify DH generation matches
│ │ ◀───│
│ │ │ │
│ │ ├───│
│ │ │ │ Ratchet receive chain: gen 0 → 1
│ │ ◀───│
│ │ │ │
│ │ ├───│
│ │ │ │ Derive message key (gen 0)
│ │ ◀───│
│ │ │ │
│ │ ├───│
│ │ │ │ Decrypt
│ │ ◀───│
│ │ │ │
│ │ ├───│
│ │ │ │ Delete message key
│ │ ◀───│
│ │ │ │
│ ┌──────────────────────────────────┐
│ │ SEND REPLY (triggers DH ratchet) │
│ └──────────────────────────────────┘
│ │ │ │
│ └───────┘
│ │
┌rect [rgb(255, 248, 240)]───────────────────────────┐
│ │ │ │
│ │ ├───│
│ │ │ │ Generate new ephemeral DH keypair
│ │ ◀───│
│ │ │ │
│ │ ├───│
│ │ │ │ DH ratchet: compute new root key
│ │ ◀───│
│ │ │ │
│ │ ├───│
│ │ │ │ Create new send chain
│ │ ◀───│
│ │ │ │
│ │ ├───│
│ │ │ │ Encrypt with new chain's key
│ │ ◀───│
│ │ │ │
│ │ [NEW_DH_pub, gen=1, idx=0] + ciphertext │ │
│ ◀────────────────────────────────────────────│ │
│ │ │ │
┌───────────────────────────────┐ │ │
│ RECEIVE (triggers DH ratchet) │ │ │
└───────────────────────────────┘ │ │
│ │ │ │
└────────────────────────────────────────────────────┘
│ │
┌rect [r┐ │
│ │ │ │
│ ├───│ │
│ │ │ Detect new DH public key │
│ ◀───│ │
│ │ │ │
│ ├───│ │
│ │ │ DH ratchet: compute matching root key │
│ ◀───│ │
│ │ │ │
│ ├───│ │
│ │ │ Create new receive chain │
│ ◀───│ │
│ │ │ │
│ ├───│ │
│ │ │ Decrypt with new chain's key │
│ ◀───│ │
│ │ │ │
└───────┘ │
│ │
┌────────┴────────┐ ┌───────┴───────┐
│ Alice's Ratchet │ │ Bob's Ratchet │
└─────────────────┘ └───────────────┘
Out-of-Order Message Handling
┌────────────────┐ ┌───────┐ ┌──────────────┐
│ Alice's Device │ │ Relay │ │ Bob's Device │
└────────┬───────┘ └───┬───┘ └───────┬──────┘
│ │ │
│ Message 1 (gen=0, idx=0) │ │
│─────────────────────────────▶ │
│ │ │
│ Message 2 (gen=0, idx=1) │ │
│─────────────────────────────▶ │
│ │ │
│ Message 3 (gen=0, idx=2) │ │
│─────────────────────────────▶ │
│ │ │
│ ┌────────────────┐ │
│ │ Network delays │ │
│ └────────────────┘ │
│ │ │
│ │ Message 3 arrives first │
│ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶
│ │ │
│ │ ├───┐
│ │ │ │ Expected idx=0, got idx=2
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Skip chain to idx=2
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Store skipped keys: [idx=0, idx=1]
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Decrypt message 3
│ │ ◀───┘
│ │ │
│ │ Message 1 arrives │
│ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶
│ │ │
│ │ ├───┐
│ │ │ │ Lookup skipped key for idx=0
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Found! Decrypt message 1
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Delete skipped key
│ │ ◀───┘
│ │ │
│ │ Message 2 arrives │
│ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶
│ │ │
│ │ ├───┐
│ │ │ │ Lookup skipped key for idx=1
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Found! Decrypt message 2
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Delete skipped key
│ │ ◀───┘
│ │ │
│ │ ┌─────────────────────────┐
│ │ │ All messages processed, │
│ │ │ skipped keys cleaned up │
│ │ └─────────────────────────┘
│ │ │
┌────────┴───────┐ ┌───┴───┐ ┌───────┴──────┐
│ Alice's Device │ │ Relay │ │ Bob's Device │
└────────────────┘ └───────┘ └──────────────┘
Relay Acknowledgment States
●───● ╭──────╮ ╭────────╮ ╭───────────────╮ ╭─────────────────────╮ ╔═══╗
│ │ │ │ │ │ │ │ │ │ ║ ║
│ │─Message─created►│ Sent ├Relay─confirms─storage─►│ Stored │ ├────Recipient─connected────►│ Delivered ├Recipient─acks─►│ ReceivedByRecipient ├────►║ ║
│ │ │ │ │ │ │ │ │ │ ║ ║
●───● ╰──────╯ ╰────┬───╯ ╰───────┬───────╯ ╰─────────────────────╯ ╚═══╝
│ │
│ Decrypt error
│ │
│ │
│ ▼
│ ╭───────────────╮ ╔═════════════════════╗
│ │ │ ║ ║
└────────────Quota─exceeded─────────────►│ Failed ├───────────────►║ ║
│ │ ║ ║
╰───────────────╯ ╚═════════════════════╝
Wire Protocol
Envelope Format
┌─────────────────────────────────────────────────────────────┐
│ MESSAGE ENVELOPE │
├─────────────────────────────────────────────────────────────┤
│ 4 bytes: Length (big-endian) │
│ JSON payload: │
│ { │
│ "version": 1, │
│ "message_id": "uuid", │
│ "timestamp": unix_secs, │
│ "payload": { ... } │
│ } │
└─────────────────────────────────────────────────────────────┘
EncryptedUpdate Payload
{
"type": "EncryptedUpdate",
"sender_id": "anonymous_id (hourly rotation)",
"recipient_id": "mailbox token (daily rotation)",
"ratchet_header": {
"dh_public": "[32 bytes] sender DH public key",
"dh_generation": 5,
"message_index": 10,
"previous_chain_length": 3
},
"ciphertext": "base64(encrypted_delta)"
}
Acknowledgment Payload
{
"type": "Acknowledgment",
"message_id": "original message uuid",
"status": "Stored|Delivered|Received|Failed",
"error": null
}
Timing Estimates
| Phase | Duration | Notes |
|---|---|---|
| Encryption + padding | 1-5 ms | XChaCha20 is fast |
| Network latency | 50-200 ms | Relay location |
| Relay storage | 1-10 ms | SQLite insert |
| Forward to recipient | 50-200 ms | If online |
| Decryption + verify | 1-5 ms | |
| Total (online) | 100-400 ms | End-to-end |
| Total (offline) | < 120 days | Until connect |
Related Features
- Contact Exchange
- How keys are established
- Sync Updates
- Multi-device sync
- Crypto Hierarchy
- Key derivation
Device Linking Sequence
Interaction Type: 🤝 IN-PERSON (Proximity Required)
User links a new device to their existing identity. The new device receives the master seed and syncs all data. A confirmation code and proximity verification prevent unauthorized remote linking.
Participants
- User - Person owning both devices
- Device A (Primary) - Existing device with identity
- Device B (New) - New device to be linked
Sequence Diagram
┌──────┐ ┌────────────────────┐ ┌────────────────┐
│ User │ │ Device A (Primary) │ │ Device B (New) │
└───┬──┘ └──────────┬─────────┘ └────────┬───────┘
│ │ │
│ Settings > Devices > Link New Device │ │
│──────────────────────────────────────────────────▶ │
│ │ │
│ ├───┐ │
│ │ │ Generate ephemeral link_key (32 bytes)
│ ◀───┘ │
│ │ │
│ ├───┐ │
│ │ │ Sign QR fields with identity Ed25519 key
│ ◀───┘ │
│ │ │
│ ├───┐ │
│ │ │ Create QR: WBDL | version | identity_pubkey | link_key | timestamp | signature
│ ◀───┘ │
│ │ │
│ Display QR code (expires in 5 minutes) │ │
◀──────────────────────────────────────────────────│ │
│ │ │
│ Link to Existing Identity │
│───────────────────────────────────────────────────────────────────────────────▶
│ │ │
│ │ ├───┐
│ │ │ │ Scan QR code from Device A
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Validate WBDL magic, version, signature, expiry
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Create DeviceLinkRequest (device_name, random nonce, timestamp)
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Encrypt request with link_key (ChaCha20-Poly1305)
│ │ ◀───┘
│ │ │
│ │ Send encrypted request │
│ ◀────────────────────────────│
│ │ │
│ ┌───────────────────────────────────────┐
│ │ CONFIRMATION & PROXIMITY VERIFICATION │
│ └───────────────────────────────────────┘
│ │ │
│ ├───┐ │
│ │ │ Decrypt request using link_key
│ ◀───┘ │
│ │ │
│ ├───┐ │
│ │ │ Derive confirmation code: HMAC-SHA256(link_key, nonce) → XXX-XXX
│ ◀───┘ │
│ │ │
│ Show: "Link device 'Device B'? Code: XXX-XXX" │ │
◀──────────────────────────────────────────────────│ │
│ │ │
│ │ ├───┐
│ │ │ │ Derive same confirmation code from link_key + nonce
│ │ ◀───┘
│ │ │
│ Show: "Confirmation code: XXX-XXX" │
◀───────────────────────────────────────────────────────────────────────────────│
│ │ │
├───┐ │ │
│ │ Verify codes match on both screens │ │
◀───┘ │ │
│ │ │
│ Confirm link │ │
│──────────────────────────────────────────────────▶ │
│ │ │
│ ├───┐ │
│ │ │ Set proximity verified │
│ ◀───┘ │
│ │ │
│ │ ┌───────────────────┐ │
│ │ │ IDENTITY TRANSFER │ │
│ │ └───────────────────┘ │
│ │ │
│ ├───┐ │
│ │ │ Derive new device keys from master_seed + device_index
│ ◀───┘ │
│ │ │
│ ├───┐ │
│ │ │ Add Device B to registry, re-sign
│ ◀───┘ │
│ │ │
│ ├───┐ │
│ │ │ Build response: master_seed + display_name + device_index + registry + sync_payload
│ ◀───┘ │
│ │ │
│ ├───┐ │
│ │ │ Encrypt response with link_key (ChaCha20-Poly1305)
│ ◀───┘ │
│ │ │
│ │ Send encrypted response │
│ │────────────────────────────▶
│ │ │
│ │ ├───┐
│ │ │ │ Decrypt response
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Extract master_seed, registry, sync_payload
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Derive own device keys from master_seed + device_index
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Store identity locally
│ │ ◀───┘
│ │ │
│ │ ├───┐
│ │ │ │ Apply sync payload (contacts, card)
│ │ ◀───┘
│ │ │
│ Device B linked successfully │ │
◀──────────────────────────────────────────────────│ │
│ │ │
│ Welcome back, [Your Name] │
◀───────────────────────────────────────────────────────────────────────────────│
│ │ │
│ ┌──────────────────────────────────────────┐
│ │ Both devices now share the same identity │
│ └──────────────────────────────────────────┘
│ │ │
┌───┴──┐ ┌──────────┴─────────┐ ┌────────┴───────┐
│ User │ │ Device A (Primary) │ │ Device B (New) │
└──────┘ └────────────────────┘ └────────────────┘
Data Exchanged
Link QR Code Contents
Binary format with WBDL magic bytes, base64-encoded for QR:
WBDL (4 bytes magic)
version (1 byte, currently 1)
identity_pubkey (32 bytes, Ed25519 public key)
link_key (32 bytes, random ephemeral key)
timestamp (8 bytes, big-endian u64 unix seconds)
signature (64 bytes, Ed25519 over all preceding fields)
─────────────────
Total: 141 bytes (before base64 encoding)
The QR expires after 300 seconds (5 minutes). Signature is verified by the new device using the embedded identity public key.
Confirmation Code
Derived independently by both devices:
HMAC-SHA256(link_key, request_nonce)
→ first 4 bytes as big-endian u32
→ modulo 1,000,000
→ formatted as XXX-XXX
Both devices display the same code. User verifies they match.
Proximity Challenge
For external proximity verification (NFC, ultrasonic, etc.):
HKDF(ikm=link_key, info="vauchi-device-link-proximity-v1", len=16)
→ 16-byte challenge
Both devices derive the same challenge from the shared link key.
DeviceLinkRequest (New → Existing)
Encrypted with ChaCha20-Poly1305 using link_key:
device_name_len (4 bytes, little-endian u32)
device_name (variable, UTF-8)
nonce (32 bytes, random)
timestamp (8 bytes, little-endian u64)
DeviceLinkResponse (Existing → New)
Encrypted with ChaCha20-Poly1305 using link_key:
master_seed (32 bytes, zeroized after use)
display_name_len (4 bytes, little-endian u32)
display_name (variable, UTF-8)
device_index (4 bytes, little-endian u32)
registry_json_len (4 bytes, little-endian u32)
registry_json (variable, signed DeviceRegistry)
sync_payload_len (4 bytes, little-endian u32)
sync_payload_json (variable, contacts + card)
Security Properties
| Property | Mechanism |
|---|---|
| Seed Encryption | ChaCha20-Poly1305 with ephemeral link_key |
| QR Authentication | Ed25519 signature over QR fields |
| Confirmation Code | HMAC-SHA256(link_key, nonce) displayed on both devices |
| Proximity Verification | HKDF-derived 16-byte challenge; enforced before confirm |
| Replay Prevention | Random 32-byte nonce in each request |
| Token Expiry | QR expires after 5 minutes |
| Registry Integrity | Ed25519 signature over version + device list |
| Memory Safety | Master seed zeroized on Drop |
| Device Limit | Maximum 10 devices per identity |
Numeric Code Fallback (No Camera)
┌──────┐ ┌──────────┐ ┌────────────────────────┐
│ User │ │ Device A │ │ Device B (Desktop/CLI) │
└───┬──┘ └─────┬────┘ └────────────┬───────────┘
│ │ │
│ Generate link code │ │
│───────────────────────────────▶ │
│ │ │
│ Show QR code + data string │ │
◀───────────────────────────────│ │
│ │ │
│ Link to Existing Identity │
│──────────────────────────────────────────────────────────────▶
│ │ │
│ Paste data string from Device A │
│──────────────────────────────────────────────────────────────▶
│ │ │
│ │ ├───┐
│ │ │ │ Parse WBDL data, validate signature + expiry
│ │ ◀───┘
│ │ │
│ ┌──────────────────────────────────────┐
│ │ Same confirmation code flow as above │
│ └──────────────────────────────────────┘
│ │ │
│ Code: XXX-XXX │ │
◀───────────────────────────────│ │
│ │ │
│ Code: XXX-XXX │
◀──────────────────────────────────────────────────────────────│
│ │ │
│ Confirm │ │
│───────────────────────────────▶ │
│ │ │
│ │ Encrypted identity bundle │
│ │──────────────────────────────▶
│ │ │
│ │ ├───┐
│ │ │ │ Complete linking
│ │ ◀───┘
│ │ │
┌───┴──┐ ┌─────┴────┐ ┌────────────┴───────────┐
│ User │ │ Device A │ │ Device B (Desktop/CLI) │
└──────┘ └──────────┘ └────────────────────────┘
Revoking a Device
┌──────┐ ┌──────────┐ ┌──────────┐ ┌───────┐
│ User │ │ Device A │ │ Device B │ │ Relay │
└───┬──┘ └─────┬────┘ └─────┬────┘ └───┬───┘
│ │ │ │
│ Settings > Devices > Revoke Device B │ │ │
│─────────────────────────────────────────▶ │ │
│ │ │ │
│ Confirm revocation │ │ │
│─────────────────────────────────────────▶ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Mark Device B as revoked in registry │
│ ◀───┘ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Re-sign registry with identity key │
│ ◀───┘ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Increment registry version │
│ ◀───┘ │ │
│ │ │ │
│ │ Push updated registry (encrypted) │
│ │────────────────────────────────────────────▶
│ │ │ │
│ │ │ Forward revocation │
│ │ ◀╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌│
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Receive revocation notice
│ │ ◀───┘ │
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Wipe all identity data
│ │ ◀───┘ │
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Return to welcome screen
│ │ ◀───┘ │
│ │ │ │
│ Device B revoked │ │ │
◀─────────────────────────────────────────│ │ │
│ │ │ │
│ This device has been unlinked │ │
◀──────────────────────────────────────────────────────────────│ │
│ │ │ │
┌───┴──┐ ┌─────┴────┐ ┌─────┴────┐ ┌───┴───┐
│ User │ │ Device A │ │ Device B │ │ Relay │
└──────┘ └──────────┘ └──────────┘ └───────┘
Platform Implementation Status
| Platform | Status | Notes |
|---|---|---|
| Core API | Complete | Full protocol with tests |
| CLI | Complete | 7 commands: list, link, join, complete, finish, revoke, info |
| Desktop (native) | Complete | Native UI (SwiftUI/GTK/Qt) with QR display, confirmation overlay |
| TUI | Complete | ratatui UI with QR overlay, vim-style navigation |
| iOS | Planned | Awaiting mobile bindings |
| Android | Planned | Awaiting mobile bindings |
Related Features
- Contact Exchange - Similar proximity verification
- Sync Updates - How changes sync between linked devices
- Contact Recovery - Recovery when all devices lost
Contact Recovery Sequence
Interaction Type: 🤝 + ☁️ MIXED (In-Person Vouching + Remote Distribution)
When a user loses all devices, they can recover their contact relationships through social vouching. Existing contacts vouch for the user in-person, and the recovery proof is distributed remotely via relay.
Participants
- Alice - User who lost their device
- Alice's New Device - Fresh install, new identity
- Bob, Charlie, Betty - Alice's contacts who will vouch
- John, David - Alice's contacts who will receive recovery proof
- Relay - WebSocket relay server
Overview
┌────────────────────────────────────────────────────────────────────┐
│ PHASE 1: Vouching (In-Person) │
│ │
│ │
│ ┌─────────────────────────────────┐ ┌─────────┐ ┌────────┐ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ Bob │ │ Charlie │ │ Betty │ │
│ │ Vouch │ │ Vouch │ │ Vouch │ │
│ │ │ │ │ │ │ │
│ └────────────────┬────────────────┘ └────┬────┘ └────┬───┘ │
│ │ │ │ │
│ │ │ │ │
│ ├───────────────────────────┴───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ │ │
│ │ │ │
│ │ Alice (new) │ │
│ │ Threshold: 3 vouchers │ │
│ │ │ │
│ └────────────────┬────────────────┘ │
│ │ │
└──────────────────┼─────────────────────────────────────────────────┘
│
│
│
┌──────────────────┼──────────────────────────────────────────────────────────────────────────────────┐
│ │ PHASE 2: Distribution (Remote) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ │ │
│ │ │ │
│ │ OHTTP Gateway │ │
│ │ (strips client IP) │ │
│ │ │ │
│ └─────────────────────────────────┘ │
│ │ │
│ │ │
│ ├───────────────────────────┬───────────────┬───────────────────────┐ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────┐ ┌─────────┐ ┌────────┐ ┌──────────────────────────┐ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ Relay │ │ John │ │ David │ │ Others │ │
│ │ Stores proof under hash(pk_old) │ │ Accept │ │ Verify │ │ Discover via relay query │ │
│ │ │ │ │ │ │ │ │ │
│ └─────────────────────────────────┘ └─────────┘ └────────┘ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────┘
Note: All client↔relay traffic in Phase 2 is routed through an OHTTP gateway per ADR-037. The detailed sequence diagrams below omit the gateway hop for clarity — they describe the protocol layer, not the transport. Operationally, the relay never sees client IP addresses; the gateway never sees request content.
Phase 1: In-Person Vouching
┌─────────────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌─────┐
│ Alice (Lost Device) │ │ Alice New Device │ │ Bob's Device │ │ Bob │
└──────────┬──────────┘ └─────────┬────────┘ └───────┬──────┘ └──┬──┘
│ │ │ │
│ Install Vauchi on new device │ │ │
│─────────────────────────────────▶ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Create new identity (pk_new) │
│ ◀───┘ │ │
│ │ │ │
│ I had identity pk_old │ │ │
│─────────────────────────────────▶ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Store recovery claim: pk_old → pk_new │
│ ◀───┘ │ │
│ │ │ │
│ Generate recovery QR │ │ │
│─────────────────────────────────▶ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Create recovery claim QR │
│ ◀───┘ │ │
│ │ │ │
│ │ ┌────────────────────────┐ │
│ │ │ QR contains: │ │
│ │ │ - type: recovery_claim │ │
│ │ │ - old_pk: pk_old │ │
│ │ │ - new_pk: pk_new │ │
│ │ │ - timestamp │ │
│ │ └────────────────────────┘ │
│ │ │ │
│ Display recovery QR │ │ │
◀─────────────────────────────────│ │ │
│ │ │ │
│ │ │ Scan Alice's recovery QR │
│ │ ◀─────────────────────────────────────│
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Decode recovery claim │
│ │ ◀───┘ │
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Lookup pk_old in contacts │
│ │ ◀───┘ │
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Found: "Alice" with pk_old │
│ │ ◀───┘ │
│ │ │ │
│ │ │ Alice claims device loss │
│ │ │─────────────────────────────────────▶
│ │ │ │
│ │ │ Show Alice's stored name & photo │
│ │ │─────────────────────────────────────▶
│ │ │ │
│ │ ┌──────────────────────────────────────────┐
│ │ │ Bob verifies Alice is physically present │
│ │ └──────────────────────────────────────────┘
│ │ │ │
│ │ │ Yes, this is Alice, I confirm │
│ │ ◀─────────────────────────────────────│
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Create voucher │
│ │ ◀───┘ │
│ │ │ │
│ │ │ ┌─────────────────────┐ │
│ │ │ │ Voucher: │ │
│ │ │ │ - old_pk │ │
│ │ │ │ - new_pk │ │
│ │ │ │ - voucher_pk (Bob) │ │
│ │ │ │ - timestamp │ │
│ │ │ │ - Ed25519 signature │ │
│ │ │ └─────────────────────┘ │
│ │ │ │
│ │ Send voucher to Alice │ │
│ ◀──────────────────────────│ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Store Bob's voucher (1 of 3) │
│ ◀───┘ │ │
│ │ │ │
│ Bob vouched for you (1/3) │ │ │
◀─────────────────────────────────│ │ │
│ │ │ │
│ ┌───────────────────────────────────────┐ │
│ │ Alice now has 1 voucher, needs 2 more │ │
│ └───────────────────────────────────────┘ │
│ │ │ │
┌──────────┴──────────┐ ┌─────────┴────────┐ ┌───────┴──────┐ ┌──┴──┐
│ Alice (Lost Device) │ │ Alice New Device │ │ Bob's Device │ │ Bob │
└─────────────────────┘ └──────────────────┘ └──────────────┘ └─────┘
Collecting Multiple Vouchers
┌──────────────────┐ ┌──────────────────┐ ┌────────────────┐
│ Alice New Device │ │ Charlie's Device │ │ Betty's Device │
└─────────┬────────┘ └─────────┬────────┘ └────────┬───────┘
│ │ │
┌rect [rgb(240, 248, 255)]────┐ │
│ │ │ │ │
│ │ Show recovery QR │ │ │
│ │─────────────────────▶ │ │
│ │ │ │ │
│ │ ├───│ │
│ │ │ │ Verify pk_old is contact "Alice"
│ │ ◀───│ │
│ │ │ │ │
│ │ Send voucher │ │ │
│ ◀─────────────────────│ │ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Store Charlie's vouc│er (2 of 3) │
│ ◀───┘ │ │ │
│ │ │ │ │
│ │ ┌───────────────────┐ │
│ │ │ Alice meets Betty │ │
│ │ └───────────────────┘ │
│ │ │ │ │
└─────────────────────────────┘ │
│ │ │
┌rect [rgb(240, 255, 240)]─────────────────────────┐
│ │ │ │ │
│ │ Show recovery QR │ │
│ │──────────────────────────────────────────▶ │
│ │ │ │ │
│ │ │ ├───│
│ │ │ │ │ Verify pk_old is contact "Alice"
│ │ │ ◀───│
│ │ │ │ │
│ │ Send voucher │ │
│ ◀──────────────────────────────────────────│ │
│ │ │ │ │
│ ├───┐ │ │ │
│ │ │ Store Betty's voucher (3 of 3) │ │
│ ◀───┘ │ │ │
│ │ │ │ │
┌─────────────────────────────────────┐ │ │
│ THRESHOLD MET: 3 vouchers collected │ │ │
└─────────────────────────────────────┘ │ │
│ │ │ │ │
└──────────────────────────────────────────────────┘
│ │ │
├───┐ │ │
│ │ Create recovery proof │
◀───┘ │ │
│ │ │
│ ┌───────────────────────────────────┐ │
│ │ Recovery Proof: │ │ │
│ │ - old_pk │ │ │
│ │ - new_pk │ │ │
│ │ - threshold: 3 │ │ │
│ │ - vouchers: [Bob, Charlie, Betty] │ │
│ └───────────────────────────────────┘ │
│ │ │
┌─────────┴────────┐ ┌─────────┴────────┐ ┌────────┴───────┐
│ Alice New Device │ │ Charlie's Device │ │ Betty's Device │
└──────────────────┘ └──────────────────┘ └────────────────┘
Phase 2: Remote Distribution
┌──────────────────┐ ┌───────┐ ┌───────────────┐ ┌────────────────┐
│ Alice New Device │ │ Relay │ │ John's Device │ │ David's Device │
└─────────┬────────┘ └───┬───┘ └───────┬───────┘ └────────┬───────┘
│ │ │ │
│ Upload recovery proof │ │ │
│──────────────────────────▶ │ │
│ │ │ │
│ ├───┐ │ │
│ │ │ Store under key: hash(pk_old) │ │
│ ◀───┘ │ │
│ │ │ │
│ Stored │ │ │
◀╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌│ │ │
│ │ │ │
│ ┌──────────────────────────┐ │ │
│ │ Proof stored for 90 days │ │ │
│ └──────────────────────────┘ │ │
│ │ │ │
│ │ Batch query for contact recovery proofs │ │
│ ◀────────────────────────────────────────────│ │
│ │ │ │
│ │ │ ┌──────────────────────────────────────────────────┐
│ │ │ │ Query: [hash(pk1), hash(pk2), hash(pk_old), ...] │
│ │ │ └──────────────────────────────────────────────────┘
│ │ │ │
│ │ Found proof for hash(pk_old) │ │
│ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ │
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Decode recovery proof
│ │ ◀───┘ │
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Verify: pk_old is contact "Alice"
│ │ ◀───┘ │
│ │ │ │
│ │ ├───┐ │
│ │ │ │ Check vouchers for mutual contacts
│ │ ◀───┘ │
│ │ │ │
│ │ ┌alt [Ha┐ │
│ │ │ │ │ │
│ │ │ ├───│ │
│ │ │ │ │ Bob is my contact
│ │ │ ◀───│ │
│ │ │ │ │ │
│ │ │ ├───│ │
│ │ │ │ │ Charlie is my contact
│ │ │ ◀───│ │
│ │ │ │ │ │
│ │ │ ├───│ │
│ │ │ │ │ 2 mutual vouchers ≥ threshold
│ │ │ ◀───│ │
│ │ │ │ │ │
│ │ │ ├───│ │
│ │ │ │ │ High confidence recovery
│ │ │ ◀───│ │
│ │ │ │ │ │
│ │ ├[No Mut┤ │
│ │ │ │ │ │
│ │ │ ├───│ │
│ │ │ │ │ No vouchers are my contacts
│ │ │ ◀───│ │
│ │ │ │ │ │
│ │ │ ├───│ │
│ │ │ │ │ Cannot verify - meet Alice in person
│ │ │ ◀───│ │
│ │ │ │ │ │
│ │ └───────┘ │
│ │ │ │
│ │ Query for recovery proofs│ │
│ ◀────────────────────────────────────────────────────────────────│
│ │ │ │
│ │ Found proof for Alice │ │
│ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶
│ │ │ │
│ │ │ ├───┐
│ │ │ │ │ Check vouchers: Bob, Charlie, Betty
│ │ │ ◀───┘
│ │ │ │
│ │ │ ├───┐
│ │ │ │ │ None are David's contacts
│ │ │ ◀───┘
│ │ │ │
│ │ │ ├───┐
│ │ │ │ │ Warning: Unknown vouchers
│ │ │ ◀───┘
│ │ │ │
│ │ │ ├───┐
│ │ │ │ │ Options: Meet in person / Verify another way / Accept anyway
│ │ │ ◀───┘
│ │ │ │
┌─────────┴────────┐ ┌───┴───┐ ┌───────┴───────┐ ┌────────┴───────┐
│ Alice New Device │ │ Relay │ │ John's Device │ │ David's Device │
└──────────────────┘ └───────┘ └───────────────┘ └────────────────┘
Data Structures
Recovery Claim QR
{
"type": "recovery_claim",
"old_pk": "Ed25519 public key (lost)",
"new_pk": "Ed25519 public key (new)",
"timestamp": "2026-01-21T10:00:00Z"
}
Voucher
{
"old_pk": "Alice's old public key",
"new_pk": "Alice's new public key",
"voucher_pk": "Bob's public key",
"timestamp": "2026-01-21T10:05:00Z",
"signature": "Ed25519 signature of above fields"
}
Recovery Proof
{
"old_pk": "Alice's old public key",
"new_pk": "Alice's new public key",
"threshold": 3,
"vouchers": [
{ /* Bob's voucher */ },
{ /* Charlie's voucher */ },
{ /* Betty's voucher */ }
],
"expires": "2026-04-21T10:00:00Z"
}
Security Properties
| Property | Mechanism |
|---|---|
| In-Person Vouching | Vouchers must physically verify the person |
| Threshold Security | Requires N vouchers (configurable, default 3) |
| Mutual Contact Verification | Recipients verify via contacts they trust |
| Relay Privacy | Relay stores proof under hash, learns nothing |
| Replay Prevention | Timestamps, signatures, 90-day expiry |
| Attack Detection | Conflicting claims trigger warnings |
Related Features
- Contact Exchange - Original key exchange
- Device Linking - Recovery not needed if devices linked
- Sync Updates - How reconnected contacts sync
Crypto Key Hierarchy
Visual documentation of Vauchi's cryptographic key hierarchy and derivation paths.
Master Hierarchy
┌────────────────────────────────────┐
│ Identity Creation │
│ │
│ │
│ ┌────────────────────────────────┐ │
│ │ │ │
│ │ │ │
│ │ Master Seed ├─┼───────────────────HKDF────────────┬──────────┐
│ │ (256-bit, CSPRNG) │ │ info='Vauchi_Exchange_Seed_v2' │
│ │ │ │ │ │
│ └────────────────┬───────────────┘ │ └──────────┼─────────────────────────HKDF─────────────────────────────────────┐
│ │ │ │ info='Vauchi_Shred_Key_v2' │
└──────────────────┼─────────────────┘ │ │
raw seed │ │
(Ed25519 requirement) │ │
│ │ │
┌──────────────────┼─────────────────┐ ┌────────────┼────────────┐ ┌───────────────────┼───────────────────────────────────────────────────────────────────────────────────┐
│ Signing│Keys │ │ Exchange Keys │ │ │ Shredding Hierarchy │
│ │ │ │ │ │ │ │ │
│ ▼ │ │ ▼ │ │ ▼ │
│ ┌────────────────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌───────────────────────────────────┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ Identity Signing Key │ │ │ │ Exchange Secret Key │ │ │ │ SMK ├──────────────────────HKDF───────────────────────┐ │
│ │ (Ed25519 secret) │ │ │ │ (X25519) │ │ │ │ (Shredding Master Key) │ info='Vauchi_FileKey_Key_v2' │ │
│ │ │ │ │ │ │ │ │ │ │ │ │
│ └────────────────┬───────────────┘ │ │ └──────────┬──────────┘ │ │ └─────────────────┬─────────────────┘ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ HKDF │ │
│ │ │ │ │ │ │ info='Vauchi_Storage_Key_v2' │ │
│ │ │ │ │ │ │ │ │ │
│ ▼ │ │ ▼ │ │ ▼ ▼ │
│ ┌────────────────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌───────────────────────────────────┐ ┌───────────────────────────┐ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ Identity Public Key │ │ │ │ Exchange Public Key │ │ ┌┄┄┤ SEK │ │ FKEK │ │
│ │ (Ed25519 public) │ │ │ │ (X25519) │ │ ┆│ │ (Storage Encryption Key) │ │ (File Key Encryption Key) │ │
│ │ │ │ │ │ │ │ ┆│ │ │ │ │ │
│ └────────────────────────────────┘ │ │ └─────────────────────┘ │ ┆│ └─────────────────┬─────────────────┘ └───────────────────────────┘ │
│ │ │ │ ┆│ ┆ │
└────────────────────────────────────┘ └─────────────────────────┘ ┆└───────────────────┆───────────────────────────────────────────────────────────────────────────────────┘
┆ encrypts
┆ ┆
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ ┆
┌──────────────encrypts─────────────────────────────────────────────────────────encrypts───────────────────────────────────────────────────────────────┆───────────────────┐
│ ┆ Per-Contact Keys ┆ │
│ ┆ ┆ ┆ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────┐ ┌─────────────────────┐ ┌───────────────────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ CEK (Contact 1) │ │ CEK (Contact 2) │ │ CEK (Contact N) │ │
│ │ random 256-bit │ │ random 256-bit │ │ random 256-bit │ │
│ │ │ │ │ │ │ │
│ └────────────────────────────────┘ └─────────────────────┘ └───────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Key Derivation Details
HKDF Convention
All HKDF derivations use standard RFC 5869 (documented as "DP-5"):
HKDF-SHA256:
- salt: None (zeros per RFC 5869 §2.2)
- ikm: master_seed (32 bytes, high-entropy input)
- info: domain string (e.g., "Vauchi_Exchange_Seed_v2")
- output: 32 bytes
This follows standard HKDF convention: high-entropy seed as IKM, no salt needed.
Key Sizes
| Key | Size | Algorithm |
|---|---|---|
| Master Seed | 256 bits | CSPRNG |
| Identity Signing | 32+64 bytes | Ed25519 (seed+keypair) |
| Exchange | 32 bytes | X25519 |
| SMK | 256 bits | HKDF-SHA256 |
| SEK | 256 bits | HKDF-SHA256 |
| FKEK | 256 bits | HKDF-SHA256 |
| CEK | 256 bits | CSPRNG |
Double Ratchet Key Hierarchy
┌────────────────────────────────┐
│ Initial Key Agreement (X3DH) │
│ │
│ │
│ ┌────────────────────────────┐ │
│ │ │ │
│ │ │ │
│ │ X3DH Shared Secret │ │
│ │ (32 bytes) │ │
│ │ │ │
│ └──────────────┬─────────────┘ │
│ │ │
└────────────────┼───────────────┘
HKDF
init
│
┌────────────────┼───────────────────────────────────────────────────────────────────────────────┐
│ │ Root Chain │
│ │ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ │ │
│ │ Root Key 0 │ │
│ │ │ │
│ └──────────────┬─────────────┘ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ │ │
│ │ DH(our_secret × their_pub) ├─────────────────┐ │
│ │ │ │ │
│ └──────────────┬─────────────┘ │ │
│ │ HKDF │
│ │ │ │
│ HKDF │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────┐ ┌─────────────────────┐ │
│ │ │ │ │ │
│ │ Root Key 1 ├───┬──┤ Send Chain Key 0 │ │
│ │ │ │ │ │ │
│ └────────────────────────────┘ │ └──────────┬──────────┘ │
│ │ │ │
│ │ HKDF │
│ HKDF────────────────┴──────CHAIN_KEY_INFO─────────────────────────┐ │
│ MESSAGE_KEY_INFO │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────┐ ┌─────────────────────┐ ┌────────────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ Message Key 0 │ ┌──┤ Send Chain Key 1 │ ┌──┤ DH(our_secret × their_pub) │ │
│ │ │ │ │ │ │ │ │ │
│ └────────────────────────────┘ │ └─────────────────────┘ │ └──────────────┬─────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ │ │ HKDF │
│ │ │ │ │
│ ┌─────────────────┘ ┌─────────────┘ │ │
│ HKDF HKDF │ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────┐ ┌─────────────────────┐ ┌────────────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ Message Key 1 │ │ Root Key 2 ├──┬──┤ Recv Chain Key 0 │ │
│ │ │ │ │ │ │ │ │
│ └────────────────────────────┘ └─────────────────────┘ │ └────────────────────────────┘ │
│ │ │
│ │ │
│ HKDF────────────────────────────HKDF────────────┴─────────────────┐ │
│ MESSAGE_KEY_INFO CHAIN_KEY_INFO │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────┐ ┌─────────────────────┐ ┌────────────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ Message Key 0 │ │ Recv Chain Key 1 │ │ Root Key N │ │
│ │ │ │ │ │ │ │
│ └────────────────────────────┘ └──────────┬──────────┘ └────────────────────────────┘ │
│ │ │
└────────────────────────────────────────────────┼───────────────────────────────────────────────┘
│
┌────────────────────────────────────────────────┼───────────────────────────────────────────────┐
│ Receive│Chain │
│ ┌────────────────────────────┐ │ │
│ │ │ │ │
│ │ Message Key 1 │◄─────HKDF───────┘ │
│ │ │ │
│ └────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
Device Key Derivation
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Master Identity │
│ │
│ │
│ ┌────────────────────────────┐ │
│ │ │ │
│ │ Master Seed │ │
│ │ │ │
│ └────────────────────────────┘ │
│ │ │
│ │ │
│ ├────────────────────────────┬─────────────────────────────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────┐ ┌────────────────┐ ┌───────────────────────────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ Device Index 0 ├──┐ │ Device Index 1 ├──┐ │ Device Index 2├─────┐ │ │
│ │ (Primary) │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ └──────────────┬─────────────┘ │ └────────────────┘ │ └───────────────────────────────────┴───────┴─┼─────HKDF(seed,─device_index=2)────────────────┬──────────────────────┐
│ │ │ │ │ │ │
└────────────────┼────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────┘ │ │
HKDF(seed, device_index=0) │ │ │ │
│ │ │ │ │
│ └───────────┐ └─────HKDF(seed,─device_index=1)───────┬──────────────────────┐ │ │
┌────────────────┼────────────────────────────┼─────────┐ ┌───────────────────────┼──────────────────────┼─────────┐ ┌─────────┼──────────────────────┼─────────┐
│ │ Device 0 Keys │ │ │ Device 1 Keys │ │ │ │ Device 2 Keys │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ ▼ ▼ │ │ ▼ ▼ │ │ ▼ ▼ │
│ ┌────────────────────────────┐ ┌────────────────┐ │ │ ┌───────────────────────────────────┬───────┬────────┐ │ │ ┌───────────────┐ ┌────────────────┐ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ Signing Key 0 │ │ Exchange Key 0 │ │ │ │ Signing Key 1 │ Exchange Key 1 │ │ │ │ Signing Key 2 │ │ Exchange Key 2 │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └────────────────────────────┘ └────────────────┘ │ │ └───────────────────────────────────┴───────┴────────┘ │ │ └───────────────┘ └────────────────┘ │
│ │ │ │ │ │
└───────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────┘ └──────────────────────────────────────────┘
Crypto-Shredding Paths
┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Destruction Targets │
│ │
│ │
│ ┌───────────────────────────────┐ ┌───────────────────────────┐ ┌───────────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ Destroy Seed │ │ Destroy SMK │ │ Destroy CEK │ │
│ │ │ │ │ │ │ │
│ └───────────────┬───────────────┘ └─────────────┬─────────────┘ └─────────────┬─────────────┘ │
│ │ │ │ │
└─────────────────┼───────────────────────────────────┼─────────────────────────────────┼───────────────┘
Complete identity destruction Storage shredding Per-contact shredding
│ │ │
│ │ │
┌─────────────────┼───────────────────────────────────┼─────────────────────────────────┼───────────────┐
│ │ Effect │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────┐ ┌───────────────────────────┐ ┌───────────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ All data unreadable │ │ All local data unreadable │ │ Single contact unreadable │ │
│ │ │ │ │ │ │ │
│ └───────────────────────────────┘ └───────────────────────────┘ └───────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────────────────────────────────────┘
Key Storage Locations
┌───────────────────────┬────────────────────────────────────────┐ ┌────────────────────────┐
│ Platform Keychain │ Memory Only │ │ SQLite Database │
│ │ │ │ │
│ │ │ │ │
│ ┌───────────────────┐ │ ┌───────────────────────┐ │ │ ┌────────────────────┐ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ SMK ├derive─on─boot─►│ SEK (derived at boot) │ │ ├─────encrypt/decrypt───┼►│ Data encrypted │ │
│ │ (encrypted) │ │ │ │ │ │ │ with SEK │ │
│ │ │ │ │ │ │ │ │ │ │
│ └───────────────────┘ │ └───────────┬───────────┘ │ │ └────────────────────┘ │
│ │ │ │ │ │
├───────────────────────┘ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ ┌───────────────────┐ │ │ │ ┌────────────────────┐ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ Message keys ├────────┐ ├─────────────┼─encrypt/decrypt─────────────┼►│ CEK encrypted │ │
│ │ (single use) │ │ │ │ │ │ with SEK │ │
│ │ │ │ │ │ │ │ │ │
│ └─delete─after─use──┘ │ │ │ │ └────────────────────┘ │
│ ▲ │ │ │ │ │
│ │ │ │ │ │ │
│ ├──────────────────┘ │ │ │ │
│ derive │ │ │ │
│ │ │ │ │ │
│ ┌─────────┴─────────┐ │ │ │ ┌────────────────────┐ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ Active chain keys │ └─────────────┼─encrypt/decrypt─────────────┼►│ Ratchet state │ │
│ │ │ │ │ │ encrypted with SEK │ │
│ │ │ │ │ │ │ │
│ └───────────────────┘ │ │ └────────────────────┘ │
│ │ │ │
└────────────────────────────────────────────────────────────────┘ └────────────────────────┘
Backup Key Derivation
┌────────────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────────────────┐
│ User Input │ │ Backup Contents │
│ │ │ │
│ │ │ │
│ ┌────────────────────┐ ┌─────────────┐ │ │ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ Password │ │ Random Salt │ │ │ │ Display Name │ │ Master Seed │ │ Device Index │ │ Device Name │ │
│ │ │ │ (16 bytes) │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └──────────┬─────────┘ └──────┬──────┘ │ │ └───────┬──────┘ └──────┬──────┘ └───────┬──────┘ └──────┬──────┘ │
│ │ │ │ │ │ │ │ │ │
└────────────┼──────────────────────┼────────┘ └─────────┼───────────────────┼────────────────────┼───────────────────┼────────┘
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
┌────────────┼───────────┐ │ │ │ │ │
│ Key Derivation │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ ▼ │ │ │ │ │ │
│ ┌────────────────────┐ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ Argon2id │◄┼──────────┘ │ │ │ │
│ │ m=64MB, t=3, p=4 │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └──────────┬─────────┘ │ │ │ │ │
│ │ │ │ │ │ │
└────────────┼───────────┘ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
┌────────────┼───────────┐ │ │ │ │
│ Output │ │ │ │ │
│ │ │ │ │ │ │
│ ▼ │ │ │ │ │
│ ┌────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ Backup Key │ │ ┌───────┘ ┌──────┘ ┌───────┘ ┌──────┘
│ │ (256 bits) │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └──────────┬─────────┘ │ │ │ │ │
│ │ │ │ │ │ │
│ XChaCha20-Poly1305 │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ ▼ │ │ │ │ │
│ ┌────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ Encrypted Backup │◄┼───────────────────────┴────────────────────┴───────────────────┴────────────────────┘
│ │ │ │
│ └────────────────────┘ │
│ │
└────────────────────────┘
Security Properties by Key
| Key | Fwd Secrecy | Break-in Rec. | Zeroized |
|---|---|---|---|
| Master Seed | N/A | No | Yes |
| Identity Signing | No | No | Yes |
| Exchange Key | No | No | Yes |
| SMK | No | No | Yes |
| SEK | No | No | Yes (mem) |
| CEK | Per-contact | N/A | Yes |
| Root Key | Via DH ratchet | Yes | Yes |
| Chain Key | Via sym ratchet | N/A | Yes |
| Message Key | Single-use | N/A | Yes |
Related Documentation
- Crypto Reference — Algorithm details
- Architecture Overview — System design
- Message Delivery Flow — Ratchet in action