How does a decentralized messenger on the blockchain work?

At the beginning of 2017, we started creating a messenger on the blockchain [the name and link is in the profile] by discussing the advantages over classic P2P messengers.

Passed 2.5 years, and we managed to confirm our concept: instant messenger applications for iOS, Web PWA, Windows, GNU/Linux, Mac OS and Android are now available.

Today we will tell you how the blockchain messenger works and how client applications work with its API.
How does a decentralized messenger on the blockchain work?

We wanted the blockchain to solve the security and privacy issues of classic P2P messengers:

  • One click to create an account - no phones and emails, no access to address books and geolocations.
  • The interlocutors never establish direct connections, all communication goes through a distributed system of nodes. User IP addresses are inaccessible to each other.
  • All messages are encrypted with End-to-End curve25519xsalsa20poly1305. It seems that this will not surprise anyone, but we have a source code open.
  • MITM attack is ruled out - each message is a transaction and is signed by Ed25519 EdDSA.
  • The message goes into its own block. Sequence and timestamp Blocks cannot be corrected, and hence the order of messages.
  • “I didn’t say that” won’t work with messages on the blockchain.
  • There is no central structure that makes checks for the "authenticity" of the message. This is done by a distributed system of nodes based on consensus, and it is owned by users.
  • Impossibility of censorship - accounts cannot be blocked, and messages cannot be deleted.
  • Blockchain 2FA is an alternative to hellish 2FA via SMS, broken a lot of health.
  • The ability to get all your dialogs from any device at any time is the ability to not store dialogs locally at all.
  • Message delivery confirmation. Not to the user's device, but to the network. In fact, this is a confirmation of the recipient's ability to read your message. This is a useful feature for sending critical notifications.

Of the benefits of the blockchain, there is also close integration with the cryptocurrencies Ethereum, Dogecoin, Lisk, Dash, Bitcoin (this one is still in progress) and the ability to send tokens in chats. We even made a built-in crypto-exchanger.

And then - how it all works.

Message is a transaction

Everyone is already used to the fact that blockchain transactions transfer tokens (coins) from one user to another. Like bitcoin. We have created a special type of transaction for message passing.

To send a message in a messenger on the blockchain, you need to go through several steps:

  1. Encrypt message text
  2. Put ciphertext into transaction
  3. Sign transaction
  4. Send a transaction to any network node
  5. A distributed system of nodes determines the “credibility” of a message
  6. If everything is OK, the transaction with the message is included in the next block
  7. The recipient retrieves the transaction with the message and decrypts

Steps 1-3 and 7 are performed locally on the client, and steps 5-6 are performed on the hosts.

Message encryption

The message is encrypted with the sender's private key and the recipient's public key. We will take the public key from the network, but for this the recipient's account must be initialized, that is, have at least one transaction. You can use REST request GET /api/accounts/getPublicKey?address={ADAMANT address}, and when loading chats, the public keys of the interlocutors will already be available.

How does a decentralized messenger on the blockchain work?

The messenger encrypts messages using the algorithm curve25519xsalsa20poly1305 (NaCl Box). Since the account contains Ed25519 keys, to form the box, the keys must first be converted to Curve25519 Diffie-Hellman.

Here is an example in JavaScript:

/**
 * Encodes a text message for sending to ADM
 * @param {string} msg message to encode
 * @param {*} recipientPublicKey recipient's public key
 * @param {*} privateKey our private key
 * @returns {{message: string, nonce: string}}
 */
adamant.encodeMessage = function (msg, recipientPublicKey, privateKey) {
  const nonce = Buffer.allocUnsafe(24)
  sodium.randombytes(nonce)

  if (typeof recipientPublicKey === 'string') {
    recipientPublicKey = hexToBytes(recipientPublicKey)
  }

  const plainText = Buffer.from(msg)
  const DHPublicKey = ed2curve.convertPublicKey(recipientPublicKey)
  const DHSecretKey = ed2curve.convertSecretKey(privateKey)

  const encrypted = nacl.box(plainText, nonce, DHPublicKey, DHSecretKey)

  return {
    message: bytesToHex(encrypted),
    nonce: bytesToHex(nonce)
  }
}

Forming a transaction with a message

A transaction has the following general structure:

{
  "id": "15161295239237781653",
  "height": 7585271,
  "blockId": "16391508373936326027",
  "type": 8,
  "block_timestamp": 45182260,
  "timestamp": 45182254,
  "senderPublicKey": "bd39cc708499ae91b937083463fce5e0668c2b37e78df28f69d132fce51d49ed",
  "senderId": "U16023712506749300952",
  "recipientId": "U17653312780572073341",
  "recipientPublicKey": "23d27f616e304ef2046a60b762683b8dabebe0d8fc26e5ecdb1d5f3d291dbe21",
  "amount": 204921300000000,
  "fee": 50000000,
  "signature": "3c8e551f60fedb81e52835c69e8b158eb1b8b3c89a04d3df5adc0d99017ffbcb06a7b16ad76d519f80df019c930960317a67e8d18ab1e85e575c9470000cf607",
  "signatures": [],
  "confirmations": 3660548,
  "asset": {}
}

For a message transaction, the most important thing is asset - it needs to place a message in the object chat with structure:

  • message - save the encrypted message
  • own_message nonce
  • type - message type

Messages are also divided into types. Essentially, the parameter type tells how to understand message. You can send just a text, or you can send an object with interesting things inside - for example, this is how the messenger makes cryptocurrency transfers in chats.

As a result, we form a transaction:

{
  "transaction": {
    "type": 8,
    "amount": 0,
    "senderId": "U12499126640447739963",
    "senderPublicKey": "e9cafb1e7b403c4cf247c94f73ee4cada367fcc130cb3888219a0ba0633230b6",
    "asset": {
      "chat": {
        "message": "cb682accceef92d7cddaaddb787d1184ab5428",
        "own_message": "e7d8f90ddf7d70efe359c3e4ecfb5ed3802297b248eacbd6",
        "type": 1
      }
    },
    "recipientId": "U15677078342684640219",
    "timestamp": 63228087,
    "signature": "тут будет подпись"
  }
}

Transaction signature

In order for everyone to be sure of the authenticity of the sender and recipient, the time of sending and the content of the message, the transaction is signed. A digital signature allows you to verify the authenticity of a transaction using a public key - a private key is not needed for this.

But the signature itself is just performed by a private key:

How does a decentralized messenger on the blockchain work?

It can be seen from the diagram that the transaction is first hashed by SHA-256, and then signed Ed25519 EdDSA and get a signature signature, and the transaction ID is part of the SHA-256 hash.

Implementation example:

1 - We form a data block, including a message

/**
 * Calls `getBytes` based on transaction type
 * @see privateTypes
 * @implements {ByteBuffer}
 * @param {transaction} trs
 * @param {boolean} skipSignature
 * @param {boolean} skipSecondSignature
 * @return {!Array} Contents as an ArrayBuffer.
 * @throws {error} If buffer fails.
 */

adamant.getBytes = function (transaction) {

  ...

  switch (transaction.type) {
    case constants.Transactions.SEND:
      break
    case constants.Transactions.CHAT_MESSAGE:
      assetBytes = this.chatGetBytes(transaction)
      assetSize = assetBytes.length
      break

…

    default:
      alert('Not supported yet')
  }

  var bb = new ByteBuffer(1 + 4 + 32 + 8 + 8 + 64 + 64 + assetSize, true)

  bb.writeByte(transaction.type)
  bb.writeInt(transaction.timestamp)

  ...

  bb.flip()
  var arrayBuffer = new Uint8Array(bb.toArrayBuffer())
  var buffer = []

  for (var i = 0; i < arrayBuffer.length; i++) {
    buffer[i] = arrayBuffer[i]
  }

  return Buffer.from(buffer)
}

2 - We consider SHA-256 from the data block

/**
 * Creates hash based on transaction bytes.
 * @implements {getBytes}
 * @implements {crypto.createHash}
 * @param {transaction} trs
 * @return {hash} sha256 crypto hash
 */
adamant.getHash = function (trs) {
  return crypto.createHash('sha256').update(this.getBytes(trs)).digest()
}

3 - We sign the transaction

adamant.transactionSign = function (trs, keypair) {
  var hash = this.getHash(trs)
  return this.sign(hash, keypair).toString('hex')
}

/**
 * Creates a signature based on a hash and a keypair.
 * @implements {sodium}
 * @param {hash} hash
 * @param {keypair} keypair
 * @return {signature} signature
 */
adamant.sign = function (hash, keypair) {
  return sodium.crypto_sign_detached(hash, Buffer.from(keypair.privateKey, 'hex'))
}

Sending a transaction with a message to a host

Since the network is decentralized, any of the nodes with a public API will do. Making a POST request to an endpoint api/transactions:

curl 'api/transactions' -X POST 
  -d 'TX_DATA'

In response, we will receive a transaction ID of the type

{
    "success": true,
    "nodeTimestamp": 63228852,
    "transactionId": "6146865104403680934"
}

Transaction Validation

A distributed system of nodes based on consensus determines the “authenticity” of a transaction-message. From whom and to whom, when, whether the message was replaced by another, and whether the time of sending is correct. This is a very important advantage of the blockchain - there is no central structure that is responsible for verification, and the sequence of messages and their content cannot be faked.

First, one node checks the authenticity, and then sends it to others - if the majority say that everything is in order, the transaction will be included in the next block of the chain - this is the consensus.

How does a decentralized messenger on the blockchain work?

The part of the node code that is responsible for the checks can be viewed on GitHub - validator.js и verify.js. Yep, node runs on Node.js.

Include a transaction with a message in a block

If consensus is reached, the transaction with our message will go into the next block along with other valid transactions.

Blocks have a strict sequence, and each subsequent block is formed based on the hashes of previous blocks.

How does a decentralized messenger on the blockchain work?

The bottom line is that our message is also included in this sequence and cannot be “rearranged”. If there are multiple messages in a block, their order will be determined by timestamp messages.

Reading messages

The messenger application retrieves transactions from the blockchain that are sent to the addressee. To do this, we made an endpoint api/chatrooms.

All transactions are available to everyone - you can receive encrypted messages. But only the recipient can decrypt with his private key and the public key of the sender:

**
 * Decodes the incoming message
 * @param {any} msg encoded message
 * @param {string} senderPublicKey sender public key
 * @param {string} privateKey our private key
 * @param {any} nonce nonce
 * @returns {string}
 */
adamant.decodeMessage = function (msg, senderPublicKey, privateKey, nonce) {
  if (typeof msg === 'string') {
    msg = hexToBytes(msg)
  }

  if (typeof nonce === 'string') {
    nonce = hexToBytes(nonce)
  }

  if (typeof senderPublicKey === 'string') {
    senderPublicKey = hexToBytes(senderPublicKey)
  }

  if (typeof privateKey === 'string') {
    privateKey = hexToBytes(privateKey)
  }

  const DHPublicKey = ed2curve.convertPublicKey(senderPublicKey)
  const DHSecretKey = ed2curve.convertSecretKey(privateKey)
  const decrypted = nacl.box.open(msg, nonce, DHPublicKey, DHSecretKey)

  return decrypted ? decode(decrypted) : ''
}

And what else?

Since messages are delivered in this way for about 5 seconds - this is the time a new block of the network appears - we came up with a socket connection client-to-node and node-to-node. When a node receives a new transaction, it checks its validity and passes it on to other nodes. The transaction is available to messenger clients even before consensus and inclusion in the block. So we will deliver messages instantly, like the usual messengers.

To store the address book, we made KVS - Key-Value Storage - this is another type of transaction in which asset encrypted not with NaCl-box, but NaCl-secretbox. So the messenger stores other data.

File/image transfer and group chats still require a lot of work. Of course, in a blunder format, this can be “fastened” quickly, but we want to maintain the same level of privacy.

Yes, there is still work to be done - ideally, real privacy assumes that users will not connect to public network nodes, but will raise their own. What percentage of users do you think do this? That's right, 0. Partially, we managed to solve this issue with the Tor version of the messenger.

We proved that a blockchain messenger can exist. Previously, there was only one attempt in 2012 - bitmessage, which failed due to the high message delivery time, processor load and lack of mobile applications.

And skepticism is due to the fact that blockchain messengers are ahead of their time - people are not ready to take responsibility for their account, the possession of personal information is not yet in trend, and technology does not allow high speeds on the blockchain. More technological analogues of our project will appear next. Here you will see.

Source: habr.com

Add a comment