В началото на 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–3 и 7 се изпълняват локално на клиента, а 5–6 — на възлите в мрежата.
Шифроване на съобщението
Съобщението се шифрова с личния ключ на подателя и публичния ключ на получателя. Публичния ключ ще вземем от мрежата, но за това акаунтът на получателя трябва да бъде инициализиран, тоест да има поне една транзакция. Може да се използва REST-заявка GET /api/accounts/getPublicKey?address={ADAMANT address}, а при зареждане на чатове публичните ключове на събеседниците вече ще са налични.

Месенджърът шифрова съобщенията с алгоритъм curve25519xsalsa20poly1305 (). Тъй като акаунтът съдържа ключове 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тип— тип на съобщението
Съобщенията също се разделят на типове. По същество, параметърът тип съобщава как да се разбира 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, а след това я подписваме. и получаваме подпис. 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')
}
/**
* Създава подпис на базата на хеш и ключова двойка.
* @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 — и . Ага, узелът работи на 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, а . По този начин съобщителната платформа съхранява и други данни.
Предаване на файлове/снимки и групови чатове изискват още много работа. Разбира се, в формат "на бързо" това може да се добави бързо, но искаме да запазим същото ниво на конфиденциалност.
Да, има още с какво да се работи — в идеалния случай реалната конфиденциалност предполага, че потребителите няма да се свързват с публични възли на мрежата, а ще създадат свои собствени. Как мислите, колко процента от потребителите го правят? Правилно, 0. Частично успяхме да решим този въпрос с Tor-версията на съобщителната платформа.
Доказахме, че съобщителната платформа на блокчейн може да съществува. По-рано имаше само един опит през 2012 година — , неуспешен заради голямото време за доставка на съобщения, натоварването на процесора и липсата на мобилни приложения.
А скептицизмът е свързан с факта, че съобщителните платформи на блокчейн изпреварват времето — хората не са готови да поемат отговорност за своя акаунт, притежаването на лична информация все още не е на мода, а технологиите не позволяват да се осигурят високи скорости на блокчейн. В следващия период ще се появят по-технологични аналози на нашия проект. Ще видите.
Източник: habr.com
