Contact Discovery
How Zentalk enables users to find and connect with contacts while preserving privacy.
Overview
Traditional messaging platforms require phone numbers, email addresses, or centralized username registries to discover contacts. Zentalk takes a fundamentally different approach, enabling contact discovery without revealing personal identifiers or building a social graph on any server.
| Approach | Zentalk | Traditional Apps |
|---|---|---|
| Phone number required | No | Yes (Signal, WhatsApp) |
| Email required | No | Sometimes (Telegram) |
| Address book upload | Never | Common practice |
| Server knows contacts | No | Yes |
| Centralized registry | No | Yes |
Discovery Methods
Direct Address Sharing
The primary method for contact discovery is direct sharing of wallet addresses through secure channels.
QR Code Sharing
Users can generate a QR code containing their contact information:
| QR Code Contents | Description |
|---|---|
| Wallet address | Primary identifier (zk1...) |
| Display name (optional) | Human-readable name |
| Identity key fingerprint | For verification |
| Protocol version | Compatibility indicator |
QR Code Payload Structure:
zentalk://contact?
addr=zk1qxy7f8h9j2k3m4n5p6q7r8s9t0u1v2w3x4y5z6
&name=Alice
&fp=37291847561928304857
&v=1Link Sharing
For digital sharing, users can copy a contact link:
https://zentalk.app/add/zk1qxy7f8h9j2k3m4n5p6q7r8s9t0u1v2w3x4y5z6| Sharing Method | Security Level | Use Case |
|---|---|---|
| In-person QR scan | Highest | Face-to-face meetings |
| Secure messenger link | High | Already trusted channel |
| Email link | Medium | Business contacts |
| Social media | Lower | Public profile sharing |
| NFC tap | High | Quick in-person exchange |
Username Lookup (Optional)
Zentalk supports optional username registration for discoverability.
How Username Registration Works
| Step | Action | Privacy Consideration |
|---|---|---|
| 1 | User chooses username | Not linked to real identity |
| 2 | Sign username with identity key | Proves ownership |
| 3 | Submit to DHT | Decentralized storage |
| 4 | Username maps to wallet address | Public mapping |
Username Properties
| Property | Value |
|---|---|
| Format | 3-32 alphanumeric characters |
| Case sensitivity | Case-insensitive (stored lowercase) |
| Uniqueness | Globally unique |
| Registration | One per wallet address |
| Revocation | Possible with identity key signature |
| Expiration | None (permanent until revoked) |
Username Registration Flow:
1. User selects username: "alice_z"
2. Client creates registration message:
{
username: "alice_z",
wallet_address: "zk1...",
timestamp: 1704067200,
signature: sign(identity_key, username || address || timestamp)
}
3. Submit to DHT with key = hash("zentalk:username:" || "alice_z")
4. DHT stores mapping, verifiable by anyoneUsername Lookup Process
| Step | Action | Data Exposed |
|---|---|---|
| 1 | User searches for “alice_z” | Username only |
| 2 | Query DHT for hash(“zentalk:username:alice_z”) | None (hashed lookup) |
| 3 | Retrieve wallet address | Public mapping |
| 4 | Fetch identity key bundle | Standard key exchange |
No Phone/Email Required
Zentalk deliberately avoids phone number or email-based discovery.
| Problem Avoided | Description |
|---|---|
| SIM swap attacks | No phone number means no SIM vulnerability |
| Carrier surveillance | No telecom metadata |
| Email tracking | No email provider involvement |
| Real identity linkage | No government-issued identifiers |
| Contact graph leakage | No address book analysis |
Privacy-Preserving Contact Sync
Why NOT to Upload Address Book
Many messaging apps request access to your address book and upload contacts to their servers. This creates serious privacy problems.
What Address Book Upload Reveals
| Data Exposed | Privacy Impact |
|---|---|
| All contact phone numbers | Server knows your entire social network |
| Contact names | Real names linked to numbers |
| Relationship patterns | Who you know, how you organize them |
| Non-user contacts | Privacy of people who never consented |
| Contact frequency | Metadata about communication patterns |
Attack Scenarios from Contact Upload
| Attack | Description |
|---|---|
| Social graph analysis | Map relationships between all users |
| De-anonymization | Link pseudonymous accounts via mutual contacts |
| Surveillance | Government requests for contact lists |
| Data breach | Stolen contact databases |
| Advertising profiles | Sell social graph to advertisers |
Zentalk’s Position
Zentalk never requests or uploads address book data. Contact discovery is purely opt-in and user-initiated.
Hash-Based Private Set Intersection (Theoretical)
While Zentalk does not implement contact sync, this section explains how privacy-preserving contact discovery could work using cryptographic techniques, for educational purposes.
Private Set Intersection (PSI) Overview
PSI allows two parties to find common elements in their sets without revealing non-matching elements.
| Participant | Input | Learns |
|---|---|---|
| Client | Own contact list | Which contacts use Zentalk |
| Server | All user identifiers | Nothing about client’s contacts |
Hash-Based PSI Protocol
Simplified PSI Protocol:
Client:
1. contacts = ["alice@email.com", "bob@email.com", ...]
2. hashed_contacts = [H(c || client_salt) for c in contacts]
3. blind hashed_contacts with random values
4. send blinded_hashes to server
Server:
1. users = [all registered user identifiers]
2. hashed_users = [H(u || server_salt) for u in users]
3. perform PSI computation on blinded_hashes
4. return blinded results
Client:
1. unblind results
2. compare with own hashed_contacts
3. matches = contacts that are Zentalk usersWhat Server Learns: Nothing
| Information | Server Knowledge |
|---|---|
| Number of contacts | Hidden by padding |
| Contact identifiers | Hidden by hashing + blinding |
| Which contacts matched | Hidden by blinding |
| Non-matching contacts | Never revealed |
Why Zentalk Doesn’t Implement PSI
| Reason | Explanation |
|---|---|
| Philosophy | Zentalk avoids phone/email identifiers entirely |
| Trust model | Any server interaction is potential metadata |
| Simplicity | Direct address sharing is simpler and safer |
| User control | Manual contact addition is more intentional |
Adding Contacts
Scanning QR Code
The recommended method for adding contacts in person.
QR Scan Process
| Step | Action | Verification |
|---|---|---|
| 1 | Open QR scanner | Camera permission required |
| 2 | Scan contact’s QR code | Parse zentalk:// URI |
| 3 | Display contact info | Show name, address preview |
| 4 | Verify fingerprint (optional) | Compare with spoken fingerprint |
| 5 | Confirm addition | User approves |
| 6 | Send contact request | Encrypted request message |
QR Code Parsing:
INPUT: Camera captures QR code image
OUTPUT: Contact information structure
1. Decode QR code to string
2. Parse URI: zentalk://contact?addr=...&name=...&fp=...
3. Validate wallet address format
4. Validate fingerprint format (if present)
5. Display to user for confirmationSecurity Considerations
| Risk | Mitigation |
|---|---|
| Malicious QR code | Validate address format strictly |
| Phishing QR code | Show address clearly before adding |
| Camera hijacking | Request camera permission only when needed |
| QR code substitution | Verify fingerprint verbally |
Entering Wallet Address
For contacts shared via text or other digital means.
Manual Entry Process
| Step | Action | Validation |
|---|---|---|
| 1 | Open “Add Contact” | Navigate to contact addition |
| 2 | Enter wallet address | zk1qxy7f8... |
| 3 | Validate format | Check prefix, length, checksum |
| 4 | Lookup identity key | Query DHT for key bundle |
| 5 | Display contact info | Show available metadata |
| 6 | Confirm addition | User approves |
Address Validation
| Check | Description | Error Message |
|---|---|---|
| Prefix | Must start with zk1 | ”Invalid address format” |
| Length | 42 characters total | ”Address too short/long” |
| Checksum | Bech32 checksum valid | ”Invalid checksum” |
| Characters | Valid Bech32 alphabet | ”Invalid characters” |
Address Validation:
FUNCTION validate_zentalk_address(address):
IF NOT address.startswith("zk1"):
RETURN error("Invalid prefix")
IF length(address) != 42:
RETURN error("Invalid length")
IF NOT bech32_checksum_valid(address):
RETURN error("Checksum failed")
IF NOT all_valid_bech32_chars(address):
RETURN error("Invalid characters")
RETURN successAccepting Contact Request
When someone adds you, you receive a contact request.
Contact Request UI Flow
| State | User Action | System Response |
|---|---|---|
| Request received | Notification shown | ”New contact request from zk1…” |
| View request | Open request details | Show sender address, message |
| Accept | Tap “Accept” | Add to contacts, notify sender |
| Reject | Tap “Reject” | Silently discard |
| Block | Tap “Block” | Add to block list |
Request Decision Factors
| Information Available | Use |
|---|---|
| Wallet address | Recognize if previously known |
| Display name | Claimed name (unverified) |
| Optional message | Context for request |
| Mutual contacts | ”Known by Alice, Bob” (if enabled) |
Verification Prompt
After adding a contact, Zentalk prompts for verification.
| Prompt Type | Trigger | User Action |
|---|---|---|
| First message | Before sending first message | Option to verify safety number |
| Unverified warning | Sending to unverified contact | Dismiss or verify |
| Key change alert | Contact’s key changed | Re-verify required |
Verification Prompt Flow:
WHEN user sends first message to unverified contact:
SHOW dialog:
"You haven't verified [Contact Name]'s identity.
For sensitive conversations, compare safety numbers
in person or via a trusted channel.
[Verify Now] [Send Anyway] [Cancel]"Contact Requests
Request Format
Contact requests are encrypted messages with a specific structure.
Request Message Structure
| Field | Type | Description |
|---|---|---|
| type | uint8 | Message type (0x01 = contact request) |
| sender_address | bytes | Sender’s wallet address |
| sender_name | string | Optional display name |
| message | string | Optional introduction message |
| timestamp | uint64 | Unix timestamp |
| pow_nonce | uint64 | Proof-of-work nonce |
| signature | bytes | Ed25519 signature |
Contact Request Wire Format:
+--------+----------------+-------------+-------------+
| type | sender_address | sender_name | message |
| 1 byte | 32 bytes | var (≤64) | var (≤256) |
+--------+----------------+-------------+-------------+
| timestamp | pow_nonce | signature |
| 8 bytes | 8 bytes | 64 bytes |
+------------------+------------------------------------+Encryption of Contact Request
| Layer | Encryption | Purpose |
|---|---|---|
| Transport | TLS 1.3 | Server cannot read |
| Message | X3DH + AES-GCM | Recipient-only decryption |
| Onion | 3-hop relay routing | Hide sender IP |
Spam Prevention
Contact requests include anti-spam mechanisms.
Proof-of-Work Requirement
| Parameter | Value | Rationale |
|---|---|---|
| Algorithm | SHA-256 | Widely supported |
| Difficulty | 20 leading zero bits | ~1 second on average |
| Target adjustment | None (fixed) | Simplicity |
| Scope | Per-request | Fresh PoW each time |
Proof-of-Work Verification:
FUNCTION verify_contact_request_pow(request):
challenge = hash(
request.sender_address ||
request.recipient_address ||
request.timestamp ||
request.pow_nonce
)
// Check for 20 leading zero bits
IF leading_zeros(challenge) ≥ 20:
RETURN valid
ELSE:
RETURN invalidPoW Difficulty Analysis
| Device | Time to Compute | Requests/Day Feasible |
|---|---|---|
| Modern smartphone | ~1 second | ~86,400 |
| Desktop computer | ~0.5 seconds | ~172,800 |
| Spammer (GPU) | ~0.01 seconds | ~8,640,000 |
| Spammer (ASIC) | Impractical (not SHA-256 mining) | N/A |
Rate Limiting
| Limit Type | Threshold | Response |
|---|---|---|
| Per-sender | 10 requests/hour | Reject additional |
| Per-recipient | 100 requests/hour | Queue overflow |
| Global | Network-level limits | DHT rate limiting |
Combined Spam Protection
| Layer | Protection | Bypass Difficulty |
|---|---|---|
| PoW | Computational cost | GPU/ASIC investment |
| Rate limiting | Temporal cost | Multiple identities |
| Client filtering | Local rules | None (client control) |
| Block lists | Permanent exclusion | New identity creation |
Accept/Reject/Block Options
Users have full control over incoming contact requests.
Action Comparison
| Action | Effect on Sender | Effect on Recipient | Reversible |
|---|---|---|---|
| Accept | Notified of acceptance | Added to contacts | Yes (remove contact) |
| Reject | No notification | Request discarded | N/A |
| Block | No notification | All future requests ignored | Yes (unblock) |
Block Persistence
| Scope | Duration | Cleared By |
|---|---|---|
| Session | Until app restart | App restart |
| Permanent | Until manual unblock | User action |
| Cross-device | Synced via encrypted storage | Sync to all devices |
Request Expiration
Contact requests have a limited lifetime.
| Parameter | Value | Rationale |
|---|---|---|
| Default expiration | 7 days | Balance availability and storage |
| Maximum extension | 30 days | Prevent indefinite storage |
| Sender notification | None | Privacy (don’t confirm existence) |
| Storage location | Recipient’s pending queue | Encrypted, local |
Request Lifecycle:
1. Sender creates request with timestamp T
2. Request encrypted and sent via mesh network
3. Recipient's device stores in pending queue
4. After 7 days: request auto-deleted if not acted upon
5. No notification to sender (prevents existence confirmation)Contact Verification
Safety Number Comparison
Safety numbers provide a cryptographic proof that you’re communicating with the intended person.
Safety Number Generation
| Input | Description |
|---|---|
| Your identity key | Your Ed25519 public key |
| Contact’s identity key | Their Ed25519 public key |
| Domain separator | ”ZentalkSafetyNumber” |
Safety Number Derivation:
FUNCTION generate_safety_number(my_identity_key, their_identity_key):
// Sort keys for consistency (both parties see same number)
IF my_identity_key < their_identity_key:
combined = my_identity_key || their_identity_key
ELSE:
combined = their_identity_key || my_identity_key
// Derive safety number
hash = SHA-256(combined || "ZentalkSafetyNumber")
// Format as readable number
safety_number = format_as_numeric_blocks(hash)
RETURN safety_number // e.g., "37291 84756 19283 04857 29184 73829"Safety Number Properties
| Property | Description |
|---|---|
| Unique per pair | Different for Alice-Bob vs Alice-Carol |
| Order independent | Alice sees same number as Bob |
| 60 digits | High collision resistance |
| Numeric only | Easy to read aloud |
Verification Methods
| Method | Security | Convenience | Recommended For |
|---|---|---|---|
| In-person comparison | Highest | Low | High-risk contacts |
| Video call | High | Medium | Remote verification |
| Phone call | Medium | Medium | Voice-familiar contacts |
| Trusted intermediary | Medium | Medium | Mutual contacts vouch |
| Screenshot exchange | Lower | High | Low-risk contacts |
In-Person QR Verification
The most secure verification method involves scanning QR codes in person.
QR Verification Flow
| Step | Alice | Bob |
|---|---|---|
| 1 | Opens verification screen | Opens verification screen |
| 2 | Shows QR code | Scans Alice’s QR code |
| 3 | Scans Bob’s QR code | Shows QR code |
| 4 | Both see “Verified” | Both see “Verified” |
QR Verification Code Contents
| Field | Purpose |
|---|---|
| Identity key | Full public key for verification |
| Safety number | Redundant check |
| Signature | Proves QR generated by key owner |
QR Verification Process:
ALICE'S DEVICE:
qr_content = {
identity_key: alice_identity_public_key,
safety_number: compute_safety_number(alice, bob),
timestamp: current_time(),
signature: sign(alice_identity_private_key, above_fields)
}
DISPLAY qr_code(qr_content)
BOB'S DEVICE:
scanned = scan_qr_code()
VERIFY signature with scanned.identity_key
VERIFY safety_number matches locally computed value
VERIFY timestamp is recent (within 5 minutes)
IF all valid:
MARK alice as verified
DISPLAY "Verification successful"Verified Badge Display
Verified contacts are visually distinguished in the UI.
Verification States
| State | Badge | Meaning |
|---|---|---|
| Unverified | None | Never verified |
| Verified | Checkmark | Safety numbers confirmed |
| Verification expired | Warning | Key changed since verification |
| Re-verification needed | Alert | Must re-verify |
Badge Display Locations
| Location | Display |
|---|---|
| Contact list | Small badge next to name |
| Conversation header | Badge with verification date |
| Contact details | Full verification status |
| Message input | Warning if unverified |
Re-Verification After Key Change
When a contact’s identity key changes, previous verification is invalidated.
Key Change Detection
| Trigger | Action |
|---|---|
| New identity key received | Compare with stored key |
| Mismatch detected | Alert user immediately |
| Session paused | Cannot send until acknowledged |
Key Change Alert
Key Change Warning Dialog:
"Security Alert: [Contact Name]'s identity key has changed.
This could mean:
- They reinstalled Zentalk
- They switched to a new device
- They restored from backup
- Someone is intercepting your messages
Your previous verification is no longer valid.
[View New Safety Number] [Block Contact] [Continue Unverified]"Re-Verification Requirement
| Scenario | Previous Status | New Status | Required Action |
|---|---|---|---|
| Key change | Verified | Unverified | Re-verify to restore |
| Key change | Unverified | Unverified | Acknowledge warning |
| Device addition | Verified | Verified | No change (multi-device) |
| Recovery | Verified | Unverified | Re-verify (new key) |
Contact Storage
Encrypted Contact List
Contacts are stored locally with full encryption.
Contact Record Structure
| Field | Encrypted | Indexed | Description |
|---|---|---|---|
| contact_id | No | Yes | Unique identifier |
| wallet_address | Yes | Hashed | Contact’s address |
| display_name | Yes | No | User-assigned name |
| nickname | Yes | No | Optional nickname |
| notes | Yes | No | User notes |
| identity_key | Yes | No | Their public key |
| verified_at | Yes | No | Verification timestamp |
| added_at | No | Yes | When contact was added |
| last_message_at | No | Yes | For sorting |
Contact Encryption
Contact Storage Encryption:
FUNCTION store_contact(contact, metadata_storage_key):
// Derive per-contact key
contact_key = HKDF(
ikm = metadata_storage_key,
salt = contact.contact_id,
info = "zentalk-contact-encrypt"
)
// Encrypt sensitive fields
encrypted_data = AES_GCM_Encrypt(
key = contact_key,
plaintext = serialize(
wallet_address,
display_name,
nickname,
notes,
identity_key,
verified_at
),
aad = contact.contact_id
)
// Store with plaintext indexes
IndexedDB.put("contacts", {
contact_id: contact.contact_id,
added_at: contact.added_at,
last_message_at: contact.last_message_at,
address_hash: hash(contact.wallet_address),
encrypted_data: encrypted_data
})Stored on Mesh (Not Server)
Contact lists are stored on the decentralized mesh network, not on central servers.
Mesh Storage Architecture
| Component | Location | Encryption |
|---|---|---|
| Contact list | User’s mesh storage space | User’s storage key |
| Sync metadata | DHT | Encrypted pointers |
| Contact details | Distributed across nodes | Per-contact encryption |
Mesh vs Server Storage
| Aspect | Mesh Storage | Server Storage |
|---|---|---|
| Control | User owns data | Server owns data |
| Availability | Distributed redundancy | Server uptime dependent |
| Privacy | No single entity has all data | Server sees encrypted blobs |
| Censorship | Resistant | Single point of control |
| Legal requests | No single target | Subpoena to server operator |
Sync Across Devices
Contacts synchronize across all user devices.
Multi-Device Contact Sync
| Step | Action | Encryption |
|---|---|---|
| 1 | Contact added on Device A | Encrypted with storage key |
| 2 | Uploaded to mesh storage | E2E encrypted |
| 3 | Device B polls for updates | Authenticated request |
| 4 | Device B downloads and decrypts | Same storage key |
| 5 | Local database updated | Re-encrypted locally |
Contact Sync Protocol:
DEVICE A (source):
1. Add new contact locally
2. Encrypt contact with user's storage key
3. Create sync record:
{
type: "contact_add",
encrypted_contact: ...,
timestamp: ...,
device_id: device_a_id
}
4. Sign with device key
5. Upload to mesh storage at user's sync location
DEVICE B (destination):
1. Poll sync location for new records
2. Verify signature from known device
3. Decrypt contact with storage key
4. Add to local database
5. Acknowledge sync (prevent re-download)Conflict Resolution
| Conflict Type | Resolution |
|---|---|
| Same contact, different names | Most recent timestamp wins |
| Deleted on A, modified on B | Deletion takes precedence |
| Verification status differs | Verified takes precedence |
| Notes differ | Merge or most recent |
Contact Metadata
Additional metadata users can store about contacts.
Available Metadata Fields
| Field | Type | Max Length | Description |
|---|---|---|---|
| Nickname | String | 64 chars | Alternate display name |
| Notes | String | 4096 chars | Free-form notes |
| Labels | Array | 10 labels | Organization tags |
| Custom fields | Key-value | 20 fields | User-defined metadata |
| Profile photo | Blob | 1 MB | Contact’s photo (if shared) |
| Trust level | Enum | N/A | Personal trust assessment |
Metadata Privacy
| Data | Stored Where | Visible To |
|---|---|---|
| Nickname | Local only | You only |
| Notes | Local only | You only |
| Labels | Local only | You only |
| Profile photo | Shared by contact | You + contact |
| Trust level | Local only | You only |
Blocking and Reporting
Block Mechanism (Client-Side)
Blocking is implemented entirely on the client side for maximum privacy.
How Blocking Works
| Action | Implementation | Privacy |
|---|---|---|
| Block contact | Add address to local block list | Server doesn’t know |
| Message from blocked | Client silently discards | Sender doesn’t know |
| Contact request | Auto-rejected | No notification |
| Unblock | Remove from local list | Immediate effect |
Block Implementation:
FUNCTION block_contact(wallet_address):
// Add to local block list
blocked_list = get_blocked_list()
blocked_list.add({
address: wallet_address,
blocked_at: current_time(),
reason: user_provided_reason // Optional, local only
})
save_blocked_list(blocked_list)
// Clear any pending messages/requests
delete_pending_from(wallet_address)
// No network communication - purely local
FUNCTION on_message_received(message):
IF sender_address IN blocked_list:
DISCARD message silently
RETURN
// Process message normally
...Block List Storage
| Property | Value |
|---|---|
| Storage location | Local IndexedDB |
| Encryption | With metadata storage key |
| Sync | Across user’s devices |
| Server knowledge | None |
No Server-Side Block List
Zentalk does not maintain server-side block lists.
Why No Server Block List
| Benefit | Description |
|---|---|
| Privacy | Server cannot know who you’ve blocked |
| Censorship resistance | Cannot be forced to block specific users |
| No social graph | Blocking pattern is metadata |
| User control | Only you decide who is blocked |
Comparison with Centralized Blocking
| Aspect | Zentalk (Client) | Centralized (Server) |
|---|---|---|
| Server knows blocks | No | Yes |
| Government requests | Cannot comply | Can reveal block lists |
| Malicious server | Cannot reveal | Could reveal blocks |
| Block effectiveness | Same | Same |
Reporting Abuse (Optional)
Users can optionally report abusive content to help the community.
Report Mechanism
| Component | Implementation |
|---|---|
| Report submission | Encrypted to community moderators |
| Content included | User’s choice (message content optional) |
| Sender identity | Can be anonymous |
| Recipient identity | Reported user’s address |
Report Structure
| Field | Required | Description |
|---|---|---|
| Report type | Yes | Spam, harassment, illegal content, etc. |
| Reported address | Yes | Wallet address of offender |
| Description | Optional | User’s explanation |
| Evidence | Optional | Message content (user consents) |
| Reporter address | Optional | For follow-up |
Report Submission Flow:
1. User selects "Report" on message/contact
2. Choose report category
3. Optionally add description
4. Optionally include message evidence
5. Choose anonymous or identified report
6. Encrypt report with community moderator keys
7. Submit via 3-hop relay (hide reporter IP)
8. Moderators review (decentralized moderation)What Happens to Reports
| Outcome | Action |
|---|---|
| Spam confirmed | Address added to public spam list |
| Harassment confirmed | Warning to community |
| Illegal content | Reported to appropriate authorities |
| False report | No action |
Opt-In Nature
| Aspect | User Control |
|---|---|
| Submit reports | Optional |
| Include content | User chooses |
| Reveal identity | User chooses |
| Use spam lists | Optional filter |
Contact Groups and Labels
Organizing Contacts
Users can organize contacts into groups and apply labels for easier management.
Group Features
| Feature | Description |
|---|---|
| Create group | Name and optional description |
| Add contacts | Multiple contacts per group |
| Nested groups | Groups within groups |
| Group messaging | Send to all group members |
| Group limits | Up to 100 contacts per group |
Label System
| Property | Description |
|---|---|
| Label name | User-defined, up to 32 characters |
| Label color | Visual distinction |
| Multiple labels | Contact can have multiple labels |
| Label hierarchy | Flat structure (no nesting) |
| Maximum labels | 50 per user |
Example Organization
| Group/Label | Use Case |
|---|---|
| Family | Close family members |
| Work | Professional contacts |
| Verified | Security-conscious marking |
| High-trust | Sensitive communications |
| Project X | Temporary project team |
Privacy of Organization
Contact organization is private and never leaves the user’s devices.
Organization Data Storage
| Data | Location | Encryption | Shared With |
|---|---|---|---|
| Group names | Local | Storage key | No one |
| Group membership | Local | Storage key | No one |
| Label assignments | Local | Storage key | No one |
| Organization hierarchy | Local | Storage key | No one |
Organization Storage:
// Groups stored separately from contacts
groups_table = {
group_id: uuid,
name: encrypted_string,
description: encrypted_string,
created_at: timestamp,
parent_group_id: nullable uuid
}
// Membership is a separate table
group_membership = {
group_id: uuid,
contact_id: uuid,
added_at: timestamp
}
// Labels stored similarly
labels_table = {
label_id: uuid,
name: encrypted_string,
color: string // Not sensitive
}
contact_labels = {
contact_id: uuid,
label_id: uuid,
applied_at: timestamp
}What No One Can Learn
| Information | Protected |
|---|---|
| How you organize contacts | Yes - encrypted locally |
| Group names | Yes - encrypted locally |
| Which contacts are grouped | Yes - encrypted locally |
| Your labeling scheme | Yes - encrypted locally |
| Organizational changes | Yes - encrypted locally |
Sync Privacy
When organization syncs across devices:
| Sync Property | Privacy Guarantee |
|---|---|
| Group data encrypted | End-to-end, only your devices |
| Membership encrypted | End-to-end, only your devices |
| Labels encrypted | End-to-end, only your devices |
| Mesh network sees | Encrypted blobs only |
Implementation Considerations
Contact Discovery Flow Summary
Complete Contact Addition Flow:
1. DISCOVERY
- Scan QR code, OR
- Enter wallet address manually, OR
- Search username (if registered)
2. VALIDATION
- Verify address format
- Check not already in contacts
- Check not in block list
3. KEY RETRIEVAL
- Query DHT for contact's key bundle
- Retrieve identity key, signed pre-key, one-time pre-keys
- Verify key bundle signatures
4. CONTACT REQUEST
- Generate proof-of-work
- Create contact request message
- Encrypt with X3DH
- Send via 3-hop relay
5. RECIPIENT PROCESSING
- Receive contact request
- Verify PoW
- Display to user for action
6. ACCEPTANCE
- User accepts request
- Contact added to encrypted local storage
- Notify sender of acceptance
7. VERIFICATION (optional)
- Compare safety numbers
- QR code verification
- Mark as verified
8. SYNC
- Encrypt contact data
- Upload to mesh storage
- Sync to other devicesSecurity Properties Summary
| Property | Mechanism | Guarantee |
|---|---|---|
| Contact privacy | Local-only storage | Server cannot enumerate contacts |
| Organization privacy | Encrypted metadata | Groups/labels are secret |
| Block privacy | Client-side blocking | Server doesn’t know who’s blocked |
| Discovery privacy | No address book upload | Contact graph protected |
| Verification integrity | Safety numbers | MITM detection |
| Spam resistance | PoW + rate limiting | Costly to spam |
Related Documentation
- Wallet-Based Identity - How wallet addresses work
- Protocol Specification - X3DH key exchange details
- Multi-Device Support - Syncing across devices
- DHT and Kademlia - Distributed storage and lookup
- Onion Routing - Anonymous message delivery