Skip to content

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.

  • 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”.

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

The fundamental cryptographic identity for each device in the Cryptid protocol.

DeviceIdentity Structure
struct 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,
}
  • device_id: [u8; 32]

    • Blake3 hash of the device’s Ed25519 public key
    • Permanent identifier for this device
    • Used only at the MLS cryptographic layer
    • MUST NOT change over the device’s lifetime
  • keypair: Ed25519KeyPair

    • Ed25519 keypair for MLS cryptographic operations
    • Private key MUST be stored securely and never transmitted
    • Public key is used to derive device_id
    • Used exclusively for MLS protocol operations
  • delivery_addresses: Vec<DeliveryAddress>

    • Collection of ephemeral addresses for message routing
    • Separate from cryptographic identity for privacy
    • Can be rotated periodically
    • Multiple addresses MAY be active simultaneously
  • created_timestamp: u64

    • Unix timestamp of device identity creation
    • Used for calculating device age for progressive trust
    • MUST NOT be modified after creation
  • Device Identity Generation:

    • Device identity MUST be generated locally without server coordination
    • device_id MUST be derived from the public key (not random)
    • keypair MUST use RFC 8032 compliant Ed25519 cryptopgraphy
    • Initial delivery_addresses MAY be empty
  • Security Requirements:

    • Private key MUST never be transmitted over the network
    • Device identity SHOULD persist across app restarts
    • device_id MUST be unique within the protocol

The fundamental cryptographic identity for each user in the Cryptid protocol.

UserIdentity Structure
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>,
}
  • user_id: [u8; 32]

    • Blake3 hash of the user’s Ed25519 public key
    • Permanent identifier for this user across ALL their devices
    • Used at the application layer (NOT for MLS operations)
    • MUST NOT change over the user’s lifetime
  • keypair: Ed25519KeyPair

    • Ed25519 keypair for application-layer operations
    • Used for signing user-level actions (device linking, profile updates)
    • NOT used for MLS cryptographic operations
    • Private key shared and securely provisioned across all the user’s devices
  • devices: Vec<DevicePublicInfo>

    • List of all devices linked to this user identity
    • Each device has its own cryptographic identity for MLS
    • Used for multi-device coordination and group invitations
    • MUST contain at least one device
    • See: DevicePublicInfo Structure
  • created_at: u64

    • Unix timestamp of user identity creation
    • Corresponds to when the first device was provisioned
    • MUST NOT be modified after creation
  • default_persona: Persona

    • Primary persona containing display name and profile information
    • Used when no specific persona is selected
    • MUST always be present
  • personas: HashMap<NonZerou16, Persona>

    • Optional additional personas for different contexts
    • Indexed by non-zero persona ID
    • Allows users to maintain separate identities within the protocol
    • MAY be empty
  • User Identity Generation:

    • User identity MUST be generated on the first device
    • Subsequent devices receive the user identity via secure provisioning
    • user_id MUST be derived from the public key (not random)
    • User Identity MUST be shared securely between a user’s devices
  • Multi-Device Coordination:

    • All devices under one user identity share the same user_id and keypair
    • Each device MUST have a unique DeviceIdentity for MLS operations
    • Adding a device requires a secure device linking protocol
    • User identity enables inviting all user’s devices to groups at once
  • Security Requirements:

    • User keypair MUST be provisioned securely to new devices
    • Device linking MUST use ephemeral key exchange
    • User identity MUST NOT be exposed to servers
  • User Identity (Application Layer):

    • Persistent across devices
    • Used for contact exchange, profiles, multi-device coordination
    • One per user
  • Device Idenetity (MLS Layer):

    • Unique to each device
    • Used for MLS cryptographic operations
    • One per device

A user with N devices has:

  • 1 UserIdentity (shared across all devices)
  • N DeviceIdentities (one per device)

Delivery addresses are routing identifiers derived from per-group Mailbox Keypair, separate from the cryptographic Device Identity. Each address is derived from its own keypair, so a device proves mailbox ownership without revealing its device identity.

