From 06ee0ac1d82a2cf7ab265f4064dab88e2c10e446 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 13 Jul 2021 12:32:57 +1000 Subject: [PATCH] Symmetric encryption for node --- src/lib/waku_message/symmetric/browser.ts | 65 +++++++++++++++++++++++ src/lib/waku_message/symmetric/index.ts | 26 +++++++++ src/lib/waku_message/symmetric/node.ts | 46 ++++++++++++++++ src/lib/waku_message/version_1.spec.ts | 17 ++++++ src/lib/waku_message/version_1.ts | 55 ++++++++++++++++++- 5 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 src/lib/waku_message/symmetric/browser.ts create mode 100644 src/lib/waku_message/symmetric/index.ts create mode 100644 src/lib/waku_message/symmetric/node.ts diff --git a/src/lib/waku_message/symmetric/browser.ts b/src/lib/waku_message/symmetric/browser.ts new file mode 100644 index 0000000000..bda8d7eda5 --- /dev/null +++ b/src/lib/waku_message/symmetric/browser.ts @@ -0,0 +1,65 @@ +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'); +} + +/** + * Proceed with symmetric encryption of `clearText` value. + */ +async function encrypt( + iv: Buffer, + key: Buffer, + clearText: Buffer +): Promise { + return subtle + .importKey('raw', key, Algorithm, false, ['encrypt']) + .then((cryptoKey) => + subtle.encrypt({ iv, ...Algorithm }, cryptoKey, clearText) + ) + .then(Buffer.from); +} + +/** + * Proceed with symmetric decryption of `cipherText` value. + */ +async function decrypt( + iv: Buffer, + key: Buffer, + cipherText: Buffer +): Promise { + return subtle + .importKey('raw', key, Algorithm, false, ['decrypt']) + .then((cryptoKey) => + subtle.encrypt({ iv, ...Algorithm }, cryptoKey, cipherText) + ) + .then(Buffer.from); +} + +/** + * Generate a new private key for Symmetric encryption purposes. + */ +function generateKeyForSymmetricEnc(): Buffer { + return crypto.getRandomValues(Buffer.alloc(SymmetricKeySize)); +} + +/** + * Generate an Initialisation Vector (iv) for for Symmetric encryption purposes. + */ +function generateIv(): Buffer { + return crypto.getRandomValues(Buffer.alloc(IvSize)); +} diff --git a/src/lib/waku_message/symmetric/index.ts b/src/lib/waku_message/symmetric/index.ts new file mode 100644 index 0000000000..06be09d234 --- /dev/null +++ b/src/lib/waku_message/symmetric/index.ts @@ -0,0 +1,26 @@ +export const SymmetricKeySize = 32; +export const IvSize = 12; +export const TagSize = 16; + +export interface Symmetric { + encrypt: (iv: Buffer, key: Buffer, clearText: Buffer) => Buffer; + decrypt: (iv: Buffer, tag: Buffer, key: Buffer, cipherText: Buffer) => Buffer; + generateKeyForSymmetricEnc: () => Buffer; + generateIv: () => Buffer; +} + +export let symmetric: Symmetric = {} as unknown as Symmetric; + +import('./browser') + .then((mod) => { + symmetric = mod as unknown as Symmetric; + }) + .catch((eBrowser) => { + import('./node') + .then((mod) => { + symmetric = mod; + }) + .catch((eNode) => { + throw `Could not load any symmetric crypto modules: ${eBrowser}, ${eNode}`; + }); + }); diff --git a/src/lib/waku_message/symmetric/node.ts b/src/lib/waku_message/symmetric/node.ts new file mode 100644 index 0000000000..53c021a584 --- /dev/null +++ b/src/lib/waku_message/symmetric/node.ts @@ -0,0 +1,46 @@ +import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'; + +import { IvSize, SymmetricKeySize } from './index'; + +const Algorithm = 'aes-256-gcm'; + +/** + * Proceed with symmetric encryption of `clearText` value. + */ +export function encrypt(iv: Buffer, key: Buffer, clearText: Buffer): 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]); +} + +/** + * Proceed with symmetric decryption of `cipherText` value. + */ +export function decrypt( + iv: Buffer, + tag: Buffer, + key: Buffer, + cipherText: Buffer +): Buffer { + const decipher = createDecipheriv(Algorithm, key, iv); + decipher.setAuthTag(tag); + const a = decipher.update(cipherText); + const b = decipher.final(); + return Buffer.concat([a, b]); +} + +/** + * Generate a new private key for Symmetric encryption purposes. + */ +export function generateKeyForSymmetricEnc(): Buffer { + return randomBytes(SymmetricKeySize); +} + +/** + * Generate an Initialisation Vector (iv) for for Symmetric encryption purposes. + */ +export function generateIv(): Buffer { + return randomBytes(IvSize); +} diff --git a/src/lib/waku_message/version_1.spec.ts b/src/lib/waku_message/version_1.spec.ts index d37326658c..89e6b4d169 100644 --- a/src/lib/waku_message/version_1.spec.ts +++ b/src/lib/waku_message/version_1.spec.ts @@ -5,7 +5,9 @@ import { clearDecode, clearEncode, decryptAsymmetric, + decryptSymmetric, encryptAsymmetric, + encryptSymmetric, getPublicKey, } 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); + } + ) + ); + }); }); diff --git a/src/lib/waku_message/version_1.ts b/src/lib/waku_message/version_1.ts index 82ddc249b6..30abafec40 100644 --- a/src/lib/waku_message/version_1.ts +++ b/src/lib/waku_message/version_1.ts @@ -7,6 +7,8 @@ import * as secp256k1 from 'secp256k1'; import { hexToBuf } from '../utils'; +import { IvSize, symmetric, TagSize } from './symmetric'; + const FlagsLength = 1; const FlagMask = 3; // 0011 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 returned result can be set to `WakuMessage.payload`. * @@ -115,6 +117,12 @@ export async function encryptAsymmetric( 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( payload: Uint8Array | Buffer, privKey: Uint8Array | Buffer @@ -122,6 +130,49 @@ export async function decryptAsymmetric( 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 { + const iv = symmetric.generateIv(); + + // Returns `cipher | tag` + const cipher = 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 { + const data = Buffer.from(payload); + const ivStart = payload.length - IvSize; + const tagStart = ivStart - TagSize; + const cipher = data.slice(0, tagStart); + const tag = data.slice(tagStart, ivStart); + const iv = data.slice(ivStart); + + return symmetric.decrypt(iv, tag, hexToBuf(key), cipher); +} + /** * Generate a new private key */ @@ -137,7 +188,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 { const fieldSize = getSizeOfPayloadSizeField(payload);