В начале 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–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
— noncetype
— тип сообщения
Сообщения тоже делятся на типы. По сути, параметр 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, неудавшаяся из-за большого времени доставки сообщений, нагрузки на процессор и отсутствия мобильных приложений.
А скептицизм связан с тем, что мессенджеры на блокчейне опережают время — люди не готовы брать ответственность за свой аккаунт на себя, владение личной информацией пока не в тренде, а технологии не позволяют обеспечить высокие скорости на блокчейне. Следом будут появляться более технологичные аналоги нашего проекта. Вот увидите.
Автор: JustDeveloper