decentrl.
Protocol

Alice and Bob

The complete protocol lifecycle — from identity creation to encrypted messaging — in a single walkthrough.

This is the complete Decentrl lifecycle. We'll follow Alice and Bob from key generation to encrypted conversation, showing how every piece fits together.

1. Alice Creates Her Identity

Alice's application generates three cryptographic keys on her device:

signing_keypair = Ed25519_generate()    // proves identity
pre_keypair     = X25519_generate()     // enables first contact
storage_key     = random_bytes(32)      // encrypts stored data

These keys produce her DID — a self-contained identifier that embeds her public keys and chosen mediator:

did:decentrl:{base64(alias)}:{base58btc(signing_pub)}:{base58btc(pre_pub)}:{base64(mediator_did)}

Anyone with Alice's DID can immediately extract her public signing key, her pre-key for initiating contact, and her mediator's address. No registry lookup. No network request.

2. Alice Registers with Her Mediator

Before Alice can receive messages, she must register with her mediator. Registration is itself a communication contract — Alice establishes a bilateral agreement with the mediator's own DID:

1. Alice creates a contract request where recipient_did = mediator_did
2. She encrypts it using X25519(her_ephemeral_private, mediator_prekey)
3. She sends it to the mediator via DIRECT_AUTHENTICATED channel
4. The mediator decrypts, auto-accepts, signs the contract
5. Both store the signed bilateral contract

There is no special registration table. An active, non-expired contract with the mediator is registration. The same primitive that handles user-to-user relationships handles infrastructure relationships.

3. Alice and Bob Establish a Communication Contract

When Alice wants to message Bob, she must first establish a contract. No contract, no communication.

Alice initiates:

alice_ephemeral = X25519_generate()   // fresh key pair for this contract only

contract = {
  requestor_did:                   alice_did,
  recipient_did:                   bob_did,
  requestor_encryption_public_key: alice_ephemeral.public,
  recipient_encryption_public_key: null,    // Bob fills this in
  expires_at:                      now + 30_days,
  timestamp:                       now
}

requestor_signature = Ed25519_sign(contract, alice_signing_private)

// Encrypt so only Bob can read it — not Bob's mediator
wrapping_key = X25519(alice_ephemeral.private, bob_prekey_public)
encrypted_request = AES_GCM_encrypt(contract, wrapping_key)

Alice sends { encrypted_request, alice_ephemeral.public } to Bob's mediator. The mediator stores it as an opaque blob. It can verify Alice's identity from the command signature, but cannot read the contract contents.

Bob accepts:

// Decrypt using his pre-key
wrapping_key = X25519(bob_prekey_private, alice_ephemeral.public)
contract = AES_GCM_decrypt(encrypted_request, wrapping_key)

// Verify Alice's signature
verify(requestor_signature, contract, alice_signing_public)

// Generate Bob's ephemeral key pair
bob_ephemeral = X25519_generate()
contract.recipient_encryption_public_key = bob_ephemeral.public

// Sign the completed contract
recipient_signature = Ed25519_sign(contract, bob_signing_private)

Bob sends the fully signed contract back to Alice's mediator and stores a copy on his own.

Root secret derivation:

Both parties independently derive the identical shared secret:

Alice:  root_secret = X25519(alice_ephemeral.private, bob_ephemeral.public)
Bob:    root_secret = X25519(bob_ephemeral.private, alice_ephemeral.public)

No secret was transported. The root secret exists only as a local mathematical derivation on each device. The ephemeral keys are unique to this contract — compromising one contract doesn't affect any other.

4. Alice Sends Bob a Message

With a contract established, Alice can send an encrypted event:

// 1. Build the event
event = { type: "chat.message", data: { chatId: "abc", content: "Hello Bob" } }

// 2. Build a signed envelope
envelope = { contract_id, event: JSON.stringify(event), timestamp: now }
signature = Ed25519_sign(envelope, alice_signing_private)

// 3. Encrypt for transit → send to Bob's mediator
transit_payload = AES_GCM_encrypt(signed_envelope, root_secret)
send_command(bob_mediator, TWO_WAY_PRIVATE, { payload: transit_payload })

// 4. Encrypt for self-storage → save on Alice's mediator
storage_payload = AES_GCM_encrypt(event, alice_storage_key)
encrypted_tags  = [sign("chat"), sign("chat.abc")]
send_command(alice_mediator, SAVE_EVENTS, { payload: storage_payload, tags: encrypted_tags })

This dual-delivery ensures both parties maintain complete records on their own infrastructure. Alice's mediator has her copy, Bob's mediator has his — each encrypted with different keys.

5. Bob Receives the Message

When Bob's client connects (HTTP poll or WebSocket push):

// 1. Query pending events
pending = query_pending_events(bob_mediator)

for each pending_event:
    // 2. Find the contract, derive the root secret
    contract = find_contract(pending_event.sender_did)
    root_secret = X25519(bob_ephemeral.private, alice_ephemeral.public)

    // 3. Decrypt and verify
    envelope = AES_GCM_decrypt(pending_event.payload, root_secret)
    verify(envelope.signature, envelope, alice_signing_public)

    // 4. Re-encrypt for self-storage
    storage_payload = AES_GCM_encrypt(envelope.event, bob_storage_key)
    save_events(bob_mediator, { payload: storage_payload, tags: [...] })

// 5. Acknowledge processed events
acknowledge(bob_mediator, processed_ids)

Bob decrypts with the root secret, verifies Alice's Ed25519 signature, then re-encrypts with his own storage key. His mediator has never seen the plaintext. Neither has Alice's mediator. The only devices that ever see "Hello Bob" are Alice's phone and Bob's phone.

What Just Happened

In five steps, Alice and Bob established an encrypted communication channel with these properties:

  • No server saw the message — mediators stored and forwarded ciphertext only
  • No server saw the encryption keys — root secret was derived locally via Diffie-Hellman
  • No server can forge messages — every event carries an Ed25519 signature
  • Both parties consented — the bilateral contract required both signatures
  • Both parties have complete records — dual-delivery on independent infrastructure
  • Either party can revoke — terminating the contract stops all communication immediately
  • The relationship is portable — switch mediators anytime without losing contacts