mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-07 00:03:07 +00:00
Merge pull request #231 from status-im/179-symmetric-encryption-store-relay
This commit is contained in:
commit
6bd93eef55
@ -19,10 +19,21 @@ module.exports = function (config) {
|
|||||||
bundlerOptions: {
|
bundlerOptions: {
|
||||||
entrypoints: /.*\.browser\.spec\.ts$/,
|
entrypoints: /.*\.browser\.spec\.ts$/,
|
||||||
},
|
},
|
||||||
tsconfig: './tsconfig.karma.json',
|
|
||||||
coverageOptions: {
|
coverageOptions: {
|
||||||
instrumentation: false,
|
instrumentation: false,
|
||||||
},
|
},
|
||||||
|
tsconfig: './tsconfig.json',
|
||||||
|
compilerOptions: {
|
||||||
|
noEmit: false,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
mode: 'replace',
|
||||||
|
values: ['src/lib/**/*.ts', 'src/proto/**/*.ts'],
|
||||||
|
},
|
||||||
|
exclude: {
|
||||||
|
mode: 'replace',
|
||||||
|
values: ['node_modules/**'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -105,5 +105,59 @@ describe('Waku Message: Node only', function () {
|
|||||||
expect(msgs[0].contentTopic).to.equal(message.contentTopic);
|
expect(msgs[0].contentTopic).to.equal(message.contentTopic);
|
||||||
expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText);
|
expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('JS decrypts nim message [symmetric, no signature]', async function () {
|
||||||
|
this.timeout(10000);
|
||||||
|
await delay(200);
|
||||||
|
|
||||||
|
const messageText = 'Here is a message encrypted in a symmetric manner.';
|
||||||
|
const message: WakuRelayMessage = {
|
||||||
|
contentTopic: DefaultContentTopic,
|
||||||
|
payload: Buffer.from(messageText, 'utf-8').toString('hex'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const symKey = generatePrivateKey();
|
||||||
|
|
||||||
|
waku.relay.addDecryptionPrivateKey(symKey);
|
||||||
|
|
||||||
|
const receivedMsgPromise: Promise<WakuMessage> = new Promise(
|
||||||
|
(resolve) => {
|
||||||
|
waku.relay.addObserver(resolve);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
dbg('Post message');
|
||||||
|
await nimWaku.postSymmetricMessage(message, symKey);
|
||||||
|
|
||||||
|
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 [symmetric, no signature]', async function () {
|
||||||
|
this.timeout(5000);
|
||||||
|
|
||||||
|
const symKey = await nimWaku.getSymmetricKey();
|
||||||
|
|
||||||
|
const messageText =
|
||||||
|
'This is a message I am going to encrypt with a symmetric key';
|
||||||
|
const message = await WakuMessage.fromUtf8String(messageText, {
|
||||||
|
symKey: symKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waku.relay.send(message);
|
||||||
|
|
||||||
|
let msgs: WakuRelayMessage[] = [];
|
||||||
|
|
||||||
|
while (msgs.length === 0) {
|
||||||
|
await delay(200);
|
||||||
|
msgs = await nimWaku.getSymmetricMessages(symKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(msgs[0].contentTopic).to.equal(message.contentTopic);
|
||||||
|
expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -117,25 +117,33 @@ export class WakuMessage {
|
|||||||
/**
|
/**
|
||||||
* Decode a byte array into Waku Message.
|
* Decode a byte array into Waku Message.
|
||||||
*
|
*
|
||||||
* If the payload is encrypted, then `decPrivateKey` is used for decryption.
|
* @params bytes The message encoded using protobuf as defined in [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/).
|
||||||
|
* @params decryptionKeys If the payload is encrypted (version = 1), then the
|
||||||
|
* keys are used to attempt decryption of the message. The passed key can either
|
||||||
|
* be asymmetric private keys or symmetric keys, both method are tried for each
|
||||||
|
* key until the message is decrypted or combinations are ran out.
|
||||||
*/
|
*/
|
||||||
static async decode(
|
static async decode(
|
||||||
bytes: Uint8Array,
|
bytes: Uint8Array,
|
||||||
decPrivateKeys?: Uint8Array[]
|
decryptionKeys?: Uint8Array[]
|
||||||
): Promise<WakuMessage | undefined> {
|
): Promise<WakuMessage | undefined> {
|
||||||
const protoBuf = proto.WakuMessage.decode(Reader.create(bytes));
|
const protoBuf = proto.WakuMessage.decode(Reader.create(bytes));
|
||||||
|
|
||||||
return WakuMessage.decodeProto(protoBuf, decPrivateKeys);
|
return WakuMessage.decodeProto(protoBuf, decryptionKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a Waku Message Protobuf Object into Waku Message.
|
* Decode and decrypt Waku Message Protobuf Object into Waku Message.
|
||||||
*
|
*
|
||||||
* If the payload is encrypted, then `decPrivateKey` is used for decryption.
|
* @params protoBuf The message to decode and decrypt.
|
||||||
|
* @params decryptionKeys If the payload is encrypted (version = 1), then the
|
||||||
|
* keys are used to attempt decryption of the message. The passed key can either
|
||||||
|
* be asymmetric private keys or symmetric keys, both method are tried for each
|
||||||
|
* key until the message is decrypted or combinations are ran out.
|
||||||
*/
|
*/
|
||||||
static async decodeProto(
|
static async decodeProto(
|
||||||
protoBuf: proto.WakuMessage,
|
protoBuf: proto.WakuMessage,
|
||||||
decPrivateKeys?: Uint8Array[]
|
decryptionKeys?: Uint8Array[]
|
||||||
): Promise<WakuMessage | undefined> {
|
): Promise<WakuMessage | undefined> {
|
||||||
if (protoBuf.payload === undefined) {
|
if (protoBuf.payload === undefined) {
|
||||||
dbg('Payload is undefined');
|
dbg('Payload is undefined');
|
||||||
@ -146,14 +154,14 @@ export class WakuMessage {
|
|||||||
let signaturePublicKey;
|
let signaturePublicKey;
|
||||||
let signature;
|
let signature;
|
||||||
if (protoBuf.version === 1 && protoBuf.payload) {
|
if (protoBuf.version === 1 && protoBuf.payload) {
|
||||||
if (decPrivateKeys === undefined) {
|
if (decryptionKeys === undefined) {
|
||||||
dbg('Payload is encrypted but no private keys have been provided.');
|
dbg('Payload is encrypted but no private keys have been provided.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a bunch of `undefined` and hopefully one decrypted result
|
// Returns a bunch of `undefined` and hopefully one decrypted result
|
||||||
const allResults = await Promise.all(
|
const allResults = await Promise.all(
|
||||||
decPrivateKeys.map(async (privateKey) => {
|
decryptionKeys.map(async (privateKey) => {
|
||||||
try {
|
try {
|
||||||
return await version_1.decryptSymmetric(payload, privateKey);
|
return await version_1.decryptSymmetric(payload, privateKey);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -172,14 +172,19 @@ export async function decryptSymmetric(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a new private key
|
* Generate a new key. Can be used as a private key for Asymmetric encryption
|
||||||
|
* or a key for symmetric encryption.
|
||||||
|
*
|
||||||
|
* If using Asymmetric encryption, use {@link getPublicKey} to get the
|
||||||
|
* corresponding Public Key.
|
||||||
*/
|
*/
|
||||||
export function generatePrivateKey(): Uint8Array {
|
export function generatePrivateKey(): Uint8Array {
|
||||||
return randomBytes(32);
|
return randomBytes(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the public key for the given private key
|
* Return the public key for the given private key, to be used for asymmetric
|
||||||
|
* encryption.
|
||||||
*/
|
*/
|
||||||
export function getPublicKey(privateKey: Uint8Array | Buffer): Uint8Array {
|
export function getPublicKey(privateKey: Uint8Array | Buffer): Uint8Array {
|
||||||
return secp256k1.publicKeyCreate(privateKey, false);
|
return secp256k1.publicKeyCreate(privateKey, false);
|
||||||
@ -217,14 +222,9 @@ function validateDataIntegrity(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
return !(
|
||||||
expectedSize > 3 &&
|
expectedSize > 3 && Buffer.from(value).equals(Buffer.alloc(value.length))
|
||||||
Buffer.from(value).equals(Buffer.alloc(value.length))
|
);
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSignature(message: Buffer): Buffer {
|
function getSignature(message: Buffer): Buffer {
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export interface GossipOptions {
|
|||||||
* Implements the [Waku v2 Relay protocol]{@link https://rfc.vac.dev/spec/11/}.
|
* Implements the [Waku v2 Relay protocol]{@link https://rfc.vac.dev/spec/11/}.
|
||||||
* Must be passed as a `pubsub` module to a {Libp2p} instance.
|
* Must be passed as a `pubsub` module to a {Libp2p} instance.
|
||||||
*
|
*
|
||||||
* @implements {Pubsub}
|
* @implements {require('libp2p-interfaces/src/pubsub')}
|
||||||
* @noInheritDoc
|
* @noInheritDoc
|
||||||
*/
|
*/
|
||||||
export class WakuRelay extends Gossipsub {
|
export class WakuRelay extends Gossipsub {
|
||||||
@ -125,7 +125,9 @@ export class WakuRelay extends Gossipsub {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a decryption private key to attempt decryption of messages of
|
* Register a decryption private key to attempt decryption of messages of
|
||||||
* the given content topic.
|
* 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 {
|
addDecryptionPrivateKey(privateKey: Uint8Array): void {
|
||||||
this.decPrivateKeys.add(privateKey);
|
this.decPrivateKeys.add(privateKey);
|
||||||
@ -145,7 +147,6 @@ export class WakuRelay extends Gossipsub {
|
|||||||
* @param callback called when a new message is received via waku relay
|
* @param callback called when a new message is received via waku relay
|
||||||
* @param contentTopics Content Topics for which the callback with be called,
|
* @param contentTopics Content Topics for which the callback with be called,
|
||||||
* all of them if undefined, [] or ["",..] is passed.
|
* all of them if undefined, [] or ["",..] is passed.
|
||||||
* @param decPrivateKeys Private keys used to decrypt incoming Waku Messages.
|
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
addObserver(
|
addObserver(
|
||||||
|
|||||||
@ -146,32 +146,42 @@ describe('Waku Store', () => {
|
|||||||
expect(result).to.not.eq(-1);
|
expect(result).to.not.eq(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Retrieves history with asymmetric encrypted messages', async function () {
|
it('Retrieves history with asymmetric & symmetric encrypted messages', async function () {
|
||||||
this.timeout(10_000);
|
this.timeout(10_000);
|
||||||
|
|
||||||
nimWaku = new NimWaku(makeLogFileName(this));
|
nimWaku = new NimWaku(makeLogFileName(this));
|
||||||
await nimWaku.start({ persistMessages: true, lightpush: true });
|
await nimWaku.start({ persistMessages: true, lightpush: true });
|
||||||
|
|
||||||
const encryptedMessageText = 'This message is encrypted for me';
|
const encryptedAsymmetricMessageText =
|
||||||
|
'This message is encrypted for me using asymmetric';
|
||||||
|
const encryptedSymmetricMessageText =
|
||||||
|
'This message is encrypted for me using symmetric encryption';
|
||||||
const clearMessageText =
|
const clearMessageText =
|
||||||
'This is a clear text message for everyone to read';
|
'This is a clear text message for everyone to read';
|
||||||
const otherEncMessageText =
|
const otherEncMessageText =
|
||||||
'This message is not for and I must not be able to read it';
|
'This message is not for and I must not be able to read it';
|
||||||
|
|
||||||
const privateKey = generatePrivateKey();
|
const privateKey = generatePrivateKey();
|
||||||
|
const symKey = generatePrivateKey();
|
||||||
const publicKey = getPublicKey(privateKey);
|
const publicKey = getPublicKey(privateKey);
|
||||||
|
|
||||||
const [encryptedMessage, clearMessage, otherEncMessage] = await Promise.all(
|
const [
|
||||||
[
|
encryptedAsymmetricMessage,
|
||||||
WakuMessage.fromUtf8String(encryptedMessageText, {
|
encryptedSymmetricMessage,
|
||||||
encPublicKey: publicKey,
|
clearMessage,
|
||||||
}),
|
otherEncMessage,
|
||||||
WakuMessage.fromUtf8String(clearMessageText),
|
] = await Promise.all([
|
||||||
WakuMessage.fromUtf8String(otherEncMessageText, {
|
WakuMessage.fromUtf8String(encryptedAsymmetricMessageText, {
|
||||||
encPublicKey: getPublicKey(generatePrivateKey()),
|
encPublicKey: publicKey,
|
||||||
}),
|
}),
|
||||||
]
|
WakuMessage.fromUtf8String(encryptedSymmetricMessageText, {
|
||||||
);
|
symKey: symKey,
|
||||||
|
}),
|
||||||
|
WakuMessage.fromUtf8String(clearMessageText),
|
||||||
|
WakuMessage.fromUtf8String(otherEncMessageText, {
|
||||||
|
encPublicKey: getPublicKey(generatePrivateKey()),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
dbg('Messages have been encrypted');
|
dbg('Messages have been encrypted');
|
||||||
|
|
||||||
@ -204,7 +214,8 @@ describe('Waku Store', () => {
|
|||||||
|
|
||||||
dbg('Sending messages using light push');
|
dbg('Sending messages using light push');
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
await waku1.lightPush.push(encryptedMessage),
|
waku1.lightPush.push(encryptedAsymmetricMessage),
|
||||||
|
waku1.lightPush.push(encryptedSymmetricMessage),
|
||||||
waku1.lightPush.push(otherEncMessage),
|
waku1.lightPush.push(otherEncMessage),
|
||||||
waku1.lightPush.push(clearMessage),
|
waku1.lightPush.push(clearMessage),
|
||||||
]);
|
]);
|
||||||
@ -218,13 +229,14 @@ describe('Waku Store', () => {
|
|||||||
dbg('Retrieve messages from store');
|
dbg('Retrieve messages from store');
|
||||||
const messages = await waku2.store.queryHistory({
|
const messages = await waku2.store.queryHistory({
|
||||||
contentTopics: [],
|
contentTopics: [],
|
||||||
decryptionPrivateKeys: [privateKey],
|
decryptionKeys: [privateKey, symKey],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(messages?.length).eq(2);
|
expect(messages?.length).eq(3);
|
||||||
if (!messages) throw 'Length was tested';
|
if (!messages) throw 'Length was tested';
|
||||||
expect(messages[0].payloadAsUtf8).to.eq(clearMessageText);
|
expect(messages[0].payloadAsUtf8).to.eq(clearMessageText);
|
||||||
expect(messages[1].payloadAsUtf8).to.eq(encryptedMessageText);
|
expect(messages[1].payloadAsUtf8).to.eq(encryptedSymmetricMessageText);
|
||||||
|
expect(messages[2].payloadAsUtf8).to.eq(encryptedAsymmetricMessageText);
|
||||||
|
|
||||||
await Promise.all([waku1.stop(), waku2.stop()]);
|
await Promise.all([waku1.stop(), waku2.stop()]);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export interface QueryOptions {
|
|||||||
direction?: Direction;
|
direction?: Direction;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
callback?: (messages: WakuMessage[]) => void;
|
callback?: (messages: WakuMessage[]) => void;
|
||||||
decryptionPrivateKeys?: Uint8Array[];
|
decryptionKeys?: Uint8Array[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,6 +64,9 @@ export class WakuStore {
|
|||||||
* @param options.pubsubTopic The pubsub topic to pass to the query. Defaults
|
* @param options.pubsubTopic The pubsub topic to pass to the query. Defaults
|
||||||
* to the value set at creation. See [Waku v2 Topic Usage Recommendations](https://rfc.vac.dev/spec/23/).
|
* to the value set at creation. See [Waku v2 Topic Usage Recommendations](https://rfc.vac.dev/spec/23/).
|
||||||
* @param options.callback Callback called on page of stored messages as they are retrieved
|
* @param options.callback Callback called on page of stored messages as they are retrieved
|
||||||
|
* @param options.decryptionKeys Keys that will be used to decrypt messages.
|
||||||
|
* It can be Asymmetric Private Keys and Symmetric Keys in the same array, all keys will be tried with both
|
||||||
|
* methods.
|
||||||
* @throws If not able to reach the peer to query.
|
* @throws If not able to reach the peer to query.
|
||||||
*/
|
*/
|
||||||
async queryHistory(options: QueryOptions): Promise<WakuMessage[] | null> {
|
async queryHistory(options: QueryOptions): Promise<WakuMessage[] | null> {
|
||||||
@ -129,7 +132,7 @@ export class WakuStore {
|
|||||||
response.messages.map(async (protoMsg) => {
|
response.messages.map(async (protoMsg) => {
|
||||||
const msg = await WakuMessage.decodeProto(
|
const msg = await WakuMessage.decodeProto(
|
||||||
protoMsg,
|
protoMsg,
|
||||||
opts.decryptionPrivateKeys
|
opts.decryptionKeys
|
||||||
);
|
);
|
||||||
|
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import debug from 'debug';
|
|||||||
import { Multiaddr, multiaddr } from 'multiaddr';
|
import { Multiaddr, multiaddr } from 'multiaddr';
|
||||||
import PeerId from 'peer-id';
|
import PeerId from 'peer-id';
|
||||||
|
|
||||||
|
import { hexToBuf } from '../lib/utils';
|
||||||
import { WakuMessage } from '../lib/waku_message';
|
import { WakuMessage } from '../lib/waku_message';
|
||||||
import { DefaultPubsubTopic } from '../lib/waku_relay';
|
import { DefaultPubsubTopic } from '../lib/waku_relay';
|
||||||
import * as proto from '../proto/waku/v2/message';
|
import * as proto from '../proto/waku/v2/message';
|
||||||
@ -251,6 +252,45 @@ export class NimWaku {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSymmetricKey(): Promise<Buffer> {
|
||||||
|
this.checkProcess();
|
||||||
|
|
||||||
|
return this.rpcCall<string>(
|
||||||
|
'get_waku_v2_private_v1_symmetric_key',
|
||||||
|
[]
|
||||||
|
).then(hexToBuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
async postSymmetricMessage(
|
||||||
|
message: WakuRelayMessage,
|
||||||
|
symKey: Uint8Array,
|
||||||
|
pubsubTopic?: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
this.checkProcess();
|
||||||
|
|
||||||
|
if (!message.payload) {
|
||||||
|
throw 'Attempting to send empty message';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.rpcCall<boolean>('post_waku_v2_private_v1_symmetric_message', [
|
||||||
|
pubsubTopic ? pubsubTopic : DefaultPubsubTopic,
|
||||||
|
message,
|
||||||
|
'0x' + bufToHex(symKey),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSymmetricMessages(
|
||||||
|
symKey: Uint8Array,
|
||||||
|
pubsubTopic?: string
|
||||||
|
): Promise<WakuRelayMessage[]> {
|
||||||
|
this.checkProcess();
|
||||||
|
|
||||||
|
return await this.rpcCall<WakuRelayMessage[]>(
|
||||||
|
'get_waku_v2_private_v1_symmetric_messages',
|
||||||
|
[pubsubTopic ? pubsubTopic : DefaultPubsubTopic, '0x' + bufToHex(symKey)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getPeerId(): Promise<PeerId> {
|
async getPeerId(): Promise<PeerId> {
|
||||||
return await this.setPeerId().then((res) => res.peerId);
|
return await this.setPeerId().then((res) => res.peerId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig",
|
|
||||||
"compilerOptions": {
|
|
||||||
"noEmit": false
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/lib/**/*.ts",
|
|
||||||
"src/proto/**/*.ts",
|
|
||||||
"src/tests/browser/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": ["node_modules/**"]
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user