Messaging

in mediator document, we have covered the mediator basics and the different ways of communication it supports. But how does Decentrl messaging work?

Decentrl messaging could be compared to an event-driven microservice design, where each client (identity) acts as a service in a big network where mediators act as message brokers responsible for delivering messages to identities.

Interacting with mediator

To start interacting with a mediator, identity first has to establish websocket connection.

Mediator message types

Communication with the mediator is made of two different types of messages

  1. Commands

  2. Events

Commands

Commands are a type of message that instructs the mediator to do an action. Below are a few examples of possible commands

  1. Request challenge

  2. Authenticate with challenge

  3. Forward Instructs Mediator to forward a specific message to identity

Command payload should be a DIDComm encrypted message with a type property describing the command type. Below is an example of how the command payload could look before being encrypted.

{
  "id": "2a2e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "AUTHENTICATE_WITH_CHALLENGE",
  "body": {
    "challenge": "signed challenge",
  },
}

Before the command is sent to the mediator, it should be encrypted using the routing public key, which can be found inside the mediators DID document.

Events

Events are a type of message that notifies the identity of what has happened. Below are a few examples of possible events

  1. Challenge generated

  2. Authentication completed

  3. Authentication failed

  4. Message forwarded

The event payload should look something like this

{
  "event": "CHALLENGE_GENERATED",
  "data": {
    "challenge": "challenge"
  },
}


{
  "event": "AUTHENTICATION_COMPLETED",
  "data": {
    "token": "jwt",
    "expiresAt": 1678442275,
  },
}

Authentication

Once a web socket connection with Mediator is established, authentication has to be performed regardless if you are registered on that mediator or not. Authentication is done so the mediator knows which web socket connection belongs to which identity.

Mediators should support two types of authentication

  1. Authentication with challenge

  2. Authentication with token

With challenge

When authenticating with a challenge, identity first has to request the challenge from the mediator using the REQUEST_CHALLENGE command. This command has no payload.

The mediator will respond with a CHALLENGE_GENERATED event, which should include the challenge in the payload. Challenge is a JWT with an expiration time of 2 minutes.

Once the challenge is retrieved, it can be packed into the AUTHENTICATE_WITH_CHALLENGE command, encrypted, and sent to the mediator.

Once the mediator receives the AUTHENTICATE_WITH_CHALLENGE command, it first decrypts it and verifies the sender's identity. Once the verification is done, the mediator verifies the signed challenge. If all verifications pass authentication is completed and the mediator sends AUTHENTICATION_COMPLETED event to the identity. The event payload should look like this

{
  "event": "AUTHENTICATION_COMPLETED",
  "data": {
    "token": "jwt",
    "expiresAt": 1678442275,
  },
}

If authentication fails AUTHENTICATION_FAILED event will be sent to the identity.

With token

If the identity has a valid token (that was returned by the challenge authentication method) they can authenticate by skipping the challenge-generating part. To authenticate with the token, an AUTHENTICATE_WITH_TOKEN command should be sent to the mediator, with the following body

{
  "token": "JWT"
}

Depending on the validity of the token, the mediator will respond with AUTHENTICATION_COMPLETED or AUTHENTICATION_FAILED events respectively.

Interacting between identities

Having covered the types of communication mediators support, and the communication with the mediator itself, we can talk about how communication between identities should look like.

In the previous example, the Decentrl protocol was compared to an event-driven microservice architecture where each identity is a service and the mediator is a message broker.

The messaging between two clients (identities) should be event-based as it would be in such a system. That means that each client builds its own state locally by aggregating the events received from others.

Example 1 - Basic Chat

This example will describe how you could implement basic chat between two identities using Decentrl.

As mentioned before, clients (identities) should not rely on mediators to build their state, but rather build it locally. By doing that, the privacy aspect is increased as the mediators don't have to know anything about the message that's being exchanged by clients. This not only increases privacy but also enables developers to develop any kind of application on top of Decentrl, as long as message transfer is supported by the mediator. One of the applications that could be easily developed is E2EE chat between two identities.

For this application two-way private communication will be used to transfer messages between two identities.

Let's say we have two different actors, Bob and Alice. Each one of them has its own DID. In order to send Alice a message, Bob first needs to know her DID. He can either get her DID by some other communication channel or by searching one of the public namespaces.

Bob: DID Resolution & Mediator connection

Bob first has to resolve Alices' DID to get a list of her mediators. Once the DID document is fetched, Bob has to connect and authenticate on Alices' mediators. Note: Bob does not have to connect to all mediators but to at least one. Connecting and sending messages to multiple mediators is preferred as it will increase the message delivery rate.

Because DIDs are randomly generated, it is very unlikely for an attacker to guess someone's DID and start spamming them unless that identity is registered on some public namespace, where the DID is open to public.

Bob: State change & Event emission

With a mediator connection established, Bob can send the message to Alice. The message should be in a form of an event that reflects the change that happened to Bobs' local client. Once Bob clicks to send a message, his local client updates the local state and adds the message to the store. This local-state change produces an event that can be forwarded to Alice, so she can also update her local state accordingly. The event should be in the format of a DIDComm message and should look something like this:

{
  "id": "2a2e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "DecentrlMessageSent",
  "body": {
    "messageBody": "Hello Alice, how are you doing?",
  },
}

The message is transported to Alice using a two-way private communication mediator feature, meaning that the message is first encrypted using Alices' public key. The encrypted text is then inserted into a mediator command payload, encrypted with the mediators' public key, and sent to it.

Alice: Event retrieval & local state update

In order for Alice to retrieve the events she first has to connect to all her mediator nodes to collect the messages. If Alices' connection to mediators was established at the time when Bob sent his message, mediators would automatically push the message to Alice. If the connection was not established, Alice has to ask the mediator to send her all messages that came in while she was offline via the message search command.

Once the mediator returns the message from Bob, Alice first has to decrypt it using her private key and Bobs' public key. This operation is also called Authcrypt as it verifies that the message was sent from Bob by requiring his public key to decrypt it. Once the message is decrypted, Alice can update her local state to reflect the change that Bob has made on his end, in this case, sending a new message.

Due to the fact that it is encouraged to send the same message to multiple mediators, Alice will receive a lot of duplicate messages from different mediators. The unencrypted version of the message should have the same ID, so Alice can ignore the duplicates when updating her local state.

Alice: Reacting to Bobs' message

Alice received and read the message, but she did not have time to reply. She decided to react to the message instead. The message reaction is first reflected in her local state and after that, the event describing that change is emitted to Bobs' mediators, so Bob can update his local state. The event would look something like this

{
  "id": "335e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "DecentrlMessageReaction",
  "body": {
    "reactionType": "like",
    "messageId": "2a2e5f12-1b4c-42be-ac43-152b2156cfab",
  },
}

Although you can send messages of any type between identities and create identity clients that support any kind of feature, Decentrl will have standards for implementing things such as chat standardized to enable interoperability between different identity client implementations.

Two-way private communication

This type of communication enables features such as chat. When identity registers on a mediator and picks this as one of the communication channels they wish to use, they will open a channel through which others who know their DID will be able to send them messages.

The two-way private communication feature implements two commands on the mediator.

  1. Forward command - instructs mediator to forward a specific encrypted message to an identity that's registered on that mediator

  2. Search messages command - adds the ability for identity to fetch messages from their mediators.

Forward command

Instructs mediator to forward a specific encrypted message to an identity that's registered on that mediator. The forward command extends DIDComm and should look like this

{
  "id": "335e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "FORWARD",
  "body": {
    "next": "identity_DID",
  },
  "attachments": [
    {
      "media_type": "application/didcomm-encrypted",
      "data": {
        "jwe": "encrypted message",
      }
    }
  ],
}
  1. The command type should be FORWARD

  2. The body should contain a property called next which should have the recipient identity DID as the value

  3. The encrypted message that is to be forwarded to the identity needs to be added to the command as an attachment

When sending forwarding a message, identity could also forward the copy of the message to themselves for backup purposes. That way their mediators will also contain their events and not only events that other identities produced related to them.

After the mediator receives a command such as this, it should first check if the recipient identity has an active web socket connection. In the case it does, it should forward the message directly. If not, the identity will have to fetch the message from the mediator using the search messages command.

Additional Metadata

When forwarding a message to a mediator, identity can choose to add additional metadata that could optimize the user experience as well as message searching.

If we refer to Example 1, Alice could attach a message ID metadata to her reaction message. The forward command would look something like this

{
  "id": "335e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "FORWARD",
  "body": {
    "next": "BOBS_DID",
  },
  "attachments": [
    {
      "media_type": "application/didcomm-encrypted",
      "data": {
        "jwe": "encrypted reaction message",
      }
    }
  ],
  "metadata": {
    "messageId": "2a2e5f12-1b4c-42be-ac43-152b2156cfab",
  }
}

With the metadata sent to the mediator, Bob could now filter message search results by metadata values. In this case, Bob could only fetch all events related to his original message by filtering by "metadata.messageId": "2a2e5f12-1b4c-42be-ac43-152b2156cfab".

When working with Metadata developers need to be careful not to compromise the privacy and security of their clients.

Search messages command

Adds the ability for identity to fetch messages from their mediators. The commnd should look something like that

{
  "id": "335e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "SEARCH_MESSAGES",
  "body": {
    "senderDid": "sender_DID",
    "olderThen": 1678442275,
    "newerThen": 1678442275,
    "maxResults": 10,
    "metadata": {
      "messageId": "2a2e5f12-1b4c-42be-ac43-152b2156cfab",
    },
  },
}
  1. The command type should be SEARCH_MESSAGES

  2. The body should have properties as described above. All of these properties are optional.

After receiving a command such as this, the mediator will query its database and pull out all messages that match the following filters, and send it back to the requester using the MESSAGE_SEARCH_RESULT event. The event should look something like this

{
  "event": "MESSAGE_SEARCH_RESULT",
  "data": {
    "messages": [
      "encrypted message jwe",
      "encrypted message jwe",
      "encrypted message jwe",
      "encrypted message jwe",
    ],
  },
}

One-way public communication

This type of communication enables features such as status updates. When identity registers on a mediator and picks this as one of the communication channels they wish to use, they will open a channel through which others who know their DID will be able to read their public messages but won't be able to react or send a message back to the original sender.

The one-way public communication feature implements two commands on the mediator.

  1. Post command - instruct the mediator to save a message that can be fetched by anyone

  2. Search posts command - adds the ability for anyone who knows Identities DID to query their public posts.

Post command

Instruct the mediator to save a message that can be fetched by anyone. The post command extends DIDComm and should look like this

{
  "id": "335e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "PUBLIC_POST",
  "body": {
    "jws": "signed public message",
  },
  "metadata": {
    "messageId": "2a2e5f12-1b4c-42be-ac43-152b2156cfab",
  }
}
  1. The command type should be PUBLIC_POST

  2. The command body should include a JSON web signature of a DIDComm extended message

  3. Metadata can be attached to the command the same way as on two-way private communication.

Search posts command

Adds the ability for anyone who knows Identities DID to query their public posts. The search command should look something like this

{
  "id": "335e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "SEARCH_PUBLIC_POSTS",
  "body": {
    "authorDid": "author_DID",
    "olderThen": 1678442275,
    "newerThen": 1678442275,
    "maxResults": 10,
    "metadata": {
      "messageId": "2a2e5f12-1b4c-42be-ac43-152b2156cfab",
    },
  },
}
  1. The command type should be SEARCH_PUBLIC_POSTS.

  2. The body should have properties as described above. All properties except authorDid are optional.

After receiving a command such as this, the mediator will query its database and pull out all messages that match the following filters and send it back to the requester using the PUBLIC_POST_SEARCH_RESULT event. The event should look something like this

{
  "event": "PUBLIC_POST_SEARCH_RESULT",
  "data": {
    "messages": [
      "message jws",
      "message jws",
      "message jws",
      "message jws",
    ],
  },
}

To get the complete picture of someone's public posts, public posts should be fetched from all mediators the identity registered on.

Example 2: Public posts wall

This example will describe how you could implement basic social network client, that displays identities public posts.

For this application one-way public communication will be used for publishing and reading posts.

Let's say we have two different actors. Bob, who is a famous entrepreneur and Alice who is a follower of her work.

Bob: Create a public-facing identity

Bob is a public figure and he wants his followers to know what he's up to, but he doesn't want to be messaged back by his followers as it would most likely result in a lot of spam. That's why Bob decides to create a new DID, which purpose is to be a public-facing identity with no way of being contacted. To do that, he registers his DID on mediators with only a one-way public communication feature enabled. This way, other DIDs will be able to query his public posts but will not be able to send him a message as he doesn't have a two-way private communication feature enabled.

Bob: Post a public message

Bob has just released a new product and he wants to share it with the world. He first creates a message informing others about his new product. The message would look something like this

{
  "id": "2a2e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "DecentrlPostCreated",
  "body": {
    "postBody": "Just released a new product! Check it out at https://decentrl.network",
  },
}

After the message is made, Bob signs it using his public signing key and posts it to his mediators using the PUBLIC_POST command.

{
  "id": "445e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "PUBLIC_POST",
  "body": {
    "jws": "signed public message",
  },
}

Once the command is sent to the mediators, they save it to their database, and the message is now avaiable to be fetched by anyone who knows Bob's DID.

Alice: Read Bob's public message

Alice decides to see what Bob's been up to, so she connects to Bob's public DIDs mediators and sends the SEARCH_PUBLIC_POSTS command.

{
  "id": "335e5f12-1b4c-42be-ac43-152b2156cfab",
  "create_time": 1678442275,
  "type": "SEARCH_PUBLIC_POSTS",
  "body": {
    "authorDid": "BOBs_DID",
    "maxResults": 10,
  },
}

After the command executes on the mediator, Alice receives the array of Bobs status updates where she sees that he just released a new product! At first she's skeptical about the authenticity of the messages and integrity of the mediators but after she verifies the message signatures, she is sure that Bob indeed released a new product!

Last updated