La începutul anului 2017, am început să dezvoltăm un messenger pe blockchain [numele și linkul sunt în profil] discutând despre avantajele sale față de clasicii messengeri P2P.
Au trecut 2.5 ani, și am reușit să ne confirmăm conceptul: acum aplicațiile messenger-ului sunt disponibile pentru iOS, Web PWA, Windows, GNU/Linux, Mac OS și Android.
Astăzi vă vom povesti cum funcționează messengerul pe blockchain și cum aplicațiile client pot interacționa cu API-ul său.

Am dorit ca blockchain-ul să rezolve problemele de securitate și confidențialitate ale mesajelor P2P clasice:
- Un singur clic pentru crearea unui cont — fără telefoane sau adrese de email, nimeni nu are acces la agendele de contacte sau la geolocalizare.
- Interlocutorii nu stabilesc conexiuni directe, toată comunicarea se desfășoară printr-un sistem distribuit de noduri. Adresele IP ale utilizatorilor nu sunt accesibile reciproc.
- Toate mesajele sunt criptate End-to-End curve25519xsalsa20poly1305. Deși pare un lucru obișnuit, codul nostru sursă este deschis.
- Atacurile MITM sunt excluse — fiecare mesaj este o tranzacție și este semnat cu Ed25519 EdDSA.
- Mesajul își găsește locul în blocul său. Secvența și
timestampblocurile nu pot fi modificate, iar prin urmare și ordinea mesajelor. - „Nu am spus asta” nu va funcționa pentru mesajele din blockchain.
- Nu există o structură centrală care să verifice "autenticitatea" mesajului. Aceasta este realizată de un sistem distribuit de noduri bazat pe consens, care aparține utilizatorilor.
- Imposibilitatea cenzurii — conturile nu pot fi blocate, iar mesajele nu pot fi șterse.
- Blockchain 2FA — o alternativă la 2FA groaznică prin SMS,
- Posibilitatea de a obține toate dialogurile de pe orice dispozitiv oricând — aceasta este o oportunitate de a nu păstra dialogurile local deloc.
- Confirmarea livrării mesajelor. Nu către dispozitivul utilizatorului, ci pe rețea. Practic, aceasta confirmă că destinatarul poate citi mesajul tău. Aceasta este o funcție utilă pentru trimiterea notificărilor critice.
Printre avantajele blockchain-ului se numără și integrarea strânsă cu criptomonedele Ethereum, Dogecoin, Lisk, Dash, Bitcoin (acesta este încă în curs de implementare) și posibilitatea de a trimite token-uri în chat-uri. Chiar am realizat un schimb de criptomonede încorporat.
Apoi — cum funcționează toate acestea.
Mesajul este o tranzacție
Toată lumea s-a obișnuit că tranzacțiile în blockchain transmit token-uri (monede) de la un utilizator la altul. Așa cum se întâmplă cu Bitcoin. Noi am creat un tip special de tranzacții pentru a transmite mesaje.
Pentru a trimite un mesaj în messengerul pe blockchain, trebuie să urmați câțiva pași:
- Criptează textul mesajului
- Introduceți textul criptat în tranzacție
- Semnați tranzacția
- Trimiteți tranzacția către orice nod din rețea
- Sistemul distribuit de noduri determină „validitatea” mesajului
- Dacă totul este în regulă - tranzacția cu mesajul este inclusă în următorul bloc
- Destinatarul extrage tranzacția cu mesajul și o decriptează
Etapele 1–3 și 7 se desfășoară local pe client, iar 5–6 pe nodurile din rețea.
Criptarea mesajului
Mesajul este criptat cu cheia privată a expeditorului și cheia publică a destinatarului. Cheia publică va fi obținută din rețea, însă pentru aceasta contul destinatarului trebuie să fie inițializat, adică să aibă cel puțin o tranzacție. Se poate folosi o cerere REST GET /api/accounts/getPublicKey?address={ADAMANT address}, iar la încărcarea chat-urilor, cheile publice ale interlocutorilor vor fi deja disponibile.

Mesageria criptează mesajele folosind algoritmul curve25519xsalsa20poly1305 (). Deoarece contul conține chei Ed25519, pentru a forma box-ul trebuie mai întâi să transformăm cheile în Curve25519 Diffie-Hellman.
Iată un exemplu în 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)
}
}Formarea tranzacției cu mesaj
Tranzacția are următoarea structură generală:
{
"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": {}
} Pentru tranzacția-mesaj, cea mai importantă valoare este asset — în el trebuie să inserăm mesajul într-un obiect chat cu structura:
message— păstrăm mesajul criptatown_message— noncetype— tipul mesajului
Mesajele sunt, de asemenea, împărțite în tipuri. Practic, parametrul type se comunică cum să fie interpretat message. Putem trimite pur și simplu text, dar putem trimite și un obiect cu elemente interesante în interior - de exemplu, așa cum mesageria face transferuri de criptovalute în chat-uri.
În cele din urmă, formăm tranzacția:
{
"transaction": {
"type": 8,
"amount": 0,
"senderId": "U12499126640447739963",
"senderPublicKey": "e9cafb1e7b403c4cf247c94f73ee4cada367fcc130cb3888219a0ba0633230b6",
"asset": {
"chat": {
"message": "cb682accceef92d7cddaaddb787d1184ab5428",
"own_message": "e7d8f90ddf7d70efe359c3e4ecfb5ed3802297b248eacbd6",
"type": 1
}
},
"recipientId": "U15677078342684640219",
"timestamp": 63228087,
"signature": "aici va fi semnătura"
}
}Semnătura tranzacției
Pentru a ne asigura de veridicitatea expeditorului și a destinatarului, de momentul trimiterii și de conținutul mesajului, tranzacția este semnată. Semnătura digitală permite verificarea autenticitații tranzacției prin cheia publică — cheia privată nu este necesară pentru aceasta.
Iată că semnătura este generată cu cheia privată:

Din schemă se observă că tranzacția este mai întâi hash-uită folosind SHA-256, iar apoi semnată. și obținem semnătura semnătură, iar identificatorul tranzacției este o parte din hash-ul SHA-256.
Exemplu de implementare:
1 — Formăm blocul de date, inclusiv mesajul
/**
* 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 — Calculăm SHA-256 al blocului de date
/**
* 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 — Semnăm tranzacția
adamant.transactionSign = function (trs, keypair) {
var hash = this.getHash(trs)
return this.sign(hash, keypair).toString('hex')
}
/**
* Creează o semnătură bazată pe o hash și o cheie.
* @implements {sodium}
* @param {hash} hash
* @param {keypair} keypair
* @return {signature} semnătură
*\/
adamant.sign = function (hash, keypair) {
return sodium.crypto_sign_detached(hash, Buffer.from(keypair.privateKey, 'hex'))
}Trimiterea tranzacției cu mesaj la un nod al rețelei
Deoarece rețeaua este descentralizată, orice nod cu API deschis este acceptabil. Facem o cerere POST la endpoint-ul api/transactions:
curl 'api/transactions' -X POST
-d 'TX_DATA'Ca răspuns, vom primi ID-ul tranzacției de tip
{
"success": true,
"nodeTimestamp": 63228852,
"transactionId": "6146865104403680934"
}Verificarea veridicității tranzacției
Sistemul distribuit de noduri pe baza consensului determină „veridicitatea” tranzacției-mesaj. De la cine și către cine, când, dacă mesajul nu a fost înlocuit, iar timpul de trimitere indicat este corect. Aceasta este o mare prioritate a blockchain-ului — nu există o structură centrală responsabilă pentru verificări, iar secvența mesajelor și conținutul acestora nu pot fi falsificate.
Mai întâi veridicitatea este verificată de un nod, iar apoi este distribuită altora — dacă majoritatea spun că totul este în regulă, tranzacția va fi inclusă în următorul bloc al lanțului — acesta este consensul.

O parte din codul nodului, care este responsabilă pentru verificări, poate fi consultată pe GitHub — și . Aha, nodul funcționează pe Node.js.
Includem tranzacția cu mesaj în bloc
Dacă consensul este atins, tranzacția cu mesajul nostru va fi inclusă în următorul bloc alături de alte tranzacții verificate.
Blocurile au o secvență strictă, iar fiecare bloc ulterior este format pe baza hash-urilor blocurilor anterioare.

Ideea este că mesajul nostru este, de asemenea, inclus în această secvență și nu poate fi „mutat”. Dacă mai multe mesaje ajung în bloc, ordinea lor va fi stabilită după timestamp mesaje.
Citirea mesajelor
Aplicația de mesagerie extrage tranzacțiile din blockchain, care sunt trimise destinatarului. Pentru asta, am creat un endpoint api/chatrooms.
Toate tranzacțiile sunt disponibile pentru toată lumea — se pot obține mesaje criptate. Dar numai destinatarul le poate decripta cu cheia lui privată și cheia publică a expeditorului:
**
* 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) : ''
}Și ce altceva?
Deoarece mesajele sunt livrate în aproximativ 5 secunde — timpul necesar pentru apariția unui nou bloc în rețea — am dezvoltat o conexiune socket client-node și node-node. Când un node primește o nouă tranzacție, verifică validitatea acesteia și o transmite altor noduri. Tranzacția este disponibilă clienților-mesageri înainte de a se ajunge la consens și de a fi inclusă în bloc. Astfel, mesajele vor fi livrate instantaneu, la fel ca în aplicațiile de mesagerie cunoscute.
Pentru a stoca agenda, am creat KVS — Key-Value Storage — acesta este un alt tip de tranzacții, în care asset nu este criptat NaCl-box, ci . Astfel, aplicația de mesagerie stochează și alte date.
Transmiterea de fișiere/imagine și grupurile de chat necesită încă multă muncă. Bineînțeles, într-un format superficial, poate fi „atașat” rapid, dar vrem să păstrăm același nivel de confidențialitate.
Da, mai este mult de lucru — în ideal, confidențialitatea reală presupune ca utilizatorii să nu se conecteze la nodurile publice ale rețelei, ci să își ridice propriile lor. Credeți că câți la sută dintre utilizatori fac asta? Corect, 0. Parțial, această problemă a fost rezolvată prin versiunea Tor a aplicației de mesagerie.
Am demonstrat că aplicația de mesagerie pe blockchain poate exista. Anterior a existat doar o singură încercare în 2012 — , eșuată din cauza timpului mare de livrare a mesajelor, a sarcinii pe procesor și a lipsei aplicațiilor mobile.
Iar scepticismul se leagă de faptul că aplicațiile de mesagerie pe blockchain sunt înaintea timpului — oamenii nu sunt pregătiți să-și asume responsabilitatea pentru contul lor, deținerea informațiilor personale nu este încă o tendință, iar tehnologiile nu permit asigurarea unor viteze mari pe blockchain. Următoarele vor fi apariția unor alternative mai tehnologice ale proiectului nostru. Veți vedea.
Sursa: habr.com
