Jak działa zdecentralizowany komunikator na blockchainie

Na początku 2017 roku zaczęliśmy tworzyć komunikator oparty na blockchainie [nazwa i link znajdują się w profilu], z omówieniem jego zalet w porównaniu do klasycznych komunikatorów P2P.

Minęło 2.5 lat, a nam udało się potwierdzić naszą koncepcję: teraz aplikacje komunikatora są dostępne na iOS, Web PWA, Windows, GNU/Linux, Mac OS i Androida.

Dziś opowiemy, jak działa komunikator oparty na blockchainie i jak aplikacje klienckie współpracują z jego API.
Jak działa zdecentralizowany komunikator na blockchainie

Chcieliśmy, aby blockchain rozwiązał problemy z bezpieczeństwem i prywatnością klasycznych komunikatorów P2P:

  • Jedno kliknięcie do utworzenia konta — żadnych telefonów ani adresów e-mail, brak dostępu do książek adresowych i geolokalizacji.
  • Rozmówcy nigdy nie łączą się bezpośrednio, cała komunikacja odbywa się przez rozproszony system węzłów. Adresy IP użytkowników są dla siebie nawzajem niedostępne.
  • Wszystkie wiadomości są szyfrowane End-to-End curve25519xsalsa20poly1305. Wydaje się, że już nikogo to nie dziwi, ale my mamy otwarty kod źródłowy.
  • Atak MITM jest wykluczony — każda wiadomość jest transakcją i jest podpisana Ed25519 EdDSA.
  • Wiadomość trafia do swojego bloku. Sekwencja i timestamp bloków nie mogą być zmieniane, a co za tym idzie — kolejność wiadomości.
  • „Nie powiedziałem tego” nie zadziała w przypadku wiadomości w blockchainie.
  • Nie ma centralnej struktury, która sprawdzałaby „autentyczność” wiadomości. To zadanie dla rozproszonego systemu węzłów na podstawie konsensusu, a należy on do użytkowników.
  • Brak możliwości cenzury — konta nie mogą być zablokowane, a wiadomości usunięte.
  • Blockchain 2FA — alternatywa dla wstrętnej 2FA za pomocą SMS-ów, która zrujnowała niejedno zdrowie.
  • Możliwość uzyskania wszystkich swoich dialogów z dowolnego urządzenia w dowolnym czasie — to możliwość nieprzechowywania dialogów lokalnie w ogóle.
  • Potwierdzenie dostarczenia wiadomości. Nie do urządzenia użytkownika, a do sieci. W praktyce jest to potwierdzenie, że odbiorca może przeczytać twoją wiadomość. To przydatna funkcja do wysyłania krytycznych powiadomień.

Zaletą blockchaina jest także bliska integracja z kryptowalutami Ethereum, Dogecoin, Lisk, Dash, Bitcoin (ten wciąż w toku) oraz możliwość wysyłania tokenów w czatach. Zrobiliśmy nawet wbudowaną wymianę kryptowalut.

A teraz — jak to wszystko działa.

Wiadomość — to transakcja.

Wszyscy przyzwyczaili się, że transakcje w blockchainie przekazują tokeny (monety) od jednego użytkownika do drugiego. Jak w przypadku Bitcoina. Stworzyliśmy jednak szczególny typ transakcji do przesyłania wiadomości.

Aby wysłać wiadomość w komunikatorze opartym na blockchainie, należy przejść przez kilka etapów:

  1. Zaszyfrować treść wiadomości
  2. Umieścić zaszyfrowaną treść w transakcji
  3. Podpisać transakcję
  4. Wysłać transakcję do dowolnego węzła sieci
  5. Rozproszony system węzłów określa „wiarygodność” wiadomości
  6. Jeśli wszystko jest w porządku — transakcja z wiadomością jest włączana do następnego bloku
  7. Odbiorca wyodrębnia transakcję z wiadomością i odszyfrowuje ją

