Au début de 2017, nous avons commencé à créer un messager sur blockchain [le nom et le lien se trouvent dans le profil] en discutant des avantages par rapport aux messagers P2P classiques.
Deux 2.5 ans se sont écoulés, et nous avons réussi à valider notre concept : des applications de messager sont désormais disponibles pour iOS, Web PWA, Windows, GNU/Linux, Mac OS et Android.
Aujourd'hui, nous allons expliquer comment fonctionne le messager sur blockchain et comment les applications client interagissent avec son API.

Nous voulions que la blockchain résolve les questions de sécurité et de vie privée des messagers P2P classiques :
- Un clic pour créer un compte — pas de numéros de téléphone ni d'adresses électroniques, pas d'accès aux carnets d'adresses et aux géolocalisations.
- Les interlocuteurs n'établissent jamais de connexions directes, toutes les communications passent par un système distribué de nœuds. Les adresses IP des utilisateurs ne sont pas accessibles les unes aux autres.
- Tous les messages sont chiffrés de bout en bout avec curve25519xsalsa20poly1305. Cela ne semble pas surprenant, mais notre code source est ouvert.
- Une attaque MITM est exclue — chaque message est une transaction et est signé par Ed25519 EdDSA.
- Le message entre dans son bloc. La séquence et
timestamples blocs ne peuvent pas être modifiés, donc l'ordre des messages non plus. - « Je ne l'ai pas dit » ne fonctionne pas avec les messages sur la blockchain.
- Il n'y a pas de structure centrale qui vérifie la "fiabilité" des messages. Cela est réalisé par un système de nœuds distribués basé sur un consensus, et il appartient aux utilisateurs.
- L'impossibilité de censure — les comptes ne peuvent pas être bloqués et les messages ne peuvent pas être supprimés.
- La blockchain 2FA — une alternative à la 2FA infernale par SMS,
- La possibilité d'accéder à tous vos dialogues depuis n'importe quel appareil à tout moment — cela permet de ne pas stocker les dialogues localement.
- Confirmation de livraison des messages. Pas sur l'appareil de l'utilisateur, mais sur le réseau. En essence, c'est une confirmation que le destinataire peut lire votre message. C'est une fonctionnalité utile pour l'envoi d'avis critiques.
Parmi les avantages de la blockchain, il y a aussi une intégration étroite avec les cryptomonnaies Ethereum, Dogecoin, Lisk, Dash, Bitcoin (celui-ci est encore en cours) et la possibilité d'envoyer des tokens dans les chats. Nous avons même créé un échange crypto intégré.
Et ensuite — comment tout cela fonctionne.
Un message — c'est une transaction.
Tout le monde est déjà habitué à ce que les transactions dans la blockchain transfèrent des tokens (monnaies) d'un utilisateur à un autre, comme avec le bitcoin. Nous avons créé un type spécial de transaction pour l'envoi de messages.
Pour envoyer un message dans le messager sur blockchain, il faut passer par plusieurs étapes :
- Chiffrer le texte du message.
- Insérer le texte crypté dans la transaction
- Signer la transaction
- Envoyer la transaction vers n'importe quel nœud du réseau
- Le système distribué de nœuds détermine la "validité" du message
- Si tout est OK, la transaction avec le message est intégrée dans le prochain bloc
- Le destinataire extrait la transaction contenant le message et le déchiffre
Les étapes 1 à 3 et 7 sont effectuées localement sur le client, tandis que 5 et 6 se font sur les nœuds du réseau.
Cryptage du message
Le message est crypté avec la clé privée de l'expéditeur et la clé publique du destinataire. Nous obtiendrons la clé publique depuis le réseau, mais pour cela, le compte du destinataire doit être initialisé, c'est-à-dire avoir au moins une transaction. On peut utiliser une requête REST GET /api/accounts/getPublicKey?address={ADAMANT address}, et lors du chargement des chats, les clés publiques des interlocuteurs seront déjà disponibles.

Le messager crypte les messages avec l'algorithme curve25519xsalsa20poly1305 (). puisque le compte contient des clés Ed25519, il est nécessaire de convertir les clés en Curve25519 Diffie-Hellman pour former le box.
Voici un exemple en 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)
}
}Formation de la transaction avec le message
La transaction a cette structure générale :
{
"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": {}
} Pour la transaction-message, la valeur la plus importante est asset — c'est là que le message doit être placé dans un objet chat avec la structure :
message— nous sauvegardons le message cryptéown_message— noncetype— type de message
Les messages sont également classés par types. En réalité, le paramètre type indique comment comprendre message. On peut envoyer simplement du texte, ou un objet avec des éléments d'intérêt à l'intérieur—c'est ainsi que le messager effectue des transferts de cryptomonnaie dans les chats.
Finalement, nous formons la transaction :
{
"transaction": {
"type": 8,
"amount": 0,
"senderId": "U12499126640447739963",
"senderPublicKey": "e9cafb1e7b403c4cf247c94f73ee4cada367fcc130cb3888219a0ba0633230b6",
"asset": {
"chat": {
"message": "cb682accceef92d7cddaaddb787d1184ab5428",
"own_message": "e7d8f90ddf7d70efe359c3e4ecfb5ed3802297b248eacbd6",
"type": 1
}
},
"recipientId": "U15677078342684640219",
"timestamp": 63228087,
"signature": "la signature sera ici"
}
}Signature de la transaction
Pour que tout le monde soit sûr de l'authenticité de l'expéditeur et du destinataire, du moment d'envoi et du contenu du message, la transaction est signée. La signature numérique permet de vérifier l'authenticité de la transaction à l'aide de la clé publique — la clé privée n'est pas nécessaire pour cela.
Mais la signature elle-même est réalisée avec la clé privée :

Le schéma montre que nous hachons d'abord la transaction avec SHA-256, puis nous la signons. et nous obtenons une signature. signature, et l'identifiant de la transaction est une partie du hachage SHA-256.
Exemple d'implémentation :
1 — Nous formons un bloc de données, y compris le message.
/**
* 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 — Nous calculons le SHA-256 du bloc de données.
/**
* 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 — Nous signons la transaction.
adamant.transactionSign = function (trs, keypair) {
var hash = this.getHash(trs)
return this.sign(hash, keypair).toString('hex')
}
/**
* Crée une signature basée sur un hachage et une paire de clés.
* @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'))
}Envoi de la transaction avec message au nœud du réseau.
Puisque le réseau est décentralisé, n'importe quel nœud avec une API ouverte fera l'affaire. Nous faisons une requête POST à l'endpoint. api/transactions:
curl 'api/transactions' -X POST
-d 'TX_DATA'En réponse, nous recevrons l'ID de la transaction de type.
{
"success": true,
"nodeTimestamp": 63228852,
"transactionId": "6146865104403680934"
}Vérification de l'authenticité de la transaction.
Un système distribué de nœuds basé sur le consensus détermine l'"authenticité" de la transaction-message. Qui envoie à qui, lorsque, le message a-t-il été remplacé par un autre, l'heure d'envoi est-elle correcte ? C'est un très grand avantage de la blockchain — il n'y a pas de structure centrale responsable des vérifications, et l'ordre des messages et leur contenu ne peuvent pas être falsifiés.
D'abord, l'authenticité est vérifiée par un nœud, puis envoyée aux autres — si la majorité dit que tout va bien, la transaction sera incluse dans le prochain bloc de la chaîne — c'est ça le consensus.

Une partie du code du nœud responsable des vérifications peut être consultée sur GitHub — et . Ah, le nœud fonctionne sur Node.js.
Inclusion de la transaction avec message dans le bloc.
Si le consensus est atteint, la transaction avec notre message sera intégrée dans le prochain bloc avec d'autres transactions authentiques.
Les blocs ont une séquence stricte, et chaque bloc suivant est formé sur la base des hachages des blocs précédents.

L'essentiel est que notre message est également inclus dans cette séquence et ne peut pas être « réarrangé ». Si plusieurs messages sont inclus dans le bloc, leur ordre sera déterminé par timestamp messages.
Lecture des messages
L'application de messagerie extrait les transactions de la blockchain qui sont envoyées au destinataire. Pour cela, nous avons créé un point de terminaison api/chatrooms.
Toutes les transactions sont accessibles à tous — il est possible d'obtenir des messages chiffrés. Cependant, seul le destinataire pourra les déchiffrer avec sa clé privée et la clé publique de l'expéditeur :
**
* 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) : ''
}Et qu'est-ce d'autre ?
Comme les messages sont livrés en environ 5 secondes — le temps d'apparition d'un nouveau bloc dans le réseau — nous avons conçu une connexion socket client-nœud et nœud-nœud. Lorsqu'un nœud reçoit une nouvelle transaction, il vérifie sa validité et la transmet aux autres nœuds. La transaction est accessible aux clients de messagerie avant même le consensus et son inclusion dans un bloc. Ainsi, nous livrerons les messages instantanément, comme le font les messageries classiques.
Pour stocker le carnet d'adresses, nous avons créé un KVS — Key-Value Storage — c'est un autre type de transactions, dans lesquelles asset n'est pas chiffré avec NaCl-box, mais . De cette façon, le messager stocke d'autres données.
Le transfert de fichiers/images et les chats de groupe nécessitent encore beaucoup de travail. Bien sûr, dans un format fait à la va-vite, cela pourrait être « connecté » rapidement, mais nous voulons maintenir le même niveau de confidentialité.
Oui, il reste encore beaucoup à faire — idéalement, une véritable confidentialité implique que les utilisateurs ne se connectent pas à des nœuds publics du réseau, mais établissent les leurs. Que pensez-vous, quel pourcentage d'utilisateurs fait cela ? Juste, 0. Partiellement, nous avons réussi à résoudre ce problème grâce à la version Tor du messager.
Nous avons prouvé qu'un messager sur blockchain peut exister. Il n'y a eu qu'une seule tentative en 2012 — , qui a échoué en raison du long temps de livraison des messages, de la charge sur le processeur et de l'absence d'applications mobiles.
Et le scepticisme est lié au fait que les messageries sur blockchain sont en avance sur leur temps — les gens ne sont pas prêts à prendre la responsabilité de leur compte, la possession d'informations personnelles n'est pas encore à la mode, et les technologies ne permettent pas d'assurer des vitesses élevées sur blockchain. Par la suite, des équivalents plus technologiques de notre projet apparaîtront. Vous verrez.
Source : habr.com
