Хабрахабр

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

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

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

Сегодня мы расскажем, как устроен мессенджер на блокчейне и как клиентским приложениям работать с его 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=, а при загрузке чатов публичные ключи собеседников уже будут в наличии.

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

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

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

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

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

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

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

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

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

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»