diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df80f7c69..76b8978efe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Examples (web-chat): New `/fleet` command to switch connection between Status prod and test fleets. +- Export `generatePrivateKey` and `getPublicKey` directly from the root. +- Usage of the encryption and signature APIs to the readme. + +### Changed +- **Breaking**: Renamed `WakuRelay.(add|delete)PrivateDecryptionKey` to `WakuRelay.(add|delete)DecryptionKey` to make it clearer that it accepts both symmetric keys and asymmetric private keys. + +### Fix +- Align `WakuMessage` readme example with actual code behaviour. + ## [0.8.0] - 2021-07-15 ### Added diff --git a/README.md b/README.md index 466776c045..72668e4f8d 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,17 @@ Install `js-waku` package: npm install js-waku ``` -Start a waku node: +### Start a waku node -```javascript +```ts import { Waku } from 'js-waku'; const waku = await Waku.create(); ``` -Connect to a new peer: - -```javascript -import { multiaddr } from 'multiaddr'; -import PeerId from 'peer-id'; +### Connect to a new peer +```ts // Directly dial a new peer await waku.dial('/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ'); @@ -36,17 +33,18 @@ waku.addPeerToAddressBook( You can also use `getStatusFleetNodes` to connect to nodes run by Status: -```javascript +```ts import { getStatusFleetNodes } from 'js-waku'; -const nodes = await getStatusFleetNodes(); -await Promise.all( - nodes.map((addr) => { - return waku.dial(addr); - }) -); +getStatusFleetNodes().then((nodes) => { + nodes.forEach((addr) => { + waku.dial(addr); + }); +}); ``` +### Listen for messages + The `contentTopic` is a metadata `string` that allows categorization of messages on the waku network. Depending on your use case, you can either create one (or several) new `contentTopic`(s) or look at the [RFCs](https://rfc.vac.dev/) and use an existing `contentTopic`. See the [Waku v2 Topic Usage Recommendations](https://rfc.vac.dev/spec/23/) for more details. @@ -54,7 +52,7 @@ See the [Waku v2 Topic Usage Recommendations](https://rfc.vac.dev/spec/23/) for For example, if you were to use a new `contentTopic` such as `/my-cool-app/1/my-use-case/proto`, here is how to listen to new messages received via [Waku v2 Relay](https://rfc.vac.dev/spec/11/): -```javascript +```ts waku.relay.addObserver((msg) => { console.log("Message received:", msg.payloadAsUtf8) }, ["/my-cool-app/1/my-use-case/proto"]); @@ -62,37 +60,199 @@ waku.relay.addObserver((msg) => { The examples chat apps currently use content topic `"/toy-chat/2/huilong/proto"`. -Send a message on the waku relay network: +### Send messages -```javascript +There are two ways to send messages: + +#### Waku Relay + +[Waku Relay](https://rfc.vac.dev/spec/11/) is the most decentralized option, +peer receiving your messages are unlikely to know whether you are the originator or simply forwarding them. +However, it does not give you any delivery information. + +```ts import { WakuMessage } from 'js-waku'; -const msg = WakuMessage.fromUtf8String("Here is a message!", "/my-cool-app/1/my-use-case/proto") +const msg = await WakuMessage.fromUtf8String("Here is a message!", { contentTopic: "/my-cool-app/1/my-use-case/proto" }) await waku.relay.send(msg); ``` -The [Waku v2 Store protocol](https://rfc.vac.dev/spec/13/) enables full nodes to store messages received via relay -and clients to retrieve them (e.g. after resuming connectivity). +#### Waku Light Push + +[Waku Light Push](https://rfc.vac.dev/spec/19/) gives you confirmation that the light push server node has +received your message. +However, it means that said node knows you are the originator of the message. +It cannot guarantee that the node will forward the message. + +```ts +const ack = await waku.lightPush.push(message); +if (!ack?.isSuccess) { + // Message was not sent +} +``` + +### Retrieve archived messages + +The [Waku v2 Store protocol](https://rfc.vac.dev/spec/13/) enables more permanent nodes to store messages received via relay +and ephemeral clients to retrieve them (e.g. mobile phone resuming connectivity). The protocol implements pagination meaning that it may take several queries to retrieve all messages. Query a waku store peer to check historical messages: -```javascript -// Process messages once they are all retrieved: -const messages = await waku.store.queryHistory(storePeerId, ["my-cool-app"]); +```ts +// Process messages once they are all retrieved +const messages = await waku.store.queryHistory({ contentTopics: ["/my-cool-app/1/my-use-case/proto"] }); messages.forEach((msg) => { console.log("Message retrieved:", msg.payloadAsUtf8) }) // Or, pass a callback function to be executed as pages are received: -waku.store.queryHistory(storePeerId, ["my-cool-app"], - (messages) => { - messages.forEach((msg) => { - console.log("Message retrieved:", msg.payloadAsUtf8) - }) +waku.store.queryHistory({ + contentTopics: ["/my-cool-app/1/my-use-case/proto"], + callback: (messages) => { + messages.forEach((msg) => { + console.log("Message retrieved:", msg.payloadAsUtf8); + }); + } }); ``` +## Encryption & Signature + +With js-waku, you can: + +- Encrypt messages over the wire using public/private key pair (asymmetric encryption), +- Encrypt messages over the wire using a unique key to both encrypt and decrypt (symmetric encryption), +- Sign and verify your waku messages (must use encryption, compatible with both symmetric and asymmetric). + +### Cryptographic Libraries + +A quick note on the cryptographic libraries used as it is a not a straightforward affair: +- Asymmetric encryption: + Uses [ecies-geth](https://github.com/cyrildever/ecies-geth/) + which in turns uses [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) Web API (browser), + [secp256k1](https://www.npmjs.com/package/secp256k1) (native binding for node) + or [elliptic](https://www.npmjs.com/package/elliptic) (pure JS if none of the other libraries are available). +- Symmetric encryption: + Uses [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) Web API (browser) + or [NodeJS' crypto](https://nodejs.org/api/crypto.html) module. + +### Create new keys + +Asymmetric private keys and symmetric keys are expected to be 32 bytes arrays. + +```ts +import { generatePrivateKey, getPublicKey } from 'js-waku'; + +// Asymmetric +const privateKey = generatePrivateKey(); +const publicKey = getPublicKey(privateKey); + +// Symmetric +const symKey = generatePrivateKey(); +``` + +### Encrypt Waku Messages + +To encrypt your waku messages, simply pass the encryption key when creating it: + +```ts +import { WakuMessage } from 'js-waku'; + +// Asymmetric +const message = await WakuMessage.fromBytes(payload, { + contentTopic: myAppContentTopic, + encPublicKey: publicKey, + }); + +// Symmetric +const message = await WakuMessage.fromBytes(payload, { + contentTopic: myAppContentTopic, + symKey: symKey, + }); +``` + +### Decrypt Waku Messages + +#### Waku Relay + +If you expect to receive encrypted messages then simply add private decryption key(s) to `WakuRelay`. +Waku Relay will attempt to decrypt incoming messages with each keys, both for symmetric and asymmetric encryption. +Messages that are successfully decrypted (or received in clear) will be passed to the observers, other messages will be omitted. + +```ts +// Asymmetric +waku.relay.addDecryptionKey(privateKey); + +// Symmetric +waku.relay.addDecryptionKey(symKey); + +// Then add the observer +waku.relay.addObserver(callback, [contentTopic]); +``` + +Keys can be removed using `WakuMessage.deleteDecryptionKey`. + +#### Waku Store + +```ts +const messages = await waku.store.queryHistory({ + contentTopics: [], + decryptionKeys: [privateKey, symKey], +}); +``` + +Similarly to relay, only decrypted or clear messages will be returned. + +### Sign Waku Messages + +As per version 1`s [specs](https://rfc.vac.dev/spec/26/), signatures are only included in encrypted messages. +In the case where your app does not need encryption then you could use symmetric encryption with a trivial key, I intend to dig [more on the subject](https://github.com/status-im/js-waku/issues/74#issuecomment-880440186) and come back with recommendation and examples. + +Signature keys can be generated the same way asymmetric keys for encryption are: + +```ts +import { generatePrivateKey, getPublicKey, WakuMessage } from 'js-waku'; + +const signPrivateKey = generatePrivateKey(); + +// Asymmetric Encryption +const message = await WakuMessage.fromBytes(payload, { + contentTopic: myAppContentTopic, + encPublicKey: recipientPublicKey, + sigPrivKey: signPrivateKey + }); + +// Symmetric Encryption +const message = await WakuMessage.fromBytes(payload, { + contentTopic: myAppContentTopic, + encPublicKey: symKey, + sigPrivKey: signPrivateKey + }); +``` + +### Verify Waku Message signatures + +Two fields are available on `WakuMessage` regarding signatures: + +- `signaturePublicKey`: If the message is signed, it holds the public key of the signature, +- `signature`: If the message is signed, it holds the actual signature. + +Thus, if you expect messages to be signed by Alice, +you can simply compare `WakuMessage.signaturePublicKey` with Alice's public key. +As comparing hex string can lead to issues (is the `0x` prefix present?), +simply use helper function `equalByteArrays`. + +```ts +import { equalByteArrays } from 'js-waku/lib/utils'; + +const sigPubKey = wakuMessage.signaturePublicKey; + +const isSignedByAlice = sigPubKey && equalByteArrays(sigPubKey, alicePublicKey); +``` + +## More documentation + Find more [examples](#examples) below or checkout the latest `main` branch documentation at [https://status-im.github.io/js-waku/docs/](https://status-im.github.io/js-waku/docs/). diff --git a/examples/eth-dm/src/App.tsx b/examples/eth-dm/src/App.tsx index 8c1f5d0a8c..707b8e8d6e 100644 --- a/examples/eth-dm/src/App.tsx +++ b/examples/eth-dm/src/App.tsx @@ -128,13 +128,13 @@ function App() { if (!waku) return; if (!ethDmKeyPair) return; - waku.relay.addDecryptionPrivateKey(ethDmKeyPair.privateKey); + waku.relay.addDecryptionKey(ethDmKeyPair.privateKey); return function cleanUp() { if (!waku) return; if (!ethDmKeyPair) return; - waku.relay.deleteDecryptionPrivateKey(ethDmKeyPair.privateKey); + waku.relay.deleteDecryptionKey(ethDmKeyPair.privateKey); }; }, [waku, ethDmKeyPair]); diff --git a/examples/eth-dm/src/crypto.ts b/examples/eth-dm/src/crypto.ts index a1d9f209a3..abe65fc50f 100644 --- a/examples/eth-dm/src/crypto.ts +++ b/examples/eth-dm/src/crypto.ts @@ -4,10 +4,7 @@ import { ethers } from 'ethers'; import { Signer } from '@ethersproject/abstract-signer'; import { PublicKeyMessage } from './messaging/wire'; import { hexToBuf, equalByteArrays, bufToHex } from 'js-waku/lib/utils'; -import { - generatePrivateKey, - getPublicKey, -} from 'js-waku/lib/waku_message/version_1'; +import { generatePrivateKey, getPublicKey } from 'js-waku'; export interface KeyPair { privateKey: Uint8Array; diff --git a/examples/web-chat/src/App.tsx b/examples/web-chat/src/App.tsx index 24115bfeb9..70538fcb90 100644 --- a/examples/web-chat/src/App.tsx +++ b/examples/web-chat/src/App.tsx @@ -74,28 +74,27 @@ async function retrieveStoreMessages( } export default function App() { - let [newMessages, setNewMessages] = useState([]); - let [archivedMessages, setArchivedMessages] = useState([]); - let [stateWaku, setWaku] = useState(undefined); - let [nick, setNick] = useState(() => { + const [newMessages, setNewMessages] = useState([]); + const [archivedMessages, setArchivedMessages] = useState([]); + const [waku, setWaku] = useState(undefined); + const [nick, setNick] = useState(() => { const persistedNick = window.localStorage.getItem('nick'); return persistedNick !== null ? persistedNick : generate(); }); + const [fleetEnv, setFleetEnv] = useState(defaultFleetEnv); useEffect(() => { localStorage.setItem('nick', nick); }, [nick]); useEffect(() => { - if (stateWaku) return; - - initWaku(setWaku) + initWaku(fleetEnv, setWaku) .then(() => console.log('Waku init done')) .catch((e) => console.log('Waku init failed ', e)); - }, [stateWaku]); + }, [fleetEnv]); useEffect(() => { - if (!stateWaku) return; + if (!waku) return; const handleRelayMessage = (wakuMsg: WakuMessage) => { console.log('Message received: ', wakuMsg); @@ -105,23 +104,25 @@ export default function App() { } }; - stateWaku.relay.addObserver(handleRelayMessage, [ChatContentTopic]); + waku.relay.addObserver(handleRelayMessage, [ChatContentTopic]); - return; - }, [stateWaku]); + return function cleanUp() { + waku?.relay.deleteObserver(handleRelayMessage, [ChatContentTopic]); + }; + }, [waku]); useEffect(() => { - if (!stateWaku) return; + if (!waku) return; const handleProtocolChange = async ( - waku: Waku, + _waku: Waku, { peerId, protocols }: { peerId: PeerId; protocols: string[] } ) => { if (protocols.includes(StoreCodec)) { console.log(`${peerId.toB58String()}: retrieving archived messages}`); try { const length = await retrieveStoreMessages( - waku, + _waku, peerId, setArchivedMessages ); @@ -135,36 +136,38 @@ export default function App() { } }; - stateWaku.libp2p.peerStore.on( + waku.libp2p.peerStore.on( 'change:protocols', - handleProtocolChange.bind({}, stateWaku) + handleProtocolChange.bind({}, waku) ); - // To clean up listener when component unmounts - return () => { - stateWaku?.libp2p.peerStore.removeListener( + return function cleanUp() { + waku?.libp2p.peerStore.removeListener( 'change:protocols', - handleProtocolChange.bind({}, stateWaku) + handleProtocolChange.bind({}, waku) ); }; - }, [stateWaku]); + }, [waku]); return (
- + { const { command, response } = handleCommand( input, - stateWaku, - setNick + waku, + setNick, + fleetEnv, + setFleetEnv ); const commandMessages = response.map((msg) => { return Message.fromUtf8String(command, msg); @@ -178,7 +181,7 @@ export default function App() { ); } -async function initWaku(setter: (waku: Waku) => void) { +async function initWaku(fleetEnv: Environment, setter: (waku: Waku) => void) { try { const waku = await Waku.create({ libp2p: { @@ -193,7 +196,7 @@ async function initWaku(setter: (waku: Waku) => void) { setter(waku); - const nodes = await getNodes(); + const nodes = await getStatusFleetNodes(fleetEnv); await Promise.all( nodes.map((addr) => { return waku.dial(addr); @@ -204,11 +207,11 @@ async function initWaku(setter: (waku: Waku) => void) { } } -function getNodes() { +function defaultFleetEnv() { // Works with react-scripts if (process?.env?.NODE_ENV === 'development') { - return getStatusFleetNodes(Environment.Test); + return Environment.Test; } else { - return getStatusFleetNodes(Environment.Prod); + return Environment.Prod; } } diff --git a/examples/web-chat/src/Room.tsx b/examples/web-chat/src/Room.tsx index e36981eaac..790c7171bb 100644 --- a/examples/web-chat/src/Room.tsx +++ b/examples/web-chat/src/Room.tsx @@ -1,4 +1,4 @@ -import { ChatMessage, WakuMessage } from 'js-waku'; +import { ChatMessage, Environment, WakuMessage } from 'js-waku'; import { ChatContentTopic } from './App'; import ChatList from './ChatList'; import MessageInput from './MessageInput'; @@ -11,17 +11,28 @@ interface Props { archivedMessages: Message[]; commandHandler: (cmd: string) => void; nick: string; + fleetEnv: Environment; } export default function Room(props: Props) { const { waku } = useWaku(); + let relayPeers = 0; + let storePeers = 0; + if (waku) { + relayPeers = waku.relay.getPeers().size; + storePeers = waku.store.peers.length; + } + return (
- + : set a new nickname', '/info: some information about the node', '/connect : connect to the given peer', + '/fleet : connect to this fleet; beware it restarts waku node.', '/help: Display this help', ]; } @@ -22,11 +23,14 @@ function nick( return [`New nick: ${nick}`]; } -function info(waku: Waku | undefined): string[] { +function info(waku: Waku | undefined, fleetEnv: Environment): string[] { if (!waku) { return ['Waku node is starting']; } - return [`PeerId: ${waku.libp2p.peerId.toB58String()}`]; + return [ + `PeerId: ${waku.libp2p.peerId.toB58String()}`, + `Fleet environment: ${fleetEnv}`, + ]; } function connect(peer: string | undefined, waku: Waku | undefined): string[] { @@ -78,6 +82,28 @@ function peers(waku: Waku | undefined): string[] { return response; } +function fleet( + newFleetEnv: string | undefined, + currFleetEnv: Environment, + setFleetEnv: (fleetEnv: Environment) => void +): string[] { + switch (newFleetEnv) { + case Environment.Test: + setFleetEnv(newFleetEnv); + break; + case Environment.Prod: + setFleetEnv(newFleetEnv); + break; + default: + return [ + `Incorrect values, acceptable values are ${Environment.Test}, ${Environment.Prod}`, + `Current fleet environment is ${currFleetEnv}`, + ]; + } + + return [`New fleet Environment: ${newFleetEnv}`]; +} + function connections(waku: Waku | undefined): string[] { if (!waku) { return ['Waku node is starting']; @@ -107,7 +133,9 @@ function connections(waku: Waku | undefined): string[] { export default function handleCommand( input: string, waku: Waku | undefined, - setNick: (nick: string) => void + setNick: (nick: string) => void, + currFleetEnv: Environment, + setFleetEnv: (fleetEnv: Environment) => void ): { command: string; response: string[] } { let response: string[] = []; const args = parseInput(input); @@ -120,7 +148,7 @@ export default function handleCommand( nick(args.shift(), setNick).map((str) => response.push(str)); break; case '/info': - info(waku).map((str) => response.push(str)); + info(waku, currFleetEnv).map((str) => response.push(str)); break; case '/connect': connect(args.shift(), waku).map((str) => response.push(str)); @@ -131,6 +159,11 @@ export default function handleCommand( case '/connections': connections(waku).map((str) => response.push(str)); break; + case '/fleet': + fleet(args.shift(), currFleetEnv, setFleetEnv).map((str) => + response.push(str) + ); + break; default: response.push(`Unknown Command '${command}'`); } diff --git a/src/index.ts b/src/index.ts index 4e029962b1..0220140537 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,11 @@ export { getStatusFleetNodes, Environment, Protocol } from './lib/discover'; export * as utils from './lib/utils'; export { Waku } from './lib/waku'; + export { WakuMessage } from './lib/waku_message'; +export { generatePrivateKey, getPublicKey } from './lib/waku_message/version_1'; + export { ChatMessage } from './lib/chat_message'; export { diff --git a/src/lib/waku.ts b/src/lib/waku.ts index 737be510b5..45c520c2ba 100644 --- a/src/lib/waku.ts +++ b/src/lib/waku.ts @@ -40,7 +40,7 @@ export interface CreateOptions { * Set keep alive frequency in seconds: Waku will send a ping request to each peer * after the set number of seconds. Set to 0 to disable the keep alive feature * - * @default 10 + * @default 0 */ keepAlive?: number; /** diff --git a/src/lib/waku_message/index.spec.ts b/src/lib/waku_message/index.spec.ts index a9f552ab81..77c2c36bff 100644 --- a/src/lib/waku_message/index.spec.ts +++ b/src/lib/waku_message/index.spec.ts @@ -62,7 +62,7 @@ describe('Waku Message: Node only', function () { const privateKey = generatePrivateKey(); - waku.relay.addDecryptionPrivateKey(privateKey); + waku.relay.addDecryptionKey(privateKey); const receivedMsgPromise: Promise = new Promise( (resolve) => { @@ -118,7 +118,7 @@ describe('Waku Message: Node only', function () { const symKey = generatePrivateKey(); - waku.relay.addDecryptionPrivateKey(symKey); + waku.relay.addDecryptionKey(symKey); const receivedMsgPromise: Promise = new Promise( (resolve) => { diff --git a/src/lib/waku_relay/index.ts b/src/lib/waku_relay/index.ts index aad4b54fe6..d14703ec9b 100644 --- a/src/lib/waku_relay/index.ts +++ b/src/lib/waku_relay/index.ts @@ -67,7 +67,7 @@ export class WakuRelay extends Gossipsub { /** * Decryption private keys to use to attempt decryption of incoming messages. */ - public decPrivateKeys: Set; + public decryptionKeys: Set; /** * observers called when receiving new message. @@ -91,7 +91,7 @@ export class WakuRelay extends Gossipsub { this.heartbeat = new RelayHeartbeat(this); this.observers = {}; - this.decPrivateKeys = new Set(); + this.decryptionKeys = new Set(); const multicodecs = [constants.RelayCodec]; @@ -124,21 +124,21 @@ export class WakuRelay extends Gossipsub { } /** - * Register a decryption private key to attempt decryption of messages of - * the given content topic. This can either be a private key for asymmetric - * encryption or a symmetric key. Waku relay will attempt to decrypt messages - * using both methods. + * Register a decryption private key or symmetric key to attempt decryption + * of messages received on the given content topic. This can either be a + * private key for asymmetric encryption or a symmetric key. Waku relay will + * attempt to decrypt messages using both methods. */ - addDecryptionPrivateKey(privateKey: Uint8Array): void { - this.decPrivateKeys.add(privateKey); + addDecryptionKey(privateKey: Uint8Array): void { + this.decryptionKeys.add(privateKey); } /** - * Delete a decryption private key to attempt decryption of messages of - * the given content topic. + * Delete a decryption key to attempt decryption of messages received on the + * given content topic. */ - deleteDecryptionPrivateKey(privateKey: Uint8Array): void { - this.decPrivateKeys.delete(privateKey); + deleteDecryptionKey(privateKey: Uint8Array): void { + this.decryptionKeys.delete(privateKey); } /** @@ -210,7 +210,7 @@ export class WakuRelay extends Gossipsub { subscribe(pubsubTopic: string): void { this.on(pubsubTopic, (event) => { dbg(`Message received on ${pubsubTopic}`); - WakuMessage.decode(event.data, Array.from(this.decPrivateKeys)) + WakuMessage.decode(event.data, Array.from(this.decryptionKeys)) .then((wakuMsg) => { if (!wakuMsg) { dbg('Failed to decode Waku Message');