diff --git a/.cspell.json b/.cspell.json index e32f92c4da..05cefddfe4 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,6 +9,7 @@ "bitauth", "bufbuild", "cimg", + "ciphertext", "circleci", "codecov", "commitlint", @@ -17,7 +18,9 @@ "Dlazy", "Dout", "Dscore", + "ecies", "editorconfig", + "ephem", "esnext", "ethersproject", "execa", @@ -55,9 +58,12 @@ "protobuf", "protoc", "reactjs", + "recid", "rlnrelay", "sandboxed", "secio", + "seckey", + "secp", "staticnode", "statusim", "submodule", diff --git a/CHANGELOG.md b/CHANGELOG.md index 700e300483..1d9005987c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `WakuRelay.deleteObserver` to allow removal of observers, useful when a React component add observers when mounting and needs to delete it when unmounting. - Keep alive feature that pings host regularly, reducing the chance of connections being dropped due to idle. - Can be disabled or default frequency (10s) can be changed when calling `Waku.create`. + Can be disabled or default frequency (10s) can be changed when calling `Waku.create`. +- New `lib/utils` module for easy, dependency-less hex/bytes conversions. ### Changed - **Breaking**: Auto select peer if none provided for store and light push protocols. - Upgrade to `libp2p@0.31.7` and `libp2p-gossipsub@0.10.0` to avoid `TextEncoder` errors in ReactJS tests. - Disable keep alive by default as latest nim-waku release does not support ping protocol. +- **Breaking**: Optional parameters for `WakuMessage.fromBytes` and `WakuMessage.fromUtf8String` are now passed in a single `Options` object. +- **Breaking**: `WakuMessage` static functions are now async to allow for encryption and decryption. +- **Breaking**: `WakuMessage` constructor is now private, `from*` and `decode*` function should be used. +- `WakuMessage` version 1 is partially supported, enabling asymmetrical encryption and signature of messages; + this can be done by passing keys to `WakuMessage.from*` and `WakuMessage.decode*` methods. + Note: this is not yet compatible with nim-waku. ### Fixed - Disable `keepAlive` if set to `0`. diff --git a/examples/cli-chat/src/chat.ts b/examples/cli-chat/src/chat.ts index 6961ddca2c..d0c3f0f7be 100644 --- a/examples/cli-chat/src/chat.ts +++ b/examples/cli-chat/src/chat.ts @@ -104,7 +104,10 @@ export default async function startChat(): Promise { rl.prompt(); const chatMessage = ChatMessage.fromUtf8String(new Date(), nick, line); - const msg = WakuMessage.fromBytes(chatMessage.encode(), ChatContentTopic); + const msg = await WakuMessage.fromBytes(chatMessage.encode(), { + contentTopic: ChatContentTopic, + timestamp: new Date(), + }); if (opts.lightPush) { await waku.lightPush.push(msg); } else { diff --git a/examples/eth-dm/src/BroadcastPublicKey.tsx b/examples/eth-dm/src/BroadcastPublicKey.tsx index 1cf2fb7f02..d54235d64c 100644 --- a/examples/eth-dm/src/BroadcastPublicKey.tsx +++ b/examples/eth-dm/src/BroadcastPublicKey.tsx @@ -25,18 +25,30 @@ export default function BroadcastPublicKey({ if (!waku) return; if (publicKeyMsg) { - const wakuMsg = encodePublicKeyWakuMessage(publicKeyMsg); - waku.lightPush.push(wakuMsg).catch((e) => { - console.error('Failed to send Public Key Message', e); - }); + encodePublicKeyWakuMessage(publicKeyMsg) + .then((wakuMsg) => { + waku.lightPush.push(wakuMsg).catch((e) => { + console.error('Failed to send Public Key Message', e); + }); + }) + .catch((e) => { + console.log('Failed to encode Public Key Message in Waku Message'); + }); } else { createPublicKeyMessage(signer, ethDmKeyPair.publicKey) .then((msg) => { setPublicKeyMsg(msg); - const wakuMsg = encodePublicKeyWakuMessage(msg); - waku.lightPush.push(wakuMsg).catch((e) => { - console.error('Failed to send Public Key Message', e); - }); + encodePublicKeyWakuMessage(msg) + .then((wakuMsg) => { + waku.lightPush.push(wakuMsg).catch((e) => { + console.error('Failed to send Public Key Message', e); + }); + }) + .catch((e) => { + console.log( + 'Failed to encode Public Key Message in Waku Message' + ); + }); }) .catch((e) => { console.error('Failed to create public key message', e); @@ -56,9 +68,11 @@ export default function BroadcastPublicKey({ ); } -function encodePublicKeyWakuMessage( +async function encodePublicKeyWakuMessage( publicKeyMessage: PublicKeyMessage -): WakuMessage { +): Promise { const payload = publicKeyMessage.encode(); - return WakuMessage.fromBytes(payload, PublicKeyContentTopic); + return await WakuMessage.fromBytes(payload, { + contentTopic: PublicKeyContentTopic, + }); } diff --git a/examples/eth-dm/src/crypto.ts b/examples/eth-dm/src/crypto.ts index 1dd7c7b0f8..1205e2d7f7 100644 --- a/examples/eth-dm/src/crypto.ts +++ b/examples/eth-dm/src/crypto.ts @@ -4,7 +4,7 @@ import * as EthCrypto from 'eth-crypto'; import { ethers } from 'ethers'; import { Signer } from '@ethersproject/abstract-signer'; import { DirectMessage, PublicKeyMessage } from './messaging/wire'; -import { byteArrayToHex, hexToBuf } from './utils'; +import { hexToBuf, equalByteArrays, bufToHex } from 'js-waku/lib/utils'; export interface KeyPair { privateKey: string; @@ -49,15 +49,7 @@ export function validatePublicKeyMessage(msg: PublicKeyMessage): boolean { const formattedMsg = formatPublicKeyForSignature(msg.ethDmPublicKey); try { const sigAddress = ethers.utils.verifyMessage(formattedMsg, msg.signature); - const sigAddressBytes = hexToBuf(sigAddress); - // Compare the actual byte arrays instead of strings that may differ in casing or prefixing. - const cmp = sigAddressBytes.compare(new Buffer(msg.ethAddress)); - console.log( - `Buffer comparison result: ${cmp} for (signature address, message address)`, - sigAddressBytes, - msg.ethAddress - ); - return cmp === 0; + return equalByteArrays(sigAddress, msg.ethAddress); } catch (e) { console.log( 'Failed to verify signature for Public Key Message', @@ -77,7 +69,7 @@ export function validatePublicKeyMessage(msg: PublicKeyMessage): boolean { */ function formatPublicKeyForSignature(ethDmPublicKey: Uint8Array): string { return JSON.stringify({ - ethDmPublicKey: byteArrayToHex(ethDmPublicKey), + ethDmPublicKey: bufToHex(ethDmPublicKey), }); } diff --git a/examples/eth-dm/src/key_pair_handling/key_pair_storage.ts b/examples/eth-dm/src/key_pair_handling/key_pair_storage.ts index d6fc7d2da6..ccbb425515 100644 --- a/examples/eth-dm/src/key_pair_handling/key_pair_storage.ts +++ b/examples/eth-dm/src/key_pair_handling/key_pair_storage.ts @@ -1,4 +1,5 @@ import { KeyPair } from '../crypto'; +import { bufToHex, hexToBuf } from 'js-waku/lib/utils'; /** * Save keypair to storage, encrypted with password @@ -10,9 +11,9 @@ export async function saveKeyPairToStorage( const { salt, iv, cipher } = await encryptKey(ethDmKeyPair, password); const data = { - salt: new Buffer(salt).toString('hex'), - iv: new Buffer(iv).toString('hex'), - cipher: new Buffer(cipher).toString('hex'), + salt: bufToHex(salt), + iv: bufToHex(iv), + cipher: bufToHex(cipher), }; localStorage.setItem('cipherEthDmKeyPair', JSON.stringify(data)); @@ -28,9 +29,9 @@ export async function loadKeyPairFromStorage( if (!str) return; const data = JSON.parse(str); - const salt = new Buffer(data.salt, 'hex'); - const iv = new Buffer(data.iv, 'hex'); - const cipher = new Buffer(data.cipher, 'hex'); + const salt = hexToBuf(data.salt); + const iv = hexToBuf(data.iv); + const cipher = hexToBuf(data.cipher); return await decryptKey(salt, iv, cipher, password); } diff --git a/examples/eth-dm/src/messaging/SendMessage.tsx b/examples/eth-dm/src/messaging/SendMessage.tsx index fb61bfd315..cb9e4cd40e 100644 --- a/examples/eth-dm/src/messaging/SendMessage.tsx +++ b/examples/eth-dm/src/messaging/SendMessage.tsx @@ -117,7 +117,9 @@ async function encodeEncryptedWakuMessage( }; const payload = encode(directMsg); - return WakuMessage.fromBytes(payload, DirectMessageContentTopic); + return WakuMessage.fromBytes(payload, { + contentTopic: DirectMessageContentTopic, + }); } function sendMessage( diff --git a/examples/eth-dm/src/waku.ts b/examples/eth-dm/src/waku.ts index 59454da0ed..9681d079b8 100644 --- a/examples/eth-dm/src/waku.ts +++ b/examples/eth-dm/src/waku.ts @@ -3,7 +3,7 @@ import { getStatusFleetNodes, Waku, WakuMessage } from 'js-waku'; import { decode, DirectMessage, PublicKeyMessage } from './messaging/wire'; import { decryptMessage, validatePublicKeyMessage } from './crypto'; import { Message } from './messaging/Messages'; -import { byteArrayToHex, equalByteArrays } from './utils'; +import { bufToHex, equalByteArrays } from 'js-waku/lib/utils'; export const PublicKeyContentTopic = '/eth-dm/1/public-key/proto'; export const DirectMessageContentTopic = '/eth-dm/1/direct-message/json'; @@ -41,7 +41,7 @@ export function handlePublicKeyMessage( if (!msg.payload) return; const publicKeyMsg = PublicKeyMessage.decode(msg.payload); if (!publicKeyMsg) return; - const ethDmPublicKey = byteArrayToHex(publicKeyMsg.ethDmPublicKey); + const ethDmPublicKey = bufToHex(publicKeyMsg.ethDmPublicKey); console.log(ethDmPublicKey, myAddress); if (myAddress && equalByteArrays(publicKeyMsg.ethAddress, myAddress)) return; @@ -50,7 +50,7 @@ export function handlePublicKeyMessage( if (res) { setter((prevPks: Map) => { - prevPks.set(byteArrayToHex(publicKeyMsg.ethAddress), ethDmPublicKey); + prevPks.set(bufToHex(publicKeyMsg.ethAddress), ethDmPublicKey); return new Map(prevPks); }); } diff --git a/examples/web-chat/src/Room.tsx b/examples/web-chat/src/Room.tsx index 55eb0ad5c9..e36981eaac 100644 --- a/examples/web-chat/src/Room.tsx +++ b/examples/web-chat/src/Room.tsx @@ -55,11 +55,10 @@ async function handleMessage( } else { const timestamp = new Date(); const chatMessage = ChatMessage.fromUtf8String(timestamp, nick, message); - const wakuMsg = WakuMessage.fromBytes( - chatMessage.encode(), - ChatContentTopic, - timestamp - ); + const wakuMsg = await WakuMessage.fromBytes(chatMessage.encode(), { + contentTopic: ChatContentTopic, + timestamp, + }); return messageSender(wakuMsg); } } diff --git a/package-lock.json b/package-lock.json index 23ba3ada1c..05f5b5bdfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,10 @@ "dependencies": { "axios": "^0.21.1", "debug": "^4.3.1", + "ecies-parity": "^0.1.1", "it-concat": "^2.0.0", "it-length-prefixed": "^5.0.2", + "js-sha3": "^0.8.0", "libp2p": "^0.31.7", "libp2p-gossipsub": "^0.10.0", "libp2p-mplex": "^0.10.3", @@ -19,6 +21,7 @@ "libp2p-tcp": "^0.15.4", "libp2p-websockets": "^0.15.6", "multiaddr": "^9.0.1", + "secp256k1": "^4.0.2", "ts-proto": "^1.79.7", "uuid": "^8.3.2" }, @@ -30,6 +33,7 @@ "@types/google-protobuf": "^3.7.4", "@types/mocha": "^8.2.2", "@types/node": "^14.14.31", + "@types/secp256k1": "^4.0.2", "@types/tail": "^2.0.0", "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "^4.0.1", @@ -3586,6 +3590,15 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, + "node_modules/@types/secp256k1": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.2.tgz", + "integrity": "sha512-QMg+9v0bbNJ2peLuHRWxzmy0HRJIG6gFZNhaRSp7S3ggSbCCxiqQB2/ybvhXyhHOCequpNkrx7OavNhrWOsW0A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/tail": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@types/tail/-/tail-2.2.0.tgz", @@ -4545,6 +4558,15 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/bl": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.0.0.tgz", @@ -4617,6 +4639,20 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "optional": true, + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/browserslist": { "version": "4.16.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", @@ -4676,6 +4712,12 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "optional": true + }, "node_modules/bufio": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/bufio/-/bufio-1.0.7.tgz", @@ -4934,6 +4976,16 @@ "npm": ">=3.0.0" } }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "optional": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/class-is": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/class-is/-/class-is-1.1.0.tgz", @@ -6544,6 +6596,33 @@ "node": ">=10" } }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "optional": true, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "optional": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -7191,6 +7270,20 @@ "node": ">=6" } }, + "node_modules/drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=", + "optional": true, + "dependencies": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -7205,6 +7298,71 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, + "node_modules/ecies-parity": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecies-parity/-/ecies-parity-0.1.1.tgz", + "integrity": "sha512-eq95uTiNkQYr9orLJZVD/aGfcvRiC6tORZvB2JiieEYX4OwbJV74ahDhDm9d/4NkO7lPoi19DWcEQ8SKh+kQ6A==", + "hasInstallScript": true, + "dependencies": { + "acorn": "7.1.0", + "elliptic": "6.5.1", + "es6-promise": "^4.2.4", + "nan": "2.14.0" + }, + "optionalDependencies": { + "secp256k1": "3.7.1" + } + }, + "node_modules/ecies-parity/node_modules/acorn": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ecies-parity/node_modules/elliptic": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", + "integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==", + "dependencies": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "node_modules/ecies-parity/node_modules/nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, + "node_modules/ecies-parity/node_modules/secp256k1": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.7.1.tgz", + "integrity": "sha512-1cf8sbnRreXrQFdH6qsg2H71Xw91fCCS9Yp021GnUNJzWJS/py96fS4lHbnTnouLp08Xj6jBoBB6V78Tdbdu5g==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.4.1", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7390,6 +7548,11 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "node_modules/es6-promisify": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.1.1.tgz", @@ -7866,6 +8029,16 @@ "node": ">=0.8.x" } }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "optional": true, + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, "node_modules/exec-sh": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", @@ -9928,6 +10101,40 @@ "node": ">=0.10.0" } }, + "node_modules/hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hash-base/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -12400,6 +12607,17 @@ "node": ">= 8.16.2" } }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "optional": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -16227,6 +16445,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "optional": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, "node_modules/rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -16792,6 +17020,19 @@ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", "peer": true }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "optional": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -22021,6 +22262,15 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, + "@types/secp256k1": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.2.tgz", + "integrity": "sha512-QMg+9v0bbNJ2peLuHRWxzmy0HRJIG6gFZNhaRSp7S3ggSbCCxiqQB2/ybvhXyhHOCequpNkrx7OavNhrWOsW0A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/tail": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@types/tail/-/tail-2.2.0.tgz", @@ -22736,6 +22986,15 @@ "file-uri-to-path": "1.0.0" } }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "bl": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.0.0.tgz", @@ -22802,6 +23061,20 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "optional": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "browserslist": { "version": "4.16.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", @@ -22837,6 +23110,12 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "optional": true + }, "bufio": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/bufio/-/bufio-1.0.7.tgz", @@ -23030,6 +23309,16 @@ "uint8arrays": "^2.1.3" } }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "optional": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "class-is": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/class-is/-/class-is-1.1.0.tgz", @@ -24307,6 +24596,33 @@ "yaml": "^1.10.0" } }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "optional": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "optional": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -24807,6 +25123,17 @@ } } }, + "drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=", + "optional": true, + "requires": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -24823,6 +25150,60 @@ } } }, + "ecies-parity": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecies-parity/-/ecies-parity-0.1.1.tgz", + "integrity": "sha512-eq95uTiNkQYr9orLJZVD/aGfcvRiC6tORZvB2JiieEYX4OwbJV74ahDhDm9d/4NkO7lPoi19DWcEQ8SKh+kQ6A==", + "requires": { + "acorn": "7.1.0", + "elliptic": "6.5.1", + "es6-promise": "^4.2.4", + "nan": "2.14.0", + "secp256k1": "3.7.1" + }, + "dependencies": { + "acorn": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==" + }, + "elliptic": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", + "integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, + "secp256k1": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.7.1.tgz", + "integrity": "sha512-1cf8sbnRreXrQFdH6qsg2H71Xw91fCCS9Yp021GnUNJzWJS/py96fS4lHbnTnouLp08Xj6jBoBB6V78Tdbdu5g==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.4.1", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + } + } + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -24978,6 +25359,11 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "es6-promisify": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.1.1.tgz", @@ -25338,6 +25724,16 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "optional": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, "exec-sh": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", @@ -26899,6 +27295,25 @@ } } }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "optional": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "optional": true + } + } + }, "hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -28899,6 +29314,17 @@ "integrity": "sha512-BJXxkuIfJchcXOJWTT2DOL+yFWifFv2yGYOUzvXg8Qz610QKw+sHCvTMYwA+qWGhlA2uivBezChZ/pBy1tWdkQ==", "dev": true }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "optional": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, "memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -31889,6 +32315,16 @@ "glob": "^7.1.3" } }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "optional": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -32347,6 +32783,16 @@ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", "peer": true }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "optional": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", diff --git a/package.json b/package.json index 471d3c8b5c..8ef7b7c3e2 100644 --- a/package.json +++ b/package.json @@ -57,8 +57,10 @@ "dependencies": { "axios": "^0.21.1", "debug": "^4.3.1", + "ecies-parity": "^0.1.1", "it-concat": "^2.0.0", "it-length-prefixed": "^5.0.2", + "js-sha3": "^0.8.0", "libp2p": "^0.31.7", "libp2p-gossipsub": "^0.10.0", "libp2p-mplex": "^0.10.3", @@ -66,6 +68,7 @@ "libp2p-tcp": "^0.15.4", "libp2p-websockets": "^0.15.6", "multiaddr": "^9.0.1", + "secp256k1": "^4.0.2", "ts-proto": "^1.79.7", "uuid": "^8.3.2" }, @@ -77,6 +80,7 @@ "@types/google-protobuf": "^3.7.4", "@types/mocha": "^8.2.2", "@types/node": "^14.14.31", + "@types/secp256k1": "^4.0.2", "@types/tail": "^2.0.0", "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "^4.0.1", diff --git a/src/index.ts b/src/index.ts index 0c37dc1565..4e029962b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ export { getStatusFleetNodes, Environment, Protocol } from './lib/discover'; +export * as utils from './lib/utils'; + export { Waku } from './lib/waku'; export { WakuMessage } from './lib/waku_message'; diff --git a/examples/eth-dm/src/utils.ts b/src/lib/utils.ts similarity index 69% rename from examples/eth-dm/src/utils.ts rename to src/lib/utils.ts index 3007efb418..145dfb335b 100644 --- a/examples/eth-dm/src/utils.ts +++ b/src/lib/utils.ts @@ -1,10 +1,10 @@ -export function byteArrayToHex(bytes: Uint8Array): string { - const buf = new Buffer(bytes); - return buf.toString('hex'); +export function hexToBuf(str: string): Buffer { + return Buffer.from(str.replace(/^0x/i, ''), 'hex'); } -export function hexToBuf(str: string): Buffer { - return Buffer.from(str.replace(/0x/, ''), 'hex'); +export function bufToHex(buf: Uint8Array | Buffer | ArrayBuffer): string { + const _buf = Buffer.from(buf); + return _buf.toString('hex'); } export function equalByteArrays( diff --git a/src/lib/waku.ts b/src/lib/waku.ts index 92479dc842..8c3d7f3b6e 100644 --- a/src/lib/waku.ts +++ b/src/lib/waku.ts @@ -76,7 +76,7 @@ export class Waku { this.lightPush = lightPush; this.keepAliveTimers = {}; - const keepAlive = options.keepAlive !== undefined ? options.keepAlive : 0; + const keepAlive = options.keepAlive || 0; if (keepAlive !== 0) { libp2p.connectionManager.on('peer:connect', (connection: Connection) => { diff --git a/src/lib/waku_light_push/index.spec.ts b/src/lib/waku_light_push/index.spec.ts index 607f1ca04c..fbab17a763 100644 --- a/src/lib/waku_light_push/index.spec.ts +++ b/src/lib/waku_light_push/index.spec.ts @@ -33,7 +33,7 @@ describe('Waku Light Push', () => { }); const messageText = 'Light Push works!'; - const message = WakuMessage.fromUtf8String(messageText); + const message = await WakuMessage.fromUtf8String(messageText); const pushResponse = await waku.lightPush.push(message); expect(pushResponse?.isSuccess).to.be.true; @@ -73,7 +73,7 @@ describe('Waku Light Push', () => { const nimPeerId = await nimWaku.getPeerId(); const messageText = 'Light Push works!'; - const message = WakuMessage.fromUtf8String(messageText); + const message = await WakuMessage.fromUtf8String(messageText); const pushResponse = await waku.lightPush.push(message, { peerId: nimPeerId, diff --git a/src/lib/waku_message.spec.ts b/src/lib/waku_message.spec.ts deleted file mode 100644 index cf37aa5e23..0000000000 --- a/src/lib/waku_message.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect } from 'chai'; -import fc from 'fast-check'; - -import { WakuMessage } from './waku_message'; - -describe('Waku Message', function () { - it('Waku message round trip binary serialization', function () { - fc.assert( - fc.property(fc.string(), (s) => { - const msg = WakuMessage.fromUtf8String(s); - const binary = msg.encode(); - const actual = WakuMessage.decode(binary); - - expect(actual).to.deep.equal(msg); - }) - ); - }); - - it('Payload to utf-8', function () { - fc.assert( - fc.property(fc.string(), (s) => { - const msg = WakuMessage.fromUtf8String(s); - const utf8 = msg.payloadAsUtf8; - - return utf8 === s; - }) - ); - }); -}); diff --git a/src/lib/waku_message.ts b/src/lib/waku_message.ts deleted file mode 100644 index 8d7936d877..0000000000 --- a/src/lib/waku_message.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Ensure that this class matches the proto interface while -import { Reader } from 'protobufjs/minimal'; - -// Protecting the user from protobuf oddities -import * as proto from '../proto/waku/v2/message'; - -export const DefaultContentTopic = '/waku/2/default-content/proto'; -const DefaultVersion = 0; - -export class WakuMessage { - public constructor(public proto: proto.WakuMessage) {} - - /** - * Create Message with a utf-8 string as payload. - */ - static fromUtf8String( - utf8: string, - contentTopic: string = DefaultContentTopic, - timestamp: Date = new Date() - ): WakuMessage { - const payload = Buffer.from(utf8, 'utf-8'); - return new WakuMessage({ - payload, - version: DefaultVersion, - contentTopic, - timestamp: timestamp.valueOf() / 1000, - }); - } - - /** - * Create Message with a byte array as payload. - */ - static fromBytes( - payload: Uint8Array, - contentTopic: string = DefaultContentTopic, - timestamp: Date = new Date() - ): WakuMessage { - return new WakuMessage({ - payload, - timestamp: timestamp.valueOf() / 1000, - version: DefaultVersion, - contentTopic, - }); - } - - static decode(bytes: Uint8Array): WakuMessage { - const wakuMsg = proto.WakuMessage.decode(Reader.create(bytes)); - return new WakuMessage(wakuMsg); - } - - encode(): Uint8Array { - return proto.WakuMessage.encode(this.proto).finish(); - } - - get payloadAsUtf8(): string { - if (!this.proto.payload) { - return ''; - } - - return Array.from(this.proto.payload) - .map((char) => { - return String.fromCharCode(char); - }) - .join(''); - } - - get payload(): Uint8Array | undefined { - return this.proto.payload; - } - - get contentTopic(): string | undefined { - return this.proto.contentTopic; - } - - get version(): number | undefined { - return this.proto.version; - } - - get timestamp(): Date | undefined { - if (this.proto.timestamp) { - return new Date(this.proto.timestamp * 1000); - } - return; - } -} diff --git a/src/lib/waku_message/index.spec.ts b/src/lib/waku_message/index.spec.ts new file mode 100644 index 0000000000..532bfbe5d3 --- /dev/null +++ b/src/lib/waku_message/index.spec.ts @@ -0,0 +1,175 @@ +import { expect } from 'chai'; +import debug from 'debug'; +import fc from 'fast-check'; +import TCP from 'libp2p-tcp'; + +import { + makeLogFileName, + NimWaku, + NOISE_KEY_1, + WakuRelayMessage, +} from '../../test_utils'; +import { delay } from '../delay'; +import { hexToBuf } from '../utils'; +import { Waku } from '../waku'; + +import { generatePrivateKey, getPublicKey } from './version_1'; + +import { DefaultContentTopic, WakuMessage } from './index'; + +const dbg = debug('waku:test:message'); + +describe('Waku Message', function () { + it('Waku message round trip binary serialization [clear]', async function () { + await fc.assert( + fc.asyncProperty(fc.string(), async (s) => { + const msg = await WakuMessage.fromUtf8String(s); + const binary = msg.encode(); + const actual = await WakuMessage.decode(binary); + + expect(actual).to.deep.equal(msg); + }) + ); + }); + + it('Payload to utf-8', async function () { + await fc.assert( + fc.asyncProperty(fc.string(), async (s) => { + const msg = await WakuMessage.fromUtf8String(s); + const utf8 = msg.payloadAsUtf8; + + return utf8 === s; + }) + ); + }); + + it('Waku message round trip binary encryption [asymmetric, no signature]', async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (payload, privKey) => { + const publicKey = getPublicKey(privKey); + + const msg = await WakuMessage.fromBytes(payload, { + encPublicKey: publicKey, + }); + + const wireBytes = msg.encode(); + const actual = await WakuMessage.decode(wireBytes, [privKey]); + + expect(actual?.payload).to.deep.equal(payload); + } + ) + ); + }); + + it('Waku message round trip binary encryption [asymmetric, signature]', async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (payload, sigPrivKey, encPrivKey) => { + const sigPubKey = getPublicKey(sigPrivKey); + const encPubKey = getPublicKey(encPrivKey); + + const msg = await WakuMessage.fromBytes(payload, { + encPublicKey: encPubKey, + sigPrivKey: sigPrivKey, + }); + + const wireBytes = msg.encode(); + const actual = await WakuMessage.decode(wireBytes, [encPrivKey]); + + expect(actual?.payload).to.deep.equal(payload); + expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey); + } + ) + ); + }); +}); + +describe.skip('Interop: Nim', function () { + let waku: Waku; + let nimWaku: NimWaku; + + beforeEach(async function () { + this.timeout(30_000); + + waku = await Waku.create({ + staticNoiseKey: NOISE_KEY_1, + libp2p: { + addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] }, + modules: { transport: [TCP] }, + }, + }); + + const multiAddrWithId = waku.getLocalMultiaddrWithID(); + nimWaku = new NimWaku(makeLogFileName(this)); + await nimWaku.start({ staticnode: multiAddrWithId, rpcPrivate: true }); + + await new Promise((resolve) => + waku.libp2p.pubsub.once('gossipsub:heartbeat', resolve) + ); + }); + + afterEach(async function () { + nimWaku ? nimWaku.stop() : null; + waku ? await waku.stop() : null; + }); + + it('JS decrypts nim message [asymmetric, no signature]', async function () { + this.timeout(10000); + await delay(200); + + const messageText = 'Here is an encrypted message.'; + const message: WakuRelayMessage = { + contentTopic: DefaultContentTopic, + payload: Buffer.from(messageText, 'utf-8').toString('hex'), + }; + + const privateKey = generatePrivateKey(); + + waku.relay.addDecryptionPrivateKey(privateKey); + + const receivedMsgPromise: Promise = new Promise((resolve) => { + waku.relay.addObserver(resolve); + }); + + const publicKey = getPublicKey(privateKey); + dbg('Post message'); + await nimWaku.postAsymmetricMessage(message, publicKey); + + const receivedMsg = await receivedMsgPromise; + + expect(receivedMsg.contentTopic).to.eq(message.contentTopic); + expect(receivedMsg.version).to.eq(1); + expect(receivedMsg.payloadAsUtf8).to.eq(messageText); + }); + + it('Js encrypts message for nim [asymmetric, no signature]', async function () { + this.timeout(5000); + + const keyPair = await nimWaku.getAsymmetricKeyPair(); + const privateKey = hexToBuf(keyPair.privateKey); + const publicKey = hexToBuf(keyPair.publicKey); + + const messageText = 'This is a message I am going to encrypt'; + const message = await WakuMessage.fromUtf8String(messageText, { + encPublicKey: publicKey, + }); + + await waku.relay.send(message); + + let msgs: WakuRelayMessage[] = []; + + while (msgs.length === 0) { + await delay(200); + msgs = await nimWaku.getAsymmetricMessages(privateKey); + } + + expect(msgs[0].contentTopic).to.equal(message.contentTopic); + expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText); + }); +}); diff --git a/src/lib/waku_message/index.ts b/src/lib/waku_message/index.ts new file mode 100644 index 0000000000..cce03537c5 --- /dev/null +++ b/src/lib/waku_message/index.ts @@ -0,0 +1,209 @@ +// Ensure that this class matches the proto interface while +import { Buffer } from 'buffer'; + +import debug from 'debug'; +import { Reader } from 'protobufjs/minimal'; + +// Protecting the user from protobuf oddities +import * as proto from '../../proto/waku/v2/message'; + +import * as version_1 from './version_1'; + +export const DefaultContentTopic = '/waku/2/default-content/proto'; +const DefaultVersion = 0; +const dbg = debug('waku:message'); + +export interface Options { + contentTopic?: string; + timestamp?: Date; + encPublicKey?: Uint8Array; + sigPrivKey?: Uint8Array; +} + +export class WakuMessage { + private constructor( + public proto: proto.WakuMessage, + private _signaturePublicKey?: Uint8Array, + private _signature?: Uint8Array + ) {} + + /** + * Create Message with a utf-8 string as payload. + */ + static async fromUtf8String( + utf8: string, + opts?: Options + ): Promise { + const payload = Buffer.from(utf8, 'utf-8'); + return WakuMessage.fromBytes(payload, opts); + } + + /** + * Create a Waku Message with the given payload. + * + * By default, the payload is kept clear (version 0). + * If `opts.encPublicKey` is passed, the payload is encrypted using + * asymmetric encryption (version 1). + * + * If `opts.sigPrivKey` is passed and version 1 is used, the payload is signed + * before encryption. + */ + static async fromBytes( + payload: Uint8Array, + opts?: Options + ): Promise { + const { timestamp, contentTopic, encPublicKey, sigPrivKey } = Object.assign( + { timestamp: new Date(), contentTopic: DefaultContentTopic }, + opts ? opts : {} + ); + + let _payload = payload; + let version = DefaultVersion; + let sig; + if (encPublicKey) { + const enc = version_1.clearEncode(_payload, sigPrivKey); + _payload = await version_1.encryptAsymmetric(enc.payload, encPublicKey); + sig = enc.sig; + version = 1; + } + + return new WakuMessage( + { + payload: _payload, + timestamp: timestamp.valueOf() / 1000, + version, + contentTopic, + }, + sig?.publicKey, + sig?.signature + ); + } + + /** + * Decode a byte array into Waku Message. + * + * If the payload is encrypted, then `decPrivateKey` is used for decryption. + */ + static async decode( + bytes: Uint8Array, + decPrivateKeys?: Uint8Array[] + ): Promise { + const protoBuf = proto.WakuMessage.decode(Reader.create(bytes)); + + return WakuMessage.decodeProto(protoBuf, decPrivateKeys); + } + + /** + * Decode a Waku Message Protobuf Object into Waku Message. + * + * If the payload is encrypted, then `decPrivateKey` is used for decryption. + */ + static async decodeProto( + protoBuf: proto.WakuMessage, + decPrivateKeys?: Uint8Array[] + ): Promise { + if (protoBuf.payload === undefined) { + dbg('Payload is undefined'); + return; + } + const payload = protoBuf.payload; + + let signaturePublicKey; + let signature; + if (protoBuf.version === 1 && protoBuf.payload) { + if (decPrivateKeys === undefined) { + dbg('Payload is encrypted but no private keys have been provided.'); + + return; + } + + // Returns a bunch of `undefined` and hopefully one decrypted result + const allResults = await Promise.all( + decPrivateKeys.map(async (privateKey) => { + try { + return await version_1.decryptAsymmetric(payload, privateKey); + } catch (e) { + dbg('Failed to decrypt asymmetric message', e); + return; + } + }) + ); + + const isDefined = (dec: Uint8Array | undefined): dec is Uint8Array => { + return !!dec; + }; + + const decodedResults = allResults.filter(isDefined); + + if (decodedResults.length === 0) { + dbg('Failed to decrypt payload.'); + return; + } + const dec = decodedResults[0]; + + const res = await version_1.clearDecode(dec); + if (!res) { + dbg('Failed to decode payload.'); + return; + } + Object.assign(protoBuf, { payload: res.payload }); + signaturePublicKey = res.sig?.publicKey; + signature = res.sig?.signature; + } + + return new WakuMessage(protoBuf, signaturePublicKey, signature); + } + + encode(): Uint8Array { + return proto.WakuMessage.encode(this.proto).finish(); + } + + get payloadAsUtf8(): string { + if (!this.proto.payload) { + return ''; + } + + return Array.from(this.proto.payload) + .map((char) => { + return String.fromCharCode(char); + }) + .join(''); + } + + get payload(): Uint8Array | undefined { + return this.proto.payload; + } + + get contentTopic(): string | undefined { + return this.proto.contentTopic; + } + + get version(): number | undefined { + return this.proto.version; + } + + get timestamp(): Date | undefined { + if (this.proto.timestamp) { + return new Date(this.proto.timestamp * 1000); + } + return; + } + + /** + * The public key used to sign the message. + * + * MAY be present if the message is version 1. + */ + get signaturePublicKey(): Uint8Array | undefined { + return this._signaturePublicKey; + } + + /** + * The signature of the message. + * + * MAY be present if the message is version 1. + */ + get signature(): Uint8Array | undefined { + return this._signature; + } +} diff --git a/src/lib/waku_message/version_1.spec.ts b/src/lib/waku_message/version_1.spec.ts new file mode 100644 index 0000000000..d37326658c --- /dev/null +++ b/src/lib/waku_message/version_1.spec.ts @@ -0,0 +1,57 @@ +import { expect } from 'chai'; +import fc from 'fast-check'; + +import { + clearDecode, + clearEncode, + decryptAsymmetric, + encryptAsymmetric, + getPublicKey, +} from './version_1'; + +describe('Waku Message Version 1', function () { + it('Sign & Recover', function () { + fc.assert( + fc.property( + fc.uint8Array(), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + (message, privKey) => { + const enc = clearEncode(message, privKey); + const res = clearDecode(enc.payload); + + const pubKey = getPublicKey(privKey); + + expect(res?.payload).deep.equal( + message, + 'Payload was not encrypted then decrypted correctly' + ); + expect(res?.sig?.publicKey).deep.equal( + pubKey, + 'signature Public key was not recovered from encrypted then decrypted signature' + ); + expect(enc?.sig?.publicKey).deep.equal( + pubKey, + 'Incorrect signature public key was returned when signing the payload' + ); + } + ) + ); + }); + + it('Asymmetric encrypt & Decrypt', async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (message, privKey) => { + const publicKey = getPublicKey(privKey); + + const enc = await encryptAsymmetric(message, publicKey); + const res = await decryptAsymmetric(enc, privKey); + + expect(res).deep.equal(message); + } + ) + ); + }); +}); diff --git a/src/lib/waku_message/version_1.ts b/src/lib/waku_message/version_1.ts new file mode 100644 index 0000000000..96c314aac3 --- /dev/null +++ b/src/lib/waku_message/version_1.ts @@ -0,0 +1,210 @@ +import { Buffer } from 'buffer'; +import * as crypto from 'crypto'; + +import ecies from 'ecies-parity'; +import { keccak256 } from 'js-sha3'; +import * as secp256k1 from 'secp256k1'; + +import { hexToBuf } from '../utils'; + +const FlagsLength = 1; +const FlagMask = 3; // 0011 +const IsSignedMask = 4; // 0100 +const PaddingTarget = 256; +const SignatureLength = 65; + +/** + * Encode the payload pre-encryption. + * + * @internal + * @param messagePayload: The payload to include in the message + * @param sigPrivKey: If set, a signature using this private key is added. + * @returns The encoded payload, ready for encryption using {@link encryptAsymmetric} + * or {@link encryptSymmetric}. + */ +export function clearEncode( + messagePayload: Uint8Array, + sigPrivKey?: Uint8Array +): { payload: Uint8Array; sig?: Signature } { + let envelope = Buffer.from([0]); // No flags + envelope = addPayloadSizeField(envelope, messagePayload); + envelope = Buffer.concat([envelope, messagePayload]); + + // Calculate padding: + let rawSize = + FlagsLength + + getSizeOfPayloadSizeField(messagePayload) + + messagePayload.length; + + if (sigPrivKey) { + rawSize += SignatureLength; + } + + const remainder = rawSize % PaddingTarget; + const paddingSize = PaddingTarget - remainder; + const pad = randomBytes(paddingSize); + + if (!validateDataIntegrity(pad, paddingSize)) { + throw new Error('failed to generate random padding of size ' + paddingSize); + } + + envelope = Buffer.concat([envelope, pad]); + + let sig; + if (sigPrivKey) { + envelope[0] |= IsSignedMask; + const hash = keccak256(envelope); + const s = secp256k1.ecdsaSign(hexToBuf(hash), sigPrivKey); + envelope = Buffer.concat([envelope, s.signature, Buffer.from([s.recid])]); + sig = { + signature: Buffer.from(s.signature), + publicKey: getPublicKey(sigPrivKey), + }; + } + + return { payload: envelope, sig }; +} + +export type Signature = { + signature: Uint8Array; + publicKey: Uint8Array; +}; + +/** + * Decode a decrypted payload. + * + * @internal + */ +export function clearDecode( + message: Uint8Array | Buffer +): { payload: Uint8Array; sig?: Signature } | undefined { + const buf = Buffer.from(message); + let start = 1; + let sig; + + const sizeOfPayloadSizeField = buf.readUIntLE(0, 1) & FlagMask; + + if (sizeOfPayloadSizeField === 0) return; + + const payloadSize = buf.readUIntLE(start, sizeOfPayloadSizeField); + start += sizeOfPayloadSizeField; + const payload = buf.slice(start, start + payloadSize); + + const isSigned = (buf.readUIntLE(0, 1) & IsSignedMask) == IsSignedMask; + if (isSigned) { + const signature = getSignature(buf); + const hash = getHash(buf, isSigned); + const publicKey = ecRecoverPubKey(hash, signature); + sig = { signature, publicKey }; + } + + return { payload, sig }; +} + +/** + * Proceed with Asymmetric encryption of the data as per [26/WAKU-PAYLOAD](rfc.vac.dev/spec/26/). + * The data MUST be flags | payload-length | payload | [signature]. + * The returned result can be set to `WakuMessage.payload`. + * + * @internal + */ +export async function encryptAsymmetric( + data: Uint8Array | Buffer, + publicKey: Uint8Array | Buffer +): Promise { + return ecies.encrypt(Buffer.from(publicKey), Buffer.from(data)); +} + +export async function decryptAsymmetric( + payload: Uint8Array | Buffer, + privKey: Uint8Array | Buffer +): Promise { + return ecies.decrypt(Buffer.from(privKey), Buffer.from(payload)); +} + +/** + * Generate a new private key + */ +export function generatePrivateKey(): Uint8Array { + return randomBytes(32); +} + +/** + * Return the public key for the given private key + */ +export function getPublicKey(privateKey: Uint8Array | Buffer): Uint8Array { + return secp256k1.publicKeyCreate(privateKey, false); +} + +/** + * Computes the flags & auxiliary-field as per [26/WAKU-PAYLOAD](rfc.vac.dev/spec/26/). + */ +function addPayloadSizeField(msg: Buffer, payload: Uint8Array): Buffer { + const fieldSize = getSizeOfPayloadSizeField(payload); + let field = Buffer.alloc(4); + field.writeUInt32LE(payload.length, 0); + field = field.slice(0, fieldSize); + msg = Buffer.concat([msg, field]); + msg[0] |= fieldSize; + return msg; +} + +/** + * Returns the size of the auxiliary-field which in turns contains the payload size + */ +function getSizeOfPayloadSizeField(payload: Uint8Array): number { + let s = 1; + for (let i = payload.length; i >= 256; i /= 256) { + s++; + } + return s; +} + +function validateDataIntegrity( + value: Uint8Array, + expectedSize: number +): boolean { + if (value.length !== expectedSize) { + return false; + } + + if ( + expectedSize > 3 && + Buffer.from(value).equals(Buffer.alloc(value.length)) + ) { + return false; + } + + return true; +} + +function getSignature(message: Buffer): Buffer { + return message.slice(message.length - SignatureLength, message.length); +} + +function getHash(message: Buffer, isSigned: boolean): string { + if (isSigned) { + return keccak256(message.slice(0, message.length - SignatureLength)); + } + return keccak256(message); +} + +function ecRecoverPubKey(messageHash: string, signature: Buffer): Uint8Array { + const recovery = signature.slice(64).readIntBE(0, 1); + return secp256k1.ecdsaRecover( + signature.slice(0, 64), + recovery, + hexToBuf(messageHash), + false + ); +} + +function randomBytes(length: number): Uint8Array { + if (typeof window !== 'undefined' && window && window.crypto) { + const array = new Uint8Array(length); + window.crypto.getRandomValues(array); + return array; + } else { + return crypto.randomBytes(length); + } +} diff --git a/src/lib/waku_relay/index.spec.ts b/src/lib/waku_relay/index.spec.ts index a3d9a2ac20..8fbc217fde 100644 --- a/src/lib/waku_relay/index.spec.ts +++ b/src/lib/waku_relay/index.spec.ts @@ -79,11 +79,9 @@ describe('Waku Relay', () => { const messageText = 'JS to JS communication works'; const messageTimestamp = new Date('1995-12-17T03:24:00'); - const message = WakuMessage.fromUtf8String( - messageText, - undefined, - messageTimestamp - ); + const message = await WakuMessage.fromUtf8String(messageText, { + timestamp: messageTimestamp, + }); const receivedMsgPromise: Promise = new Promise( (resolve) => { @@ -108,8 +106,12 @@ describe('Waku Relay', () => { const fooMessageText = 'Published on content topic foo'; const barMessageText = 'Published on content topic bar'; - const fooMessage = WakuMessage.fromUtf8String(fooMessageText, 'foo'); - const barMessage = WakuMessage.fromUtf8String(barMessageText, 'bar'); + const fooMessage = await WakuMessage.fromUtf8String(fooMessageText, { + contentTopic: 'foo', + }); + const barMessage = await WakuMessage.fromUtf8String(barMessageText, { + contentTopic: 'bar', + }); const receivedBarMsgPromise: Promise = new Promise( (resolve) => { @@ -144,10 +146,9 @@ describe('Waku Relay', () => { const messageText = 'Published on content topic with added then deleted observer'; - const message = WakuMessage.fromUtf8String( - messageText, - 'added-then-deleted-observer' - ); + const message = await WakuMessage.fromUtf8String(messageText, { + contentTopic: 'added-then-deleted-observer', + }); // The promise **fails** if we receive a message on this observer. const receivedMsgPromise: Promise = new Promise( @@ -204,7 +205,7 @@ describe('Waku Relay', () => { ]); const messageText = 'Communicating using a custom pubsub topic'; - const message = WakuMessage.fromUtf8String(messageText); + const message = await WakuMessage.fromUtf8String(messageText); const waku2ReceivedMsgPromise: Promise = new Promise( (resolve) => { @@ -275,7 +276,7 @@ describe('Waku Relay', () => { this.timeout(5000); const messageText = 'This is a message'; - const message = WakuMessage.fromUtf8String(messageText); + const message = await WakuMessage.fromUtf8String(messageText); await waku.relay.send(message); @@ -294,7 +295,7 @@ describe('Waku Relay', () => { it('Nim publishes to js', async function () { this.timeout(5000); const messageText = 'Here is another message.'; - const message = WakuMessage.fromUtf8String(messageText); + const message = await WakuMessage.fromUtf8String(messageText); const receivedMsgPromise: Promise = new Promise( (resolve) => { @@ -360,7 +361,7 @@ describe('Waku Relay', () => { this.timeout(30000); const messageText = 'This is a message'; - const message = WakuMessage.fromUtf8String(messageText); + const message = await WakuMessage.fromUtf8String(messageText); await delay(1000); await waku.relay.send(message); @@ -381,7 +382,7 @@ describe('Waku Relay', () => { await delay(200); const messageText = 'Here is another message.'; - const message = WakuMessage.fromUtf8String(messageText); + const message = await WakuMessage.fromUtf8String(messageText); const receivedMsgPromise: Promise = new Promise( (resolve) => { @@ -463,7 +464,7 @@ describe('Waku Relay', () => { ).to.be.false; const msgStr = 'Hello there!'; - const message = WakuMessage.fromUtf8String(msgStr); + const message = await WakuMessage.fromUtf8String(msgStr); const waku2ReceivedMsgPromise: Promise = new Promise( (resolve) => { diff --git a/src/lib/waku_relay/index.ts b/src/lib/waku_relay/index.ts index 784935e918..041a428e35 100644 --- a/src/lib/waku_relay/index.ts +++ b/src/lib/waku_relay/index.ts @@ -1,3 +1,4 @@ +import debug from 'debug'; import Libp2p from 'libp2p'; import Gossipsub from 'libp2p-gossipsub'; import { AddrInfo, MessageIdFunction } from 'libp2p-gossipsub/src/interfaces'; @@ -24,6 +25,8 @@ import { DefaultPubsubTopic, RelayCodec } from './constants'; import { getRelayPeers } from './get_relay_peers'; import { RelayHeartbeat } from './relay_heartbeat'; +const dbg = debug('waku:relay'); + export { RelayCodec, DefaultPubsubTopic }; /** @@ -60,9 +63,15 @@ export interface GossipOptions { export class WakuRelay extends Gossipsub { heartbeat: RelayHeartbeat; pubsubTopic: string; + + /** + * Decryption private keys to use to attempt decryption of incoming messages. + */ + public decPrivateKeys: Set; + /** * observers called when receiving new message. - * Observers under key "" are always called. + * Observers under key `""` are always called. */ public observers: { [contentTopic: string]: Set<(message: WakuMessage) => void>; @@ -82,16 +91,13 @@ export class WakuRelay extends Gossipsub { this.heartbeat = new RelayHeartbeat(this); this.observers = {}; + this.decPrivateKeys = new Set(); const multicodecs = [constants.RelayCodec]; Object.assign(this, { multicodecs }); - if (options?.pubsubTopic) { - this.pubsubTopic = options.pubsubTopic; - } else { - this.pubsubTopic = constants.DefaultPubsubTopic; - } + this.pubsubTopic = options?.pubsubTopic || constants.DefaultPubsubTopic; } /** @@ -114,7 +120,23 @@ export class WakuRelay extends Gossipsub { */ public async send(message: WakuMessage): Promise { const msg = message.encode(); - await super.publish(this.pubsubTopic, new Buffer(msg)); + await super.publish(this.pubsubTopic, Buffer.from(msg)); + } + + /** + * Register a decryption private key to attempt decryption of messages of + * the given content topic. + */ + addDecryptionPrivateKey(privateKey: Uint8Array): void { + this.decPrivateKeys.add(privateKey); + } + + /** + * Delete a decryption private key to attempt decryption of messages of + * the given content topic. + */ + deleteDecryptionPrivateKey(privateKey: Uint8Array): void { + this.decPrivateKeys.delete(privateKey); } /** @@ -123,6 +145,7 @@ export class WakuRelay extends Gossipsub { * @param callback called when a new message is received via waku relay * @param contentTopics Content Topics for which the callback with be called, * all of them if undefined, [] or ["",..] is passed. + * @param decPrivateKeys Private keys used to decrypt incoming Waku Messages. * @returns {void} */ addObserver( @@ -185,19 +208,30 @@ export class WakuRelay extends Gossipsub { */ subscribe(pubsubTopic: string): void { this.on(pubsubTopic, (event) => { - const wakuMsg = WakuMessage.decode(event.data); - if (this.observers['']) { - this.observers[''].forEach((callbackFn) => { - callbackFn(wakuMsg); + dbg(`Message received on ${pubsubTopic}`); + WakuMessage.decode(event.data, Array.from(this.decPrivateKeys)) + .then((wakuMsg) => { + if (!wakuMsg) { + dbg('Failed to decode Waku Message'); + return; + } + + if (this.observers['']) { + this.observers[''].forEach((callbackFn) => { + callbackFn(wakuMsg); + }); + } + if (wakuMsg.contentTopic) { + if (this.observers[wakuMsg.contentTopic]) { + this.observers[wakuMsg.contentTopic].forEach((callbackFn) => { + callbackFn(wakuMsg); + }); + } + } + }) + .catch((e) => { + dbg('Failed to decode Waku Message', e); }); - } - if (wakuMsg.contentTopic) { - if (this.observers[wakuMsg.contentTopic]) { - this.observers[wakuMsg.contentTopic].forEach((callbackFn) => { - callbackFn(wakuMsg); - }); - } - } }); super.subscribe(pubsubTopic); diff --git a/src/lib/waku_store/index.spec.ts b/src/lib/waku_store/index.spec.ts index 9e0ee71ea2..2a5b935b5d 100644 --- a/src/lib/waku_store/index.spec.ts +++ b/src/lib/waku_store/index.spec.ts @@ -24,7 +24,9 @@ describe('Waku Store', () => { for (let i = 0; i < 2; i++) { expect( - await nimWaku.sendMessage(WakuMessage.fromUtf8String(`Message ${i}`)) + await nimWaku.sendMessage( + await WakuMessage.fromUtf8String(`Message ${i}`) + ) ).to.be.true; } @@ -58,7 +60,9 @@ describe('Waku Store', () => { for (let i = 0; i < 15; i++) { expect( - await nimWaku.sendMessage(WakuMessage.fromUtf8String(`Message ${i}`)) + await nimWaku.sendMessage( + await WakuMessage.fromUtf8String(`Message ${i}`) + ) ).to.be.true; } @@ -98,7 +102,7 @@ describe('Waku Store', () => { for (let i = 0; i < 2; i++) { expect( await nimWaku.sendMessage( - WakuMessage.fromUtf8String(`Message ${i}`), + await WakuMessage.fromUtf8String(`Message ${i}`), customPubSubTopic ) ).to.be.true; diff --git a/src/lib/waku_store/index.ts b/src/lib/waku_store/index.ts index 88102ce2e2..c0cbe65bf1 100644 --- a/src/lib/waku_store/index.ts +++ b/src/lib/waku_store/index.ts @@ -114,19 +114,24 @@ export class WakuStore { return messages; } - const pageMessages = response.messages.map((protoMsg) => { - return new WakuMessage(protoMsg); - }); + const pageMessages: WakuMessage[] = []; + await Promise.all( + response.messages.map(async (protoMsg) => { + const msg = await WakuMessage.decodeProto(protoMsg); + + if (msg) { + messages.push(msg); + pageMessages.push(msg); + } + }) + ); if (opts.callback) { // TODO: Test the callback feature + // TODO: Change callback to take individual messages opts.callback(pageMessages); } - pageMessages.forEach((wakuMessage) => { - messages.push(wakuMessage); - }); - const responsePageSize = response.pagingInfo?.pageSize; const queryPageSize = historyRpcQuery.query?.pagingInfo?.pageSize; if ( diff --git a/src/test_utils/nim_waku.ts b/src/test_utils/nim_waku.ts index 8b5ac45179..cef77d7c4c 100644 --- a/src/test_utils/nim_waku.ts +++ b/src/test_utils/nim_waku.ts @@ -41,6 +41,7 @@ export interface Args { persistMessages?: boolean; lightpush?: boolean; topics?: string; + rpcPrivate?: boolean; } export enum LogLevel { @@ -53,6 +54,16 @@ export enum LogLevel { Fatal = 'fatal', } +export interface KeyPair { + privateKey: string; + publicKey: string; +} + +export interface WakuRelayMessage { + payload: string; + contentTopic?: string; +} + export class NimWaku { private process?: ChildProcess; private pid?: number; @@ -180,9 +191,64 @@ export class NimWaku { async messages(): Promise { this.checkProcess(); - return this.rpcCall('get_waku_v2_relay_v1_messages', [ - DefaultPubsubTopic, - ]).then((msgs) => msgs.map((protoMsg) => new WakuMessage(protoMsg))); + const isDefined = (msg: WakuMessage | undefined): msg is WakuMessage => { + return !!msg; + }; + + const protoMsgs = await this.rpcCall( + 'get_waku_v2_relay_v1_messages', + [DefaultPubsubTopic] + ); + + const msgs = await Promise.all( + protoMsgs.map(async (protoMsg) => await WakuMessage.decodeProto(protoMsg)) + ); + + return msgs.filter(isDefined); + } + + async getAsymmetricKeyPair(): Promise { + this.checkProcess(); + + const { seckey, pubkey } = await this.rpcCall<{ + seckey: string; + pubkey: string; + }>('get_waku_v2_private_v1_asymmetric_keypair', []); + + return { privateKey: seckey, publicKey: pubkey }; + } + + async postAsymmetricMessage( + message: WakuRelayMessage, + publicKey: Uint8Array, + pubsubTopic?: string + ): Promise { + this.checkProcess(); + + if (!message.payload) { + throw 'Attempting to send empty message'; + } + + return this.rpcCall('post_waku_v2_private_v1_asymmetric_message', [ + pubsubTopic ? pubsubTopic : DefaultPubsubTopic, + message, + '0x' + bufToHex(publicKey), + ]); + } + + async getAsymmetricMessages( + privateKey: Uint8Array, + pubsubTopic?: string + ): Promise { + this.checkProcess(); + + return await this.rpcCall( + 'get_waku_v2_private_v1_asymmetric_messages', + [ + pubsubTopic ? pubsubTopic : DefaultPubsubTopic, + '0x' + bufToHex(privateKey), + ] + ); } async getPeerId(): Promise { diff --git a/src/types/ecies-parity.d.ts b/src/types/ecies-parity.d.ts new file mode 100644 index 0000000000..9888231f64 --- /dev/null +++ b/src/types/ecies-parity.d.ts @@ -0,0 +1,43 @@ +// TypeScript Version: 2.1 +/// +declare module 'ecies-parity' { + // Compute the public key for a given private key. + export function getPublic(privateKey: Buffer): Buffer; + + // Compute the compressed public key for a given private key. + export function getPublicCompressed(privateKey: Buffer): Buffer; + + // Create an ECDSA signature. + export function sign(key: Buffer, msg: Buffer): Promise; + + // Verify an ECDSA signature. + export function verify( + publicKey: Buffer, + msg: Buffer, + sig: Buffer + ): Promise; + + // Derive shared secret for given private and public keys. + export function derive( + privateKeyA: Buffer, + publicKeyB: Buffer + ): Promise; + + // Input/output structure for ECIES operations. + export interface Ecies { + iv: Buffer; + ephemPublicKey: Buffer; + ciphertext: Buffer; + mac: Buffer; + } + + // Encrypt message for given recipient's public key. + export function encrypt( + publicKeyTo: Buffer, + msg: Buffer, + opts?: { iv?: Buffer; ephemPrivateKey?: Buffer } + ): Promise; + + // Decrypt message using given private key. + export function decrypt(privateKey: Buffer, payload: Buffer): Promise; +} diff --git a/tsconfig.json b/tsconfig.json index 3edb3bcf13..544cde09a7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -41,7 +41,7 @@ // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, - "lib": ["es2017"], + "lib": ["es2017", "dom"], "types": ["node", "mocha"], "typeRoots": ["node_modules/@types", "src/types"] },