Як працуе дэцэнтралізаваны мэсанджар на блокчэйне

У пачатку 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

Дадаць каментар