mirror of https://github.com/waku-org/js-waku.git
Merge pull request #229 from status-im/179-symmetric-encryption
This commit is contained in:
commit
2715fe0e8c
|
@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- New `peers` and `randomPeer` methods on `WakuStore` and `WakuLightPush` to have a better idea of available peers;
|
- New `peers` and `randomPeer` methods on `WakuStore` and `WakuLightPush` to have a better idea of available peers;
|
||||||
Note that it does not check whether Waku node is currently connected to said peers.
|
Note that it does not check whether Waku node is currently connected to said peers.
|
||||||
- Enable passing decryption private keys to `WakuStore.queryHistory`.
|
- Enable passing decryption private keys to `WakuStore.queryHistory`.
|
||||||
|
- Test: Introduce testing in browser environment (Chrome) using Karma.
|
||||||
|
- Add support for Waku Message version 1: Asymmetric encryption, symmetric encryption, and signature of the data.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **Breaking**: Auto select peer if none provided for store and light push protocols.
|
- **Breaking**: Auto select peer if none provided for store and light push protocols.
|
||||||
|
|
|
@ -19,9 +19,10 @@ To help ensure your PR passes, just run before committing:
|
||||||
|
|
||||||
To build and test this repository, you need:
|
To build and test this repository, you need:
|
||||||
|
|
||||||
- [Node.js & npm](https://nodejs.org/en/)
|
- [Node.js & npm](https://nodejs.org/en/).
|
||||||
- [bufbuild](https://github.com/bufbuild/buf) (only if changing protobuf files)
|
- [bufbuild](https://github.com/bufbuild/buf) (only if changing protobuf files).
|
||||||
- [protoc](https://grpc.io/docs/protoc-installation/) (only if changing protobuf files)
|
- [protoc](https://grpc.io/docs/protoc-installation/) (only if changing protobuf files).
|
||||||
|
- Chrome (for browser testing).
|
||||||
|
|
||||||
To ensure interoperability with [nim-waku](https://github.com/status-im/nim-waku/), some tests are run against a nim-waku node.
|
To ensure interoperability with [nim-waku](https://github.com/status-im/nim-waku/), some tests are run against a nim-waku node.
|
||||||
This is why `nim-waku` is present as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules), which itself contain several submodules.
|
This is why `nim-waku` is present as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules), which itself contain several submodules.
|
||||||
|
@ -29,10 +30,17 @@ At this stage, it is not possible to exclude nim-waku tests, hence `git submodul
|
||||||
|
|
||||||
To build nim-waku, you also need [Rust](https://www.rust-lang.org/tools/install).
|
To build nim-waku, you also need [Rust](https://www.rust-lang.org/tools/install).
|
||||||
|
|
||||||
|
Note that we run tests in both NodeJS and browser environments (both Chrome and Firefox, using [karma](https://karma-runner.github.io/)).
|
||||||
|
Files named `*.spec.ts` are only run in NodeJS environment;
|
||||||
|
Files named `*.browser.spec.ts` are run in both NodeJS and browser environment.
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
|
||||||
- Please follow [Chris Beam's commit message guide](https://chris.beams.io/posts/git-commit/),
|
- Please follow [Chris Beam's commit message guide](https://chris.beams.io/posts/git-commit/) for commit patches,
|
||||||
- Usually best to test new code,
|
- Please test new code, we use [mocha](https://mochajs.org/),
|
||||||
|
[chai](https://www.chaijs.com/),
|
||||||
|
[fast-check](https://github.com/dubzzz/fast-check)
|
||||||
|
and [karma](https://karma-runner.github.io/).
|
||||||
|
|
||||||
### Committing Patches
|
### Committing Patches
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,7 @@ process.env.CHROME_BIN = require('puppeteer').executablePath();
|
||||||
module.exports = function (config) {
|
module.exports = function (config) {
|
||||||
config.set({
|
config.set({
|
||||||
frameworks: ['mocha', 'karma-typescript'],
|
frameworks: ['mocha', 'karma-typescript'],
|
||||||
files: [
|
files: ['src/lib/**/*.ts', 'src/proto/**/*.ts'],
|
||||||
'src/lib/**/*.ts',
|
|
||||||
'src/proto/**/*.ts',
|
|
||||||
'src/tests/browser/*.spec.ts',
|
|
||||||
],
|
|
||||||
preprocessors: {
|
preprocessors: {
|
||||||
'**/*.ts': ['karma-typescript'],
|
'**/*.ts': ['karma-typescript'],
|
||||||
},
|
},
|
||||||
|
@ -21,7 +17,7 @@ module.exports = function (config) {
|
||||||
singleRun: true,
|
singleRun: true,
|
||||||
karmaTypescriptConfig: {
|
karmaTypescriptConfig: {
|
||||||
bundlerOptions: {
|
bundlerOptions: {
|
||||||
entrypoints: /src\/tests\/browser\/.*\.spec\.ts$/,
|
entrypoints: /.*\.browser\.spec\.ts$/,
|
||||||
},
|
},
|
||||||
tsconfig: './tsconfig.karma.json',
|
tsconfig: './tsconfig.karma.json',
|
||||||
coverageOptions: {
|
coverageOptions: {
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import fc from 'fast-check';
|
||||||
|
|
||||||
|
import { WakuMessage } from '../../lib/waku_message';
|
||||||
|
import { getPublicKey } from '../../lib/waku_message/version_1';
|
||||||
|
|
||||||
|
describe('Waku Message: Browser & Node', 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);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Waku message round trip binary encryption [symmetric, no signature]', async function () {
|
||||||
|
await fc.assert(
|
||||||
|
fc.asyncProperty(
|
||||||
|
fc.uint8Array({ minLength: 1 }),
|
||||||
|
fc.uint8Array({ minLength: 32, maxLength: 32 }),
|
||||||
|
async (payload, key) => {
|
||||||
|
const msg = await WakuMessage.fromBytes(payload, {
|
||||||
|
symKey: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wireBytes = msg.encode();
|
||||||
|
const actual = await WakuMessage.decode(wireBytes, [key]);
|
||||||
|
|
||||||
|
expect(actual?.payload).to.deep.equal(payload);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Waku message round trip binary encryption [symmetric, 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, symKey) => {
|
||||||
|
const sigPubKey = getPublicKey(sigPrivKey);
|
||||||
|
|
||||||
|
const msg = await WakuMessage.fromBytes(payload, {
|
||||||
|
symKey: symKey,
|
||||||
|
sigPrivKey: sigPrivKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wireBytes = msg.encode();
|
||||||
|
const actual = await WakuMessage.decode(wireBytes, [symKey]);
|
||||||
|
|
||||||
|
expect(actual?.payload).to.deep.equal(payload);
|
||||||
|
expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,5 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import fc from 'fast-check';
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore: No types available
|
// @ts-ignore: No types available
|
||||||
import TCP from 'libp2p-tcp';
|
import TCP from 'libp2p-tcp';
|
||||||
|
@ -21,77 +20,7 @@ import { DefaultContentTopic, WakuMessage } from './index';
|
||||||
|
|
||||||
const dbg = debug('waku:test:message');
|
const dbg = debug('waku:test:message');
|
||||||
|
|
||||||
describe('Waku Message', function () {
|
describe('Waku Message: Node only', 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('Interop: Nim', function () {
|
describe('Interop: Nim', function () {
|
||||||
let waku: Waku;
|
let waku: Waku;
|
||||||
let nimWaku: NimWaku;
|
let nimWaku: NimWaku;
|
||||||
|
@ -135,9 +64,11 @@ describe('Interop: Nim', function () {
|
||||||
|
|
||||||
waku.relay.addDecryptionPrivateKey(privateKey);
|
waku.relay.addDecryptionPrivateKey(privateKey);
|
||||||
|
|
||||||
const receivedMsgPromise: Promise<WakuMessage> = new Promise((resolve) => {
|
const receivedMsgPromise: Promise<WakuMessage> = new Promise(
|
||||||
|
(resolve) => {
|
||||||
waku.relay.addObserver(resolve);
|
waku.relay.addObserver(resolve);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const publicKey = getPublicKey(privateKey);
|
const publicKey = getPublicKey(privateKey);
|
||||||
dbg('Post message');
|
dbg('Post message');
|
||||||
|
@ -175,3 +106,4 @@ describe('Interop: Nim', function () {
|
||||||
expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText);
|
expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -14,9 +14,31 @@ const DefaultVersion = 0;
|
||||||
const dbg = debug('waku:message');
|
const dbg = debug('waku:message');
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
|
/**
|
||||||
|
* Content topic to set on the message, defaults to {@link DefaultContentTopic}
|
||||||
|
* if not passed.
|
||||||
|
*/
|
||||||
contentTopic?: string;
|
contentTopic?: string;
|
||||||
|
/**
|
||||||
|
* Timestamp to set on the message, defaults to now if not passed.
|
||||||
|
*/
|
||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
|
/**
|
||||||
|
* Public Key to use to encrypt the messages using ECIES (Asymmetric Encryption).
|
||||||
|
*
|
||||||
|
* @throws if both `encPublicKey` and `symKey` are passed
|
||||||
|
*/
|
||||||
encPublicKey?: Uint8Array | string;
|
encPublicKey?: Uint8Array | string;
|
||||||
|
/**
|
||||||
|
* Key to use to encrypt the messages using AES (Symmetric Encryption).
|
||||||
|
*
|
||||||
|
* @throws if both `encPublicKey` and `symKey` are passed
|
||||||
|
*/
|
||||||
|
symKey?: Uint8Array | string;
|
||||||
|
/**
|
||||||
|
* Private key to use to sign the message, either `encPublicKey` or `symKey` must be provided as only
|
||||||
|
* encrypted messages are signed.
|
||||||
|
*/
|
||||||
sigPrivKey?: Uint8Array;
|
sigPrivKey?: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,12 +69,15 @@ export class WakuMessage {
|
||||||
*
|
*
|
||||||
* If `opts.sigPrivKey` is passed and version 1 is used, the payload is signed
|
* If `opts.sigPrivKey` is passed and version 1 is used, the payload is signed
|
||||||
* before encryption.
|
* before encryption.
|
||||||
|
*
|
||||||
|
* @throws if both `opts.encPublicKey` and `opt.symKey` are passed
|
||||||
*/
|
*/
|
||||||
static async fromBytes(
|
static async fromBytes(
|
||||||
payload: Uint8Array,
|
payload: Uint8Array,
|
||||||
opts?: Options
|
opts?: Options
|
||||||
): Promise<WakuMessage> {
|
): Promise<WakuMessage> {
|
||||||
const { timestamp, contentTopic, encPublicKey, sigPrivKey } = Object.assign(
|
const { timestamp, contentTopic, encPublicKey, symKey, sigPrivKey } =
|
||||||
|
Object.assign(
|
||||||
{ timestamp: new Date(), contentTopic: DefaultContentTopic },
|
{ timestamp: new Date(), contentTopic: DefaultContentTopic },
|
||||||
opts ? opts : {}
|
opts ? opts : {}
|
||||||
);
|
);
|
||||||
|
@ -60,11 +85,21 @@ export class WakuMessage {
|
||||||
let _payload = payload;
|
let _payload = payload;
|
||||||
let version = DefaultVersion;
|
let version = DefaultVersion;
|
||||||
let sig;
|
let sig;
|
||||||
|
|
||||||
|
if (encPublicKey && symKey) {
|
||||||
|
throw 'Pass either `encPublicKey` or `symKey`, not both.';
|
||||||
|
}
|
||||||
|
|
||||||
if (encPublicKey) {
|
if (encPublicKey) {
|
||||||
const enc = version_1.clearEncode(_payload, sigPrivKey);
|
const enc = version_1.clearEncode(_payload, sigPrivKey);
|
||||||
_payload = await version_1.encryptAsymmetric(enc.payload, encPublicKey);
|
_payload = await version_1.encryptAsymmetric(enc.payload, encPublicKey);
|
||||||
sig = enc.sig;
|
sig = enc.sig;
|
||||||
version = 1;
|
version = 1;
|
||||||
|
} else if (symKey) {
|
||||||
|
const enc = version_1.clearEncode(_payload, sigPrivKey);
|
||||||
|
_payload = await version_1.encryptSymmetric(enc.payload, symKey);
|
||||||
|
sig = enc.sig;
|
||||||
|
version = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WakuMessage(
|
return new WakuMessage(
|
||||||
|
@ -113,19 +148,23 @@ export class WakuMessage {
|
||||||
if (protoBuf.version === 1 && protoBuf.payload) {
|
if (protoBuf.version === 1 && protoBuf.payload) {
|
||||||
if (decPrivateKeys === undefined) {
|
if (decPrivateKeys === 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) => {
|
decPrivateKeys.map(async (privateKey) => {
|
||||||
|
try {
|
||||||
|
return await version_1.decryptSymmetric(payload, privateKey);
|
||||||
|
} catch (e) {
|
||||||
|
dbg('Failed to decrypt message using symmetric encryption', e);
|
||||||
try {
|
try {
|
||||||
return await version_1.decryptAsymmetric(payload, privateKey);
|
return await version_1.decryptAsymmetric(payload, privateKey);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dbg('Failed to decrypt asymmetric message', e);
|
dbg('Failed to decrypt message using asymmetric encryption', e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { IvSize, SymmetricKeySize } from './index';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
msCrypto?: Crypto;
|
||||||
|
}
|
||||||
|
interface Crypto {
|
||||||
|
webkitSubtle?: SubtleCrypto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const crypto = window.crypto || window.msCrypto;
|
||||||
|
const subtle: SubtleCrypto = crypto.subtle || crypto.webkitSubtle;
|
||||||
|
|
||||||
|
const Algorithm = { name: 'AES-GCM', length: 128 };
|
||||||
|
|
||||||
|
if (subtle === undefined) {
|
||||||
|
throw new Error('Failed to load Subtle CryptoAPI');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function encrypt(
|
||||||
|
iv: Buffer | Uint8Array,
|
||||||
|
key: Buffer,
|
||||||
|
clearText: Buffer
|
||||||
|
): Promise<Buffer> {
|
||||||
|
return subtle
|
||||||
|
.importKey('raw', key, Algorithm, false, ['encrypt'])
|
||||||
|
.then((cryptoKey) =>
|
||||||
|
subtle.encrypt({ iv, ...Algorithm }, cryptoKey, clearText)
|
||||||
|
)
|
||||||
|
.then(Buffer.from);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decrypt(
|
||||||
|
iv: Buffer,
|
||||||
|
key: Buffer,
|
||||||
|
cipherText: Buffer
|
||||||
|
): Promise<Buffer> {
|
||||||
|
return subtle
|
||||||
|
.importKey('raw', key, Algorithm, false, ['decrypt'])
|
||||||
|
.then((cryptoKey) =>
|
||||||
|
subtle.decrypt({ iv, ...Algorithm }, cryptoKey, cipherText)
|
||||||
|
)
|
||||||
|
.then(Buffer.from);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateKeyForSymmetricEnc(): Buffer {
|
||||||
|
return crypto.getRandomValues(Buffer.alloc(SymmetricKeySize));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateIv(): Uint8Array {
|
||||||
|
const iv = new Uint8Array(IvSize);
|
||||||
|
crypto.getRandomValues(iv);
|
||||||
|
return iv;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
export const SymmetricKeySize = 32;
|
||||||
|
export const IvSize = 12;
|
||||||
|
export const TagSize = 16;
|
||||||
|
|
||||||
|
export interface Symmetric {
|
||||||
|
/**
|
||||||
|
* Proceed with symmetric encryption of `clearText` value.
|
||||||
|
*/
|
||||||
|
encrypt: (
|
||||||
|
iv: Buffer | Uint8Array,
|
||||||
|
key: Buffer,
|
||||||
|
clearText: Buffer
|
||||||
|
) => Promise<Buffer>;
|
||||||
|
/**
|
||||||
|
* Proceed with symmetric decryption of `cipherText` value.
|
||||||
|
*/
|
||||||
|
decrypt: (iv: Buffer, key: Buffer, cipherText: Buffer) => Promise<Buffer>;
|
||||||
|
/**
|
||||||
|
* Generate a new private key for Symmetric encryption purposes.
|
||||||
|
*/
|
||||||
|
generateKeyForSymmetricEnc: () => Buffer;
|
||||||
|
/**
|
||||||
|
* Generate an Initialization Vector (iv) for for Symmetric encryption purposes.
|
||||||
|
*/
|
||||||
|
generateIv: () => Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let symmetric: Symmetric = {} as unknown as Symmetric;
|
||||||
|
|
||||||
|
import('./browser')
|
||||||
|
.then((mod) => {
|
||||||
|
symmetric = mod;
|
||||||
|
})
|
||||||
|
.catch((eBrowser) => {
|
||||||
|
import('./node')
|
||||||
|
.then((mod) => {
|
||||||
|
symmetric = mod;
|
||||||
|
})
|
||||||
|
.catch((eNode) => {
|
||||||
|
throw `Could not load any symmetric crypto modules: ${eBrowser}, ${eNode}`;
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
||||||
|
|
||||||
|
import { IvSize, SymmetricKeySize, TagSize } from './index';
|
||||||
|
|
||||||
|
const Algorithm = 'aes-256-gcm';
|
||||||
|
|
||||||
|
export async function encrypt(
|
||||||
|
iv: Buffer | Uint8Array,
|
||||||
|
key: Buffer,
|
||||||
|
clearText: Buffer
|
||||||
|
): Promise<Buffer> {
|
||||||
|
const cipher = createCipheriv(Algorithm, key, iv);
|
||||||
|
const a = cipher.update(clearText);
|
||||||
|
const b = cipher.final();
|
||||||
|
const tag = cipher.getAuthTag();
|
||||||
|
return Buffer.concat([a, b, tag]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decrypt(
|
||||||
|
iv: Buffer,
|
||||||
|
key: Buffer,
|
||||||
|
data: Buffer
|
||||||
|
): Promise<Buffer> {
|
||||||
|
const tagStart = data.length - TagSize;
|
||||||
|
const cipherText = data.slice(0, tagStart);
|
||||||
|
const tag = data.slice(tagStart);
|
||||||
|
const decipher = createDecipheriv(Algorithm, key, iv);
|
||||||
|
decipher.setAuthTag(tag);
|
||||||
|
const a = decipher.update(cipherText);
|
||||||
|
const b = decipher.final();
|
||||||
|
return Buffer.concat([a, b]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateKeyForSymmetricEnc(): Buffer {
|
||||||
|
return randomBytes(SymmetricKeySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateIv(): Buffer {
|
||||||
|
return randomBytes(IvSize);
|
||||||
|
}
|
|
@ -5,7 +5,9 @@ import {
|
||||||
clearDecode,
|
clearDecode,
|
||||||
clearEncode,
|
clearEncode,
|
||||||
decryptAsymmetric,
|
decryptAsymmetric,
|
||||||
|
decryptSymmetric,
|
||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
|
encryptSymmetric,
|
||||||
getPublicKey,
|
getPublicKey,
|
||||||
} from './version_1';
|
} from './version_1';
|
||||||
|
|
||||||
|
@ -54,4 +56,19 @@ describe('Waku Message Version 1', function () {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Symmetric encrypt & Decrypt', async function () {
|
||||||
|
await fc.assert(
|
||||||
|
fc.asyncProperty(
|
||||||
|
fc.uint8Array(),
|
||||||
|
fc.uint8Array({ minLength: 32, maxLength: 32 }),
|
||||||
|
async (message, key) => {
|
||||||
|
const enc = await encryptSymmetric(message, key);
|
||||||
|
const res = await decryptSymmetric(enc, key);
|
||||||
|
|
||||||
|
expect(res).deep.equal(message);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
|
@ -7,6 +7,8 @@ import * as secp256k1 from 'secp256k1';
|
||||||
|
|
||||||
import { hexToBuf } from '../utils';
|
import { hexToBuf } from '../utils';
|
||||||
|
|
||||||
|
import { IvSize, symmetric } from './symmetric';
|
||||||
|
|
||||||
const FlagsLength = 1;
|
const FlagsLength = 1;
|
||||||
const FlagMask = 3; // 0011
|
const FlagMask = 3; // 0011
|
||||||
const IsSignedMask = 4; // 0100
|
const IsSignedMask = 4; // 0100
|
||||||
|
@ -102,7 +104,7 @@ export function clearDecode(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proceed with Asymmetric encryption of the data as per [26/WAKU-PAYLOAD](rfc.vac.dev/spec/26/).
|
* Proceed with Asymmetric encryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/).
|
||||||
* The data MUST be flags | payload-length | payload | [signature].
|
* The data MUST be flags | payload-length | payload | [signature].
|
||||||
* The returned result can be set to `WakuMessage.payload`.
|
* The returned result can be set to `WakuMessage.payload`.
|
||||||
*
|
*
|
||||||
|
@ -115,6 +117,12 @@ export async function encryptAsymmetric(
|
||||||
return ecies.encrypt(hexToBuf(publicKey), Buffer.from(data));
|
return ecies.encrypt(hexToBuf(publicKey), Buffer.from(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proceed with Asymmetric decryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/).
|
||||||
|
* The return data is expect to be flags | payload-length | payload | [signature].
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
export async function decryptAsymmetric(
|
export async function decryptAsymmetric(
|
||||||
payload: Uint8Array | Buffer,
|
payload: Uint8Array | Buffer,
|
||||||
privKey: Uint8Array | Buffer
|
privKey: Uint8Array | Buffer
|
||||||
|
@ -122,6 +130,47 @@ export async function decryptAsymmetric(
|
||||||
return ecies.decrypt(Buffer.from(privKey), Buffer.from(payload));
|
return ecies.decrypt(Buffer.from(privKey), Buffer.from(payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proceed with Symmetric encryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/).
|
||||||
|
*
|
||||||
|
* @param data The data to encrypt, expected to be `flags | payload-length | payload | [signature]`.
|
||||||
|
* @param key The key to use for encryption.
|
||||||
|
* @returns The decrypted data, `cipherText | tag | iv` and can be set to `WakuMessage.payload`.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export async function encryptSymmetric(
|
||||||
|
data: Uint8Array | Buffer,
|
||||||
|
key: Uint8Array | Buffer | string
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const iv = symmetric.generateIv();
|
||||||
|
|
||||||
|
// Returns `cipher | tag`
|
||||||
|
const cipher = await symmetric.encrypt(iv, hexToBuf(key), Buffer.from(data));
|
||||||
|
return Buffer.concat([cipher, iv]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proceed with Symmetric decryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/).
|
||||||
|
*
|
||||||
|
* @param payload The cipher data, it is expected to be `cipherText | tag | iv`.
|
||||||
|
* @param key The key to use for decryption.
|
||||||
|
* @returns The decrypted data, expected to be `flags | payload-length | payload | [signature]`.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export async function decryptSymmetric(
|
||||||
|
payload: Uint8Array | Buffer,
|
||||||
|
key: Uint8Array | Buffer | string
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const data = Buffer.from(payload);
|
||||||
|
const ivStart = data.length - IvSize;
|
||||||
|
const cipher = data.slice(0, ivStart);
|
||||||
|
const iv = data.slice(ivStart);
|
||||||
|
|
||||||
|
return symmetric.decrypt(iv, hexToBuf(key), cipher);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a new private key
|
* Generate a new private key
|
||||||
*/
|
*/
|
||||||
|
@ -137,7 +186,7 @@ export function getPublicKey(privateKey: Uint8Array | Buffer): Uint8Array {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes the flags & auxiliary-field as per [26/WAKU-PAYLOAD](rfc.vac.dev/spec/26/).
|
* Computes the flags & auxiliary-field as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/).
|
||||||
*/
|
*/
|
||||||
function addPayloadSizeField(msg: Buffer, payload: Uint8Array): Buffer {
|
function addPayloadSizeField(msg: Buffer, payload: Uint8Array): Buffer {
|
||||||
const fieldSize = getSizeOfPayloadSizeField(payload);
|
const fieldSize = getSizeOfPayloadSizeField(payload);
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { expect } from 'chai';
|
|
||||||
import fc from 'fast-check';
|
|
||||||
|
|
||||||
import {
|
|
||||||
clearDecode,
|
|
||||||
clearEncode,
|
|
||||||
decryptAsymmetric,
|
|
||||||
encryptAsymmetric,
|
|
||||||
getPublicKey,
|
|
||||||
} from '../../lib/waku_message/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);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue