Pull to refresh

Как работает децентрализованный мессенджер на блокчейне

Reading time8 min
Views29K
В начале 2017 мы начали создавать мессенджер на блокчейне [название и ссылка есть в профиле] с обсуждения преимуществ перед классическими P2P-мессенджерами.

Прошло 2.5 года, и нам удалось подтвердить свой концепт: сейчас доступны приложения мессенджера для iOS, Web PWA, Windows, GNU/Linux, Mac OS и Android.

Сегодня мы расскажем, как устроен мессенджер на блокчейне и как клиентским приложениям работать с его API.


Мы хотели, чтобы блокчейн решил вопросы безопасности и приватности классических P2P-мессенджеров:

  • Один клик для создания аккаунта — никаких телефонов и электронных почт, нет доступа к адресным книгам и геолокациям.
  • Собеседники никогда не устанавливают прямых соединений, все общение идет через распределенную систему узлов. IP-адресы пользователей недоступны друг другу.
  • Все сообщения шифруются End-to-End curve25519xsalsa20poly1305. Вроде бы этим никого не удивишь, но у нас-то исходный код открыт.
  • MITM-атака исключена — каждое сообщение является транзакцией и подписывается Ed25519 EdDSA.
  • Сообщение попадает в свой блок. Последовательность и timestamp блоков не исправишь, а следовательно и порядок сообщений.
  • “Я этого не говорил” не прокатит с сообщениями в блокчейне.
  • Нет центральной структуры, которая делает проверки на “достоверность” сообщения. Это делает распределенная система узлов на основе консенсуса, а она принадлежит пользователям.
  • Невозможность цензуры — аккаунты нельзя блокировать, а сообщения удалять.
  • Блокчейн 2FA — альтернатива адской 2FA по SMS, поломавшей немало здоровья.
  • Возможность получить все свои диалоги с любого устройства в любое время — это возможность не хранить диалоги локально вообще.
  • Подтверждение доставки сообщений. Не на устройство пользователя, а в сеть. По сути, это подтверждение возможности получателя прочитать ваше сообщение. Это полезная фича для отправки критических уведомлений.

Из плюшек блокчейна также тесная интеграция с криптовалютами Ethereum, Dogecoin, Lisk, Dash, Bitcoin (этот пока в процессе) и возможность отправки токенов в чатах. Мы даже сделали встроенный крипто-обменник.

А дальше — как все это работает.

Сообщение — это транзакция


Все уже привыкли, что транзакции в блокчейне передают токены (монеты) от одного пользователя другому. Как у биткоина. Мы же создали особый тип транзакций для передачи сообщений.

Чтобы отправить сообщение в мессенджере на блокчейне, нужно пройти несколько этапов:

  1. Зашифровать текст сообщения
  2. Поместить зашифрованный текст в транзакцию
  3. Подписать транзакцию
  4. Отправить транзакцию на любой узел сети
  5. Распределенная система узлов определяет “достоверность” сообщения
  6. Если все ОК — транзакция с сообщением включается в следующий блок
  7. Получатель извлекает транзакцию с сообщением и расшифровывает

Этапы 1–3 и 7 выполняются локально на клиенте, а 5–6 — на узлах сети.

Шифрование сообщения


Сообщение шифруется приватным ключом отправителя и публичным ключом получателя. Публичный ключ мы возьмем из сети, но для этого аккаунт получателя должен быть инициализирован, то есть иметь хотя бы одну транзакцию. Можно использовать REST-запрос GET /api/accounts/getPublicKey?address={ADAMANT address}, а при загрузке чатов публичные ключи собеседников уже будут в наличии.



Мессенджер шифрует сообщения алгоритмом curve25519xsalsa20poly1305 (NaCl Box). Поскольку аккаунт содержит ключи Ed25519, для формирования box’а предварительно ключи нужно преобразовать в Curve25519 Diffie-Hellman.

Вот пример на 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)
  }
}

Формирование транзакции с сообщением


Транзакция имеет такую общую структуру:

{
  "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": {}
}

Для транзакции-сообщения самое важное значение имеет asset — в него нужно разместить сообщение в объекте chat со структурой:

  • message — сохраняем зашифрованное сообщение
  • own_message — nonce
  • type — тип сообщения

Сообщения тоже делятся на типы. По сути, параметр type сообщает, как понимать message. Можно отправить просто текст, а можно объект с интересностями внутри — например, так мессенджер делает переводы криптовалют в чатах.

В итоге мы формируем транзакцию:

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

Подпись транзакции


Чтобы все были уверены в достоверности отправителя и получателя, во времени отправки и содержимом сообщения, транзакцию подписывают. Цифровая подпись позволяет проверить достоверность транзакции по публичному ключу — приватный ключ для этого не нужен.

А вот сама подпись как раз выполняется приватным ключом:



Из схемы видно, что транзакцию сначала хешируем SHA-256, а потом подписываем Ed25519 EdDSA и получаем подпись signature, а идентификатор транзакции — это часть SHA-256-хэша.

Пример реализации:


1 — Формируем блок данных, включая сообщение

/**
 * 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 — Считаем SHA-256 от блока данных

/**
 * 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 — Подписываем транзакцию

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'))
}

Отправка транзакции с сообщением на узел сети


Поскольку сеть децентрализованная, подойдет любой из узлов с открытым API. Делаем POST-запрос на эндпоинт api/transactions:

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

В ответ получим ID транзакции типа

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

Проверка достоверности транзакции


Распределенная система узлов на основе консенсуса определяет “достоверность” транзакции-сообщения. От кого и кому, когда, не заменили ли сообщение другим, а правильное ли указано время отправки. Это очень важное преимущества блокчейна — нет центральной структуры, которая отвечает за проверки, и последовательность сообщений и их содержимое не подделать.

Сначала достоверность проверяет одна нода, а потом рассылает другим — если большинство говорят, что все в порядке, транзакция будет включена в следующий блок цепи — это и есть консенсус.



Часть кода узла, которая отвечает за проверки, можно посмотреть в GitHub — validator.js и verify.js. Ага, узел работает на Node.js.

Включаем транзакцию с сообщением в блок


Если консенсус достигнут, транзакция с нашим сообщением попадет в следующий блок наряду с другими достоверными транзакциями.

Блоки имеют строгую последовательность, и каждый последующий блок формируется на основе хешей предыдущих блоков.



Суть в том, что наше сообщение также включено в эту последовательность и не может быть “переставлено”. Если в блок попадает несколько сообщений, их порядок будет определен по timestamp сообщений.

Чтение сообщений


Приложение-мессенджер извлекает транзакции из блокчейна, которые отправлены адресату. Для этого мы сделали эндпоинт api/chatrooms.

Все транзакции доступны для каждого — можно получить зашифрованные сообщения. А вот расшифровать сможет только получатель своим приватным ключом и публичным ключом отправителя:

**
 * 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) : ''
}

А что еще?


Поскольку сообщения таким способом доставляются около 5 секунд — это время появления нового блока сети — мы придумали сокет-подключение клиент-узел и узел-узел. Когда узел получает новую транзакцию, он проверяет ее валидность и передает на другие узлы. Транзакция доступна клиентам-мессенджерам еще до наступления консенсуса и включения в блок. Так мы будем доставлять сообщения мгновенно, как и привычные мессенджеры.

Чтобы хранить адресную книгу, мы сделали KVS — Key-Value Storage — это еще один тип транзакций, в которых asset шифруется не NaCl-box, а NaCl-secretbox. Так мессенджер хранит и другие данные.

Передача файлов/изображений и групповые чаты требуют еще много работы. Конечно, в формате тяп-ляп это можно “прикрутить” быстро, но мы хотим сохранить тот же уровень приватности.

Да, есть еще над чем работать — в идеале реальная приватность предполагает, что пользователи не будут подключаться к публичным узлам сети, а поднимут свои. Как вы думаете, сколько процентов пользователей так делает? Правильно, 0. Частично этот вопрос нам удалось решить Tor-версией мессенджера.

Мы доказали, что мессенджер на блокчейне может существовать. Ранее была только одна попытка в 2012 году — bitmessage, неудавшаяся из-за большого времени доставки сообщений, нагрузки на процессор и отсутствия мобильных приложений.

А скептицизм связан с тем, что мессенджеры на блокчейне опережают время — люди не готовы брать ответственность за свой аккаунт на себя, владение личной информацией пока не в тренде, а технологии не позволяют обеспечить высокие скорости на блокчейне. Следом будут появляться более технологичные аналоги нашего проекта. Вот увидите.
Tags:
Hubs:
+24
Comments77

Articles