DeliveryAddress Structure
struct DeliveryAddress {
// Base32 identifier, 24 chars, derived from mailbox_public
prefix: String,
// Server domain (e.g., "chat.example.com")
server: String,
// Senders encrypt the outer envelope layer to this key. Reaches contacts
// inside MLS via AddressRotation, never via server.
mailbox_public: X25519PublicKey,
// Timestamp of when the address was generated
created_at: u64,
// Kept live through the rotation overlap window, so a contact who missed an
// AddressRotation still reaches a real mailbox
active: bool,
}
impl DeliveryAddress {
fn full_address(&self) -> String {
format!("{}@{}", self.prefix, self.server)
}
// Anyone holding the address can check if the prefix binds to the key
fn verify_binding(&self) -> bool {
let hash = blake3::hash(self.mailbox_public.as_bytes());
self.prefix == base32::encode(&hash.as_bytes()[..15])
}
}
  • prefix: String

    • First 15 bytes of the Blake3 hash of mailbox_public, base32-encoded
    • Exactly 24 characters
    • base32 is case-insensitive, which an address of the form prefix@server requires
    • MUST be recomputable from mailbox_public by any party holding the address
    • Is a routing label, not a security boundary; ownership is proven against mailbox_public
  • server: String

    • Domain name of the server hosting this mailbox (e.g., chat.example.com)
    • MUST be a valid domain name, and MAY include a port if non-standard
    • Devices MAY hold mailboxes on multiple servers (multi-homing)
  • mailbox_public: X25519PublicKey

    • The key senders encrypt the outer envelope layer to
    • Distributed to group members inside MLS via AddressRotation, never to servers
    • MUST be bound to prefix by the derivation above
  • created_at: u64

    • Unix timestamp of when the address was generated
    • MUST NOT be modified after creation
  • active: bool

    • Whether the mailbox currently accepts delivery
    • Kept true through the rotation overlap window, so a member who missed an AddressRotation still reaches a live mailbox
    • MUST be set to false before deregistration
  • Address Generation:

    • Addresses MUST be derived from a mailbox keypair, never generated randomly
    • The mailbox keypair MUST be derived from the device’s master mailbox seed with the server domain bound into the derivation context
    • A mailbox keypair MUST NOT be reused across servers
    • Each group MUST get its own mailbox; ContactRequest traffic uses a separate well-known scope
  • Address Rotation:

    • Devices MAY rotate addresses to prevent tracking or to burn an address that has become spammy
    • Rotation increments the rotation counter for that scope and derives a fresh keypair
    • Old addresses MUST remain active through an overlap window to receive in-flight messages
    • Rotation MUST NOT affect the underlying device identity
    • Rotation MUST NOT require a group commit; routing is not a group governance decision
    • Rotation period is implementation-defined but SHOULD be at least 12 hours
  • Multi-Homing:

    • Devices MAY register mailboxes on multiple servers simultaneously
    • Each server requires separately derived keypairs, per domain separation above
    • A device MAY hold several mailboxes for the same group, published in preference order
    • Senders use the first address and fail over to later ones; see Failover
    • Only the primary receives traffic under normal operation, so a secondary mailbox does not double a device’s metadata exposure
    • Two federated servers cannot link a multi-homed device, as they share no identifier for it
  • Message Routing:

    • Servers route on the full address (prefix@server) and MUST receive nothing further
    • Messages MUST only be delivered to active addresses
    • Servers SHOULD NOT store delivery address history
    • Address lookup MUST NOT reveal device identity to servers
  • Privacy Considerations:

    • Addresses are routing metadata visible to servers
    • A server can link one device’s mailboxes to each other by source address and retrieval timing; this requires network-level anonymity to prevent and is out of scope
    • Group members can link a device across shared groups via its MLS credential, by design
    • Device identity remains constant regardless of address changes
  • Device Identity (Permanent)

    • Cryptographic keypair-based
    • Used for MLS operations
    • Never changes
    • Not visible to servers
  • Delivery Address (Ephemeral)

    • Random routing identifier
    • Used for message delivery
    • Rotates periodically
    • Visible to servers for routing only

A device has:

  • One permanent Device Identity
  • Multiple delivery addresses over its lifetime
  • MAY have addresses on multiple servers (multi-homing)

A subset of DeviceIdentity that is safe to share with other users as part of your UserIdentity.

