Як працює децентралізований месенджер на блокчейні

На початку 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, що невдала через великий час доставки повідомлень, навантаження на процесор та відсутність мобільних додатків.

А скептицизм пов'язаний з тим, що месенджери на блокчейні випереджають час — люди не готові брати відповідальність за свій аккаунт на себе, володіння особистою інформацією поки не в тренді, а технології не дозволяють забезпечити високу швидкість на блокчейні. Далі з'являтимуться більш технологічні аналоги нашого проекту. Ось побачите.

Джерело: habr.com

Додати коментар або відгук