Etapy 1–3 i 7 odbywają się lokalnie na kliencie, a 5–6 — na węzłach sieci.

Szyfrowanie wiadomości

Wiadomość jest szyfrowana prywatnym kluczem nadawcy i publicznym kluczem odbiorcy. Publiczny klucz pobierzemy z sieci, ale w tym celu konto odbiorcy musi być zainicjalizowane, co oznacza, że ​​musi mieć przynajmniej jedną transakcję. Można użyć zapytania REST GET /api/accounts/getPublicKey?address={ADAMANT address}, a przy ładowaniu czatów publiczne klucze rozmówców będą już dostępne.

Jak działa zdecentralizowany komunikator na blockchainie

Komunikator szyfruje wiadomości za pomocą algorytmu curve25519xsalsa20poly1305 (NaCl Box). Ponieważ konto zawiera klucze Ed25519, aby utworzyć box’a, klucze muszą być wcześniej przekształcone na Curve25519 Diffie-Hellman.

Oto przykład w 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)
  }
}

Tworzenie transakcji z wiadomością

Transakcja ma taką ogólną strukturę:

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

Dla transakcji-wiadomości najważniejszą wartością jest asset — to tutaj należy umieścić wiadomość w obiekcie czat o strukturze:

  • message — zachowujemy zaszyfrowaną wiadomość
  • own_message — nonce
  • type — typ wiadomości

Wiadomości również dzielą się na typy. W zasadzie parametr type informuje, jak je rozumieć message. Można wysłać po prostu tekst, a można również obiekt z interesującymi elementami w środku — na przykład tak, jak komunikator dokonuje przelewów kryptowalut w czatach.

W efekcie tworzymy transakcję:

{
  "transaction": {
    "type": 8,
    "amount": 0,
    "senderId": "U12499126640447739963",
    "senderPublicKey": "e9cafb1e7b403c4cf247c94f73ee4cada367fcc130cb3888219a0ba0633230b6",
    "asset": {
      "chat": {
        "message": "cb682accceef92d7cddaaddb787d1184ab5428",
        "own_message": "e7d8f90ddf7d70efe359c3e4ecfb5ed3802297b248eacbd6",
        "type": 1
      }
    },
    "recipientId": "U15677078342684640219",
    "timestamp": 63228087,
    "signature": "tut budzie podpis"
  }
}

Podpis transakcji

Aby wszyscy mogli być pewni wiarygodności nadawcy i odbiorcy, czasu wysyłki oraz treści wiadomości, transakcję podpisuje się. Podpis cyfrowy pozwala zweryfikować autentyczność transakcji za pomocą klucza publicznego — klucz prywatny do tego nie jest potrzebny.

Podpis wykonywany jest przy użyciu klucza prywatnego:

Jak działa zdecentralizowany komunikator na blockchainie

Z schematu widać, że transakcję najpierw haszujemy SHA-256, a następnie podpisujemy Ed25519 EdDSA i otrzymujemy podpis signature, a identyfikator transakcji to część SHA-256-hesha.

Przykład implementacji:

1 — Tworzymy blok danych, w tym wiadomość

/**
 * 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 — Obliczamy SHA-256 bloku danych

/**
 * 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 — Podpisujemy transakcję

adamant.transactionSign = function (trs, keypair) {
  var hash = this.getHash(trs)
  return this.sign(hash, keypair).toString('hex')
}

/**
 * Tworzy podpis na podstawie hasha i 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'))
}

Wysyłanie transakcji z wiadomością do węzła sieci

Ponieważ sieć jest zdecentralizowana, pasuje każdy węzeł z otwartym API. Wykonujemy żądanie POST do końcówki api/transactions:

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

W odpowiedzi otrzymamy ID transakcji typu

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

Weryfikacja autentyczności transakcji

Rozproszony system węzłów oparty na konsensusie określa „wiarygodność” transakcji-wiadomości. Kto, komu, kiedy, czy wiadomość nie została zamieniona inną, a prawidłowy czas wysyłki został podany. To bardzo ważna zaleta blockchaina — nie ma centralnej struktury odpowiedzialnej za weryfikację, a sekwencja wiadomości i ich zawartość nie jest fałszowana.

Najpierw wiarygodność sprawdza jeden węzeł, a następnie rozsyła innym — jeśli większość mówi, że wszystko w porządku, transakcja zostanie włączona do następnego bloku łańcucha — to właśnie jest konsensus.

Jak działa zdecentralizowany komunikator na blockchainie

Część kodu węzła, odpowiedzialna za weryfikacje, można zobaczyć w GitHub — validator.js i verify.js. Aha, węzeł działa na Node.js.

Włączamy transakcję z wiadomością do bloku

Jeśli osiągnięto konsensus, transakcja z naszą wiadomością trafi do następnego bloku obok innych wiarygodnych transakcji.

Bloki mają ścisłą sekwencję, a każdy kolejny blok tworzy się na podstawie haszy poprzednich bloków.

Jak działa zdecentralizowany komunikator na blockchainie

Istotą jest to, że nasza wiadomość również zostaje włączona w tę sekwencję i nie może być 'przestawiona'. Jeśli do bloku trafia wiele wiadomości, ich kolejność zostanie określona przez timestamp wiadomości.

Odczyt wiadomości

Aplikacja-messenger wyciąga transakcje z blockchaina, które zostały wysłane do adresata. W tym celu stworzyliśmy endpoint api/chatrooms.

Wszystkie transakcje są dostępne dla każdego — można uzyskać zaszyfrowane wiadomości. Jednak tylko odbiorca może je odszyfrować swoim kluczem prywatnym oraz kluczem publicznym nadawcy:

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

A co jeszcze?

Ponieważ wiadomości dostarczane są w około 5 sekundy — to czas pojawienia się nowego bloku w sieci — wymyśliliśmy połączenie soketowe klient-węzeł oraz węzeł-węzeł. Gdy węzeł otrzymuje nową transakcję, sprawdza jej ważność i przekazuje ją do innych węzłów. Transakcja jest dostępna dla klientów-messengerów jeszcze zanim osiągnięty zostanie konsensus i zanim zostanie dołączona do bloku. Dzięki temu będziemy dostarczać wiadomości natychmiastowo, tak jak tradycyjne messengery.

Aby przechowywać książkę adresową, stworzyliśmy KVS — Key-Value Storage — to kolejny typ transakcji, w których asset szyfrowany jest nie NaCl-box, a NaCl-secretbox. W ten sposób messenger przechowuje również inne dane.

Przesyłanie plików/zdjęć oraz czaty grupowe wymagają jeszcze wielu prac. Oczywiście, w formacie chaotycznym można to 'przypiąć' szybko, ale chcemy zachować ten sam poziom prywatności.

Tak, jest jeszcze nad czym pracować — w idealnym przypadku prawdziwa prywatność zakłada, że użytkownicy nie będą łączyć się z publicznymi węzłami sieci, ale założą swoje. Ile myślicie, że procent użytkowników tak robi? Właściwie, 0. Częściowo udało nam się rozwiązać ten problem wersją Tor messengera.

Udowodniliśmy, że messenger na blockchainie może istnieć. Wcześniej była tylko jedna próba w 2012 roku — bitmessage, nieudana z powodu długiego czasu dostarczania wiadomości, obciążenia procesora oraz braku aplikacji mobilnych.

Sceptycyzm wynika z tego, że komunikatory oparte na blockchainie wyprzedzają swój czas — ludzie nie są gotowi wziąć odpowiedzialności za swoje konto, posiadanie prywatnych informacji nie jest jeszcze w trendzie, a technologie nie zapewniają wysokich prędkości w blockchainie. Wkrótce pojawią się bardziej technologiczne wersje naszego projektu. Przekonacie się.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster