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.

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
timestampblokó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,
- 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:
- Zaszyfrować treść wiadomości
- Umieścić zaszyfrowaną treść w transakcji
- Podpisać transakcję
- Wysłać transakcję do dowolnego węzła sieci
- Rozproszony system węzłów określa „wiarygodność” wiadomości
- Jeśli wszystko jest w porządku — transakcja z wiadomością jest włączana do następnego bloku
- 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.

Komunikator szyfruje wiadomości za pomocą algorytmu curve25519xsalsa20poly1305 (). 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— noncetype— 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:

Z schematu widać, że transakcję najpierw haszujemy SHA-256, a następnie podpisujemy 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.

Część kodu węzła, odpowiedzialna za weryfikacje, można zobaczyć w GitHub — i . 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.

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 . 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 — , 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
