Cryptid's Identity System
Instead of traditional accounts (like alice@server.com with a password), Cryptid uses a two-layer identity system. Each user has a persistent User Identity that’s shared across devices, while each device maintains its own Device Identity for MLS cryptographic operations. This separation enables seamless multi-device support while maintaining strong cryptographic guarantees.
Traditional Authentication Model
Section titled “Traditional Authentication Model”- User Account:
alice@server.com - Password: hunter2
- Server stores:
- Username
- Password hash
- User data
The trust model essentially boils down to “Server vouches that alice@server.com is legitimate”.
Cryptid’s Two-Layer Model
Section titled “Cryptid’s Two-Layer Model”User Identity: Persistent Ed25519 keypair shared across all your devices
- Used for application-layer operations (contact exchange, group invitations)
- Enables multi-device coordination
- First device creates it, subsequent devices receive it via secure provisioning
Device Identity: Per-device Ed25519 keypair (distinct from User Identity)
- Used exclusively for MLS cryptographic operations
- Each device has its own Device ID and keypair
- Enables independent operation and device-level revocation
The server stores nothing permanent about either identity.
Here, the trust model becomes “This message can be verified cryptographically at multiple layers”:
- MLS Layer: Device signatures prove message authenticity
- Application Layer: User signatures prove identity ownership
Technical Implementation
Section titled “Technical Implementation”User Identity (Application Layer)
Section titled “User Identity (Application Layer)”User-level identity shared across all devices controlled by a single user.
struct UserIdentity { // Blake3 hash of user public key user_id: [u8; 32],
// User-level Ed25519 keypair (for application layer only) keypair: Ed25519KeyPair,
// All devices controlled by this user devices: Vec<DevicePublicInfo>,
// Unix timestamp of initial creation created_at: u64,
// Default persona default_persona: Persona,
// Additional personas (indexed by NonZeroU16) personas: HashMap<NonZeroU16, Persona>,}
struct DevicePublicInfo { // Blake3 hash of device's public key (permanent, for MLS layer only) device_id: [u8; 32], // The device's public key public_key: Ed25519PublicKey, // Address to initially contact this device initial_delivery_address: DeliveryAddress, // Server that hosts the KeyPackages for this device keypackage_server: String, // Timestamp of when it was linked by the user linked_at: u64,}
struct ProfilePicture { // e.g., "image/png" mime_type: String, // Raw image bytes or hash/link if large data: Vec<u8>, // Timestamp uploaded_at: u64, // Signed for authenticity signature: Ed25519Signature,}
struct Persona { // Display name for this persona display_name: String,
// Profile picture for this persona profile_picture: Option<ProfilePicture>,
// Bio for this persona bio: Option<String>,
// Pronouns for this persona pronouns: Option<String>,}
pub enum PersonaId { Default, Id(NonZeroU16),}
impl UserIdentity { /// Generate a new user identity (only happens on first device setup) fn generate() -> Self { let keypair = Ed25519KeyPair::generate();
Self { user_id: Blake3::hash(keypair.public_key().as_bytes()).into(), keypair, devices: Vec::new(), created_at: current_timestamp(), default_persona: Persona { display_name: Some("My Name".to_string()), profile_picture: None, bio: None, pronouns: None, }, personas: HashMap::new(), } }
fn add_persona(&mut self, persona: Persona) -> Option<u16> { // Find next available key let next_key = (1..NonZeroU16::MAX) .find(|k| !self.personas.contains_key(k))?;
self.personas.insert(next_key, persona); Some(next_key) }
pub fn persona_for(&self, id: PersonaId) -> Option<&Persona> { match id { PersonalId::Default => Some(&self.default_persona), PersonalId::Id(id) => self.personas.get(&id), } }
pub fn default_persona(&self) -> &Persona { &self.default_persona }
pub fn remove_persona(&mut self, id: NonZeroU16) -> Result<Persona, &'static str> { self.personas.remove(&id) .ok_or("Persona not found") }
fn create_info_package(&self) -> Result<InfoPackageUploadRequest> { // Create identity package content let identity_package = IdentityInfoPackage { user_id: self.user_id, user_public_key: self.keypair.public_key(), devices: self.devices.clone(), default_persona: self.default_persona.clone(), personas: self.personas.clone(), profile_picture: None, created_at: current_timestamp(), };
// Prepare upload request with TTL and usage limits Ok(InfoPackageUploadRequest { package_type: InfoPackageType::Identity, content: InfoPackageContent::Identity(identity_package), ttl_seconds: 24 * 3600, // 24 hours default max_uses: Some(100), // Up to 100 scans }) }
/// Sign data with user identity key (application-level signatures) fn sign(&self, data: &[u8]) -> Ed25519Signature { self.keypair.sign(data) }
/// Add a linked device to this user account fn add_linked_device(&mut self, device_info: DevicePublicInfo) { self.devices.push(device_info); }
/// Remove a device (for revocation) fn remove_device(&mut self, deviceid: &[u8; 32]) { self.devices.retain(|d| &d.deviceid != deviceid); }}Personas
Section titled “Personas”Personas allow users to maintain multiple presentation identities and switch between them contextually.
Use Cases:
- Plural systems: Different headmates with distinct profiles
- Role-based switching: Moderator/Admin modes with enhanced visibility
Structure:
default_persona: Always exists, cannot be deletedpersonas: Optional additional personas indexed by NonZeroU16 (1-65535)PersonaId: Enum for referencing personas (Default or Id(NonZeroU16))
Protocol Responsibility:
The protocol only carries persona data for rendering. Clients are responsible for how they use them. Some example client-side features might include:
- Proxy tag detection (e.g., PluralKit-style
[text]patterns) - Persona switching UI/UX
- Role indicators and badges
Device Identity (MLS Layer)
Section titled “Device Identity (MLS Layer)”Per-device identity used by each device for all MLS operations.
/// Device-level identity (MLS Layer)/// Each device has its own keypair, even within the same user identitystruct DeviceIdentity { // Core cryptographic identity (permanent, MLS layer only) device_id: [u8; 32], // Device keypair used exclisively for MLS operations keypair: Ed25519KeyPair,
// Delivery addresses (ephemeral, rotatable) delivery_addresses: Vec<DeliveryAddress>
// Unix timestamp of creation created_timestamp: u64,}
struct DeliveryAddress { // Address identifier (e.g., "a1b2c3d4e5...") prefix: String, // Server domain (e.g., "chat.example.com") server: String, // Timestamp of when the address was generated created_at: u64, // Can be deactivated without deletion active: bool,}
impl DeliveryAddress { fn full_address(&self) -> String { format!("{}@{}", self.prefix, self.server) }}
impl DeviceIdentity { /// Generate a new device identity without any external dependencies fn generate() -> Self { // Modern elliptic curve cryptography let keypair = Ed25519KeyPair::generate();
Self { // Derive device_id from public key device_id: Blake3(keypair.public_key().as_bytes()).into(), keypair, created_timestamp: current_unix_timestamp(), // User can set this later, e.g., "Alice's Phone" display_name: None, // Initially create empty delivery addresses delivery_addresses: Vec::new(), } }
/// Sign arbitrary data with this device's private key fn sign(&self, data: &[u8]) -> Ed25519Signature { self.keypair.sign(data) }
/// Create MLS credential for group operations fn create_mls_credential(&self) -> MLSCredential { MLSCredential::basic(self.device_id, self.keypair.public_key()) }
/// Create a new delivery address for your device fn create_delivery_address(&mut self, server: &str) -> String { let addr = DeliveryAddress { prefix: Uuid::new_v4(), server: server.to_string(), created_at: current_unix_timestamp(), active: true, };
self.delivery_addresses.push(addr);
// Return full address format!("{}@{}", prefix, server) }
/// Deactivate a delivery address fn burn_address(&mut self, full_address: &str) { if let Some(addr) = self.delivery_addresses.iter_mut() .find(|a| a.full_address() == full_address) { addr.active = false; } }
/// Get all active delivery addresses fn active_addresses(&self) -> Vec<String> { self.delivery_addresses.iter() .filter(|a| a.active) .map(|a| a.full_address()) .collect() }
/// Get all active addresses for a specific server fn active_addresses_for_server(&self, server: &str) -> Vec<String> { self.delivery_addresses.iter() .filter(|a| a.active && a.server == server) .map(|a| a.full_address()) .collect() }
/// Generate MLS KeyPackages for others to add this device to groups. /// Implementation uses OpenMLS KeyPackage::builder() API. fn generate_key_packages(&mut self, count: usize) -> Vec<KeyPackageBundle> { // Implementation note: Uses MLS RFC 9420 KeyPackage format // with cipher suite MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519 // // See: reference/contact-exchange-and-trust for usage details
(0..count).map(|_| { // OpenMLS implementation KeyPackage::builder() .build(ciphersuite, provider, &self.keypair, self.create_mls_credential())
// Returns KeyPackageBundle with: // - key_package: Public part (uploaded to server) // - private_key: Secret part (stored locally for welcome decryption) }).collect() }
/// Verify device_id is correctly derived from public key fn verify_device_id(&self) -> bool { let expected = Blake3(self.keypair.public_key().as_bytes()); self.device_id == expected }
}Why Ed25519?
Section titled “Why Ed25519?”Ed25519 is a modern elliptic curve signature scheme that provides:
- High security: 128-bit security level (equivalent to 3072-bit RSA).
- Performance: Fast signature generation and verification.
- Simplicity: No parameter choices, no pitfalls.
- Deterministic signatures: Same message + key = same signature (important for MLS).
- Small keys: 32-byte public keys, 64-byte signatures.
RFC 8032 Compliance Requirement
Section titled “RFC 8032 Compliance Requirement”Cryptid requires Ed25519 as defined in RFC 8032. This is also known as Ed25519-IETF.
- MLS security proofs require SUF-CMA (Strong Unforgeability under Chosen Message Attack)
- RFC 8032 Ed25519 provides SUF-CMA security
- The original Ed25519 paper from 2011 only provides EUF-CMA (Existential Unforgeability), which is insufficient for us.
User ID and Device ID Derivation
Section titled “User ID and Device ID Derivation”User IDs and Device IDs are deterministically derived from their respective public keys using Blake3 hashing.
Formula:
user_id = Blake3(user_public_key)device_id = Blake3(public_key)
We do this for the following reasons:
- Cryptographically bound: The ids cannot be forged independently of the keypair
- Verifiable: Anyone can verify that a specific id matches the public key
- Prevents squatting: Attackers cannot claim arbitrary user/device IDs
- Single source of truth: The public key uniquely determines the id.
Properties:
- Both produce 32-byte (64 hex character) identifiers
- User ID is the same across all of a user’s devices
- Device ID is unique to each device, even for the same user
- No central authority needed for ID assignment
Contact Exchange via InfoPackages
Section titled “Contact Exchange via InfoPackages”For contact exchange (QR codes, invite links), use InfoPackages instead of directly sharing identity bundles. InfoPackages provide the same functionality with added privacy and security benefits.
An InfoPackage wraps your identity information in an encrypted, server-stored package that expires automatically. When someone scans your QR code or clicks your invite link, they receive:
- Your
user_id(permanent user identity) - Your
user_public_key(for verifying user-level signatures) - A list of ALL your devices with their individual device IDs, public keys, and initial delivery addresses
CompactInfoQR
Section titled “CompactInfoQR”The QR code contains only a compact reference, not the full identity data:
struct CompactInfoQR { // Server URL to fetch encrypted data from info_package_url: String, // e.g., "https://chat.example.com/api/v1/info-package/abc123xyz"
// Decryption key (client holds this, never sent to server) info_package_key: [u8; 32],
// Type of package (for display before fetching) package_type: InfoPackageType,
// Display name (for UI, e.g., "Alice" or "Team Chat") display_name: String,}
#[serde(tag = "type")]enum InfoPackageType { #[serde(rename = "identity")] Identity,
#[serde(rename = "group_invite")] GroupInvite { group_id: [u8; 32] },}IdentityInfoPackage Content
Section titled “IdentityInfoPackage Content”When decrypted, an InfoPackage contains the encrypted identity data:
struct IdentityInfoPackage { user_id: [u8; 32], user_public_key: [u8; 32],
// Default persona (always present) default_persona: Persona,
// Additional personas personas: HashMap<NonZeroU16, Persona>,
// User's current devices devices: Vec<DevicePublicInfo>,
// Profile picture reference (for fetching via device-to-device) profile_picture: Option<ProfilePicture>,
// Metadata created_at: u64,}Why Share User Identity via InfoPackages?
Section titled “Why Share User Identity via InfoPackages?”When you upload an InfoPackage and someone scans your QR code, the InfoPackage system ensures:
- Encryption: Server never sees your identity data (encrypted with
info_package_key) - Expiration: Package automatically expires after configured TTL
- Revocation: You can manually revoke access at any time
- One-time capable: Can limit to single-use or multiple scans
- Compact: QR code contains only a tiny reference, not full identity
Recipients scanning your code:
- Receive your user_id (permanent user identity)
- Receive your user_public_key (for verifying user-level signatures)
- See a list of ALL your devices
- Can add all your devices to a group in a single MLS operation
- Can inspect individual device fingerprints if desired
See Info Packages for a more detailed specification.
Device Identity vs. Delivery Addresses
Section titled “Device Identity vs. Delivery Addresses”Cryptid separates user identity, device identity, and routing into three different concepts:
User Identity (user_id)
Section titled “User Identity (user_id)”- Permanent cryptographic identity representing you across all devices
- Used for contact exchange and group invitations
- Shared across all your devices via secure provisioning
- Visible to group members (they see “Alice”, not “Alice’s Phone”)
- Never changes unless you create an entirely new user account
Device Identity (device_id)
Section titled “Device Identity (device_id)”- Permanent cryptographic identity representing a specific device
- Used in MLS groups for member authentication and message encryption
- Each device has its own, even within the same user account
- Visible to group members (if they inspect user’s device list)
- Never changes unless you create an entirely new device identity
Delivery Address
Section titled “Delivery Address”- Ephemeral routing endpoints (e.g.,
a1b2c3d4...@chat.example.com) - Used by servers for message delivery
- Can be created, rotated, and burned (deleted) freely
- Hidden from message recipients (only routing metadata visible to servers)
- Not used for blocking (recipients block by user_id, not address)
Key Properties
Section titled “Key Properties”- Two Keypairs: Users have ONE User Identity keypair (shared across devices) and each device has ONE Device Identity keypair used for MLS only.
- Multiple Addresses: Devices can maintain multiple active delivery addresses simultaneously.
- Server-Specific: Each delivery address belongs to a specific server.
- Multi-Homing: Devices can have addresses on multiple servers (e.g., primary + backups).
Why Separate These?
Section titled “Why Separate These?”- Multi-Device UX: User Identity provides consistent identity across devices while Device Identity enables independent MLS operations.
- Spam Prevention: Proactively burn addresses that receive spam and create new ones.
- Privacy: Rotate addresses to prevent long-term tracking by network observers.
- Flexibility: Different addresses for different contexts (work, general, public, etc).
- Stability: Contacts know your user_id and will be notified of address changes IF they share a chat with you.
Address Derivation
Section titled “Address Derivation”Delivery address prefixes are randomly generated UUIDv4s:
- They have no relationship to device_id
- They are not derived from the main keypair
- Pure randomness for maximum privacy
This ensures that address rotation cannot be linked through cryptographic analysis.
MLS KeyPackages
Section titled “MLS KeyPackages”Devices generate KeyPackages (one-time use cryptographic material) for MLS group additions. KeyPackages are uploaded to a designated server and fetched when adding the device to groups.
When inviting a user to a group, the inviting device fetches KeyPackages for all of the user’s devices and adds them simultaneously in a single MLS commit.
For complete details on KeyPackage management, contact exchange, and trust establishment, see Contact Exchange and Trust.
User Identity vs. Device Identity: When Each Is Used
Section titled “User Identity vs. Device Identity: When Each Is Used”Understanding which identity is used for what operation:
| Operation | Uses User Identity | Uses Device Identity |
|---|---|---|
| Contact exchange (QR codes) | ✅ | Embedded in bundle |
| Group invitations | ✅ | All devices invited |
| MLS encryption/decryption | ❌ | ✅ |
| MLS group membership | ❌ | ✅ |
| Message signing (MLS) | ❌ | ✅ |
| Display name/avatar | ✅ | ❌ |
| Multi-device linking | ✅ Same keypair on all devices | ✅ Unique per device |
| Device revocation | ✅ Signs revocation | Device being revoked |
Layer Separation
Section titled “Layer Separation”Application Layer: Uses UserIdentity for user-facing operations
- “Alice wants to add Bob to the group”
- Fetch Bob’s ShareableIdentityBundle (contains user_id + all their devices)
- Fetch KeyPackages for all of Bob’s devices
MLS Layer: Uses DeviceIdentity for cryptographic operations
- “Alice’s Phone adds Bob’s Phone, Bob’s Laptop, and Bob’s Tablet to the MLS group”
- Three separate MLS add operations in a single commit
- Each device independently encrypts/decrypts using its own DeviceIdentity keypair
Comparison with Traditional Systems
Section titled “Comparison with Traditional Systems”| Aspect | Traditional Authentication Systems | Cryptid’s Device-Centric System |
|---|---|---|
| Account Creation | Server registration required | Local key generation only |
| Identity Proof | Server password verification | Cryptographic signature |
| Trust Anchor | ”Server says alice@server is legitimate" | "Alice’s signature proves ownership” |
| Multi-Device | Shared account across devices | User Identity shared across devices; Device Identities independent |
| Server Compromise | All user accounts affected | Individual devices unaffected |
| Privacy | Username/email required | No PII needed |