DevicePublicInfo Structure
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,
// This device's contact mailboxes (MailboxScope::Contact), in preference
// order. Used for first contact and for Welcome delivery, before any
// group-scoped address exists
contact_addresses: Vec<DeliveryAddress>,
// Server that hosts the KeyPackages for this device
keypackage_server: String,
// Timestamp of when it was linked by the user
linked_at: u64,
}
  • device_id: [u8; 32]

    • Blake3 hash of the device’s Ed25519 public key
    • Distributed as part of an InfoPackage
    • Used by other users to reference this device in multi-device contexts
    • MUST remain stable for the lifetime of this device entry in UserIdentity.devices
  • public_key: Ed25519PublicKey

    • The public half of this device’s keypair
    • Used to verify device-level signatures
    • MUST correspond to to the device’s private key used for MLS operations
  • contact_addresses: Vec<DeliveryAddress>

    • This device’s contact mailboxes (MailboxScope::Contact), in preference order
    • Used for first contact, Welcome delivery, and early direct messages, before any group-scoped address exists
    • Senders use index 0 and fail over to later entries; see Failover
    • MUST contain at least one address
    • MAY be rotated later; the field captures the addresses active at link time
  • keypackage_server: String

    • Base domain or URL of the server hosting this device’s MLS KeyPackages
    • Used by contacts to fetch KeyPackages when adding this device to groups
    • MUST be reachable over HTTPS for interoperable deployments
  • linked_at: u64

    • Unix timestamp of when this device was linked to the UserIdentity
    • Used for UX (e.g., linked 3 days ago) and device management views
    • MAY be used for heuristics like device age or in client UIs
    • MUST be set at link time and SHOULD NOT be modified afterward
  • DevicePublicInfo Construction

    • DevicePublicInfo MUST be derived from an existing valid DeviceIdentity
    • device_id and public_key MUST match the underlying device MLS identity
    • contact_addresses MUST contain at least one active contact mailbox for the device at the time of linking
    • keypackage_server MUST point to a server that exposes the KeyPackage upload/fetch API for this device
  • Sharing and Privacy Requirements:

    • DevicePublicInfo MAY be shared with other users via identity InfoPackages
    • Private key material MUST NOT be included in DevicePublicInfo
    • Clients SHOULD treat DevicePublicInfo as public metadata suitable for QR codes and links
    • Removing a device from UserIdentity.devices MUST be treated as a revocation of that device for future operations

Personas allow users to maintain different identities within the Cryptid protocol. Each User Identity has a default persona and can create additional personas for different contexts (e.g., work, personal, community-specific, etc)

Persona Structure
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),
}
  • display_name: String

    • Human readable name shown to other users
    • MUST be non-empty
    • MAY contain Unicode characters (emoji, international characters, etc)
    • Length SHOULD be limited by implementations (recommended 1-32 characters)
    • Used in contact lists, group member lists, and message displays
  • profile_picture: Option<ProfilePicture>

    • Optional profile picture for this persona
    • If None, implementations MAY display a default avatar
    • Profile pictures are signed for authenticity
    • See ProfilePicture below for structure details
  • bio: Option<String>

    • Optional biography or status message
    • If None, no bio is displayed
    • MAY contain unicode characters
    • Length SHOULD be limited by implementations (recommended 0-500 characters)
    • Used for personal descriptions, status messages, or context about the persona
  • pronouns: Option<String>

    • Optional pronouns for this persona
    • If None, no pronouns are displayed
    • SHOULD NOT contain unicode characters
    • Length SHOULD be limited by implementations (recommended 0-20 characters)
    • Used to display a persona’s pronouns
  • Persona Management

    • Every User Identity MUST have at least one persona (the default one)
    • Default persona MUST be present at user identity creation
    • Additional personas MAY be created with unique NonZeroU16 identifiers
    • Persona IDs MUST be unique within a user identity
    • Personas are stored in the user identity, not transmitted separately
  • Display Name Requirements

    • Display names MUST NOT be used as unique identifiers
    • Multiple users MAY have identical display names
    • Display names MAY be changed at any time
    • Name changes propagate through contact updates
  • Privacy Considerations

    • Personas are visible to anyone who has the user’s identity
    • Profile pictures are visible to contacts
    • Different personas do not provide cryptographic unlinkability
    • For true unlinkability, users should create separate user identity instances
  • Plural systems: Different headmates with distinct profiles
  • Role-based switching: Moderator/Admin modes with enhanced visibility

Protocol Responsibility Regarding Personas

Section titled “Protocol Responsibility Regarding Personas”

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

Profile pictures provide visual identity while maintaining authenticity through signatures.

ProfilePicture Structure
struct ProfilePicture {
// Inline thumbnail for instant display. AVIF, 64x64, max 16KB.
// Travels inside InfoPackages, so it must stay small
thumbnail: Vec<u8>,
// Full resolution image, fetched on demand. Identity-scoped, so it uses
// FileKey::Direct because a picture shared via InfoPackage has no group
// context from which to derive an epoch key
full_image: Option<FileRef>,
// Timestamp
uploaded_at: u64,
// Signed by the user keypair for authenticity
signature: Ed25519Signature,
}
  • thumbnail: Vec<u8>

    • AVIF-encoded 64x64 preview, maximum 16KB
    • Displayed immediately; travels inline in InfoPackages
    • MUST be square and lossless
  • full_image: Option<FileRef>

    • Full resolution image in the blob store, fetched on demand
    • key MUST be FileKey::Direct as there’s no group context for an identity-scoped picture
    • None where the owner has published only a thumbnail
    • See Media Handling for size and format limits
  • uploaded_at: u64

    • Unix timestamp of when the picture was set
    • Used for detecting profile picture updates
    • MUST NOT be backdated
  • signature: Ed25519Signature

    • Signature over the thumbnail and the FileRef, using the user’s keypair
    • Proves the profile picture was set by the user identity owner
    • Prevents impersonation through unauthorized profile picture changes
    • MUST be verified before accepting profile picture updates
  • Profile Picture Authentication

    • Profile pictures MUST be signed by the user’s keypair
    • Implementations MUST verify signatures before displaying pictures
    • Unsigned or incorrectly signed pictures MUST be rejected
    • Signature covers the image data and metadata
  • Size and Format

    • Profile pictures MUST be AVIF, square, and losslessly encoded
    • Thumbnails MUST NOT exceed 16kb at 64x64
    • Full images MUST NOT exceed 200 KB, with 512x512 recommended
    • Implementations MUST reject pictures violating any of the above
  • Updates and Propagation

    • Profile picture changes propagate through contact updates
    • Old profile pictures MAY be retained temporarily for UI consistency
    • Implementations SHOULD cache profile pictures locally
    • Cache invalidation based on uploaded_at timestamp

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(device_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
type UserId = [u8; 32];
type DeviceId = [u8; 32];
type GroupId = [u8; 32];
type Handle = [u8; 32];

All of them are 32 bytes. device_id and user_id are Blake3 hashes of their respective public keys, as above.

Group IDs are also 32 bytes, but how they are produced depends on the group type:

  • DirectMessageGroup: blake3(sorted(user1_id, user2_id)) - deterministic, so both parties derive the same identifier without coordinating
  • Multi-user group: 32 bytes from a cryptographically secure random source

Both MUST be exactly 32 bytes. InnerEnvelope.group_id is on the wire, and a single width means an implementation needs no discriminator to parse it.

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

The QR code contains only a compact reference, not the full identity data:

CompactInfoQR Structure
struct CompactInfoQR {
// Server URL to fetch encrypted data from
info_package_url: String, // e.g., "https://chat.example.com/api/v1/infopackage/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: GroupId },
}

When decrypted, an InfoPackage contains the encrypted identity data:

IdentityInfoPackage Structure
struct IdentityInfoPackage {
user_id: UserId,
user_public_key: Ed25519PublicKey,
// Default persona (always present)
default_persona: Persona,
// Additional personas
personas: HashMap<NonZeroU16, Persona>,
// User's current devices
devices: Vec<DevicePublicInfo>,
// Profile picture reference (fetched from the blob store)
profile_picture: Option<ProfilePicture>,
// Metadata
created_at: u64,
}

When you upload an InfoPackage and someone scans your QR code, the InfoPackage system ensures:

  1. Encryption: Server never sees your identity data (encrypted with info_package_key)
  2. Expiration: Package automatically expires after configured TTL
  3. Revocation: You can manually revoke access at any time
  4. One-time capable: Can limit to single-use or multiple scans
  5. 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.

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:

OperationUses User IdentityUses Device Identity
Contact exchange (QR codes)Embedded in bundle
Group invitationsAll 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 revocationDevice being revoked

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
AspectTraditional Authentication SystemsCryptid’s Device-Centric System
Account CreationServer registration requiredLocal key generation only
Identity ProofServer password verificationCryptographic signature
Trust Anchor”Server says alice@server is legitimate""Alice’s signature proves ownership”
Multi-DeviceShared account across devicesUser Identity shared across devices; Device Identities independent
Server CompromiseAll user accounts affectedIndividual devices unaffected
PrivacyUsername/email requiredNo PII needed