From 563b66eab5806610f7a300e0232363db56c814bc Mon Sep 17 00:00:00 2001 From: "fryorcraken.eth" Date: Wed, 23 Nov 2022 16:59:15 +1100 Subject: [PATCH] refactor: separate symmetric and asymmetric encoders --- packages/message-encryption/src/ecies.spec.ts | 71 +++++ packages/message-encryption/src/ecies.ts | 134 ++++++++++ packages/message-encryption/src/index.spec.ts | 212 --------------- packages/message-encryption/src/index.ts | 245 +----------------- .../message-encryption/src/symmetric.spec.ts | 66 +++++ packages/message-encryption/src/symmetric.ts | 133 ++++++++++ .../src/waku_payload.spec.ts | 84 ++++++ 7 files changed, 490 insertions(+), 455 deletions(-) create mode 100644 packages/message-encryption/src/ecies.spec.ts create mode 100644 packages/message-encryption/src/ecies.ts delete mode 100644 packages/message-encryption/src/index.spec.ts create mode 100644 packages/message-encryption/src/symmetric.spec.ts create mode 100644 packages/message-encryption/src/symmetric.ts create mode 100644 packages/message-encryption/src/waku_payload.spec.ts diff --git a/packages/message-encryption/src/ecies.spec.ts b/packages/message-encryption/src/ecies.spec.ts new file mode 100644 index 0000000000..b9f75c03c5 --- /dev/null +++ b/packages/message-encryption/src/ecies.spec.ts @@ -0,0 +1,71 @@ +import { expect } from "chai"; +import fc from "fast-check"; + +import { getPublicKey } from "./crypto/index.js"; +import { createAsymDecoder, createAsymEncoder } from "./ecies.js"; + +const TestContentTopic = "/test/1/waku-message/utf8"; + +describe("Ecies Encryption", function () { + it("Round trip binary encryption [ecies, no signature]", async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (payload, privateKey) => { + const publicKey = getPublicKey(privateKey); + + const encoder = createAsymEncoder(TestContentTopic, publicKey); + const bytes = await encoder.toWire({ payload }); + + const decoder = createAsymDecoder(TestContentTopic, privateKey); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(protoResult); + if (!result) throw "Failed to decode"; + + expect(result.contentTopic).to.equal(TestContentTopic); + expect(result.version).to.equal(1); + expect(result?.payload).to.deep.equal(payload); + expect(result.signature).to.be.undefined; + expect(result.signaturePublicKey).to.be.undefined; + } + ) + ); + }); + + it("R trip binary encryption [ecies, signature]", async function () { + this.timeout(4000); + + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (payload, alicePrivateKey, bobPrivateKey) => { + const alicePublicKey = getPublicKey(alicePrivateKey); + const bobPublicKey = getPublicKey(bobPrivateKey); + + const encoder = createAsymEncoder( + TestContentTopic, + bobPublicKey, + alicePrivateKey + ); + const bytes = await encoder.toWire({ payload }); + + const decoder = createAsymDecoder(TestContentTopic, bobPrivateKey); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(protoResult); + if (!result) throw "Failed to decode"; + + expect(result.contentTopic).to.equal(TestContentTopic); + expect(result.version).to.equal(1); + expect(result?.payload).to.deep.equal(payload); + expect(result.signature).to.not.be.undefined; + expect(result.signaturePublicKey).to.deep.eq(alicePublicKey); + } + ) + ); + }); +}); diff --git a/packages/message-encryption/src/ecies.ts b/packages/message-encryption/src/ecies.ts new file mode 100644 index 0000000000..f0075f99ba --- /dev/null +++ b/packages/message-encryption/src/ecies.ts @@ -0,0 +1,134 @@ +import { + Decoder as DecoderV0, + proto, +} from "@waku/core/lib/waku_message/version_0"; +import type { + Decoder as IDecoder, + Encoder as IEncoder, + Message, + ProtoMessage, +} from "@waku/interfaces"; +import debug from "debug"; + +import { + decryptAsymmetric, + encryptAsymmetric, + postCipher, + preCipher, +} from "./waku_payload.js"; + +import { DecodedMessage, OneMillion, Version } from "./index.js"; + +const log = debug("waku:message-encryption:ecies"); + +class AsymEncoder implements IEncoder { + constructor( + public contentTopic: string, + private publicKey: Uint8Array, + private sigPrivKey?: Uint8Array, + public ephemeral: boolean = false + ) {} + + async toWire(message: Partial): Promise { + const protoMessage = await this.toProtoObj(message); + if (!protoMessage) return; + + return proto.WakuMessage.encode(protoMessage); + } + + async toProtoObj( + message: Partial + ): Promise { + const timestamp = message.timestamp ?? new Date(); + if (!message.payload) { + log("No payload to encrypt, skipping: ", message); + return; + } + const preparedPayload = await preCipher(message.payload, this.sigPrivKey); + + const payload = await encryptAsymmetric(preparedPayload, this.publicKey); + + return { + payload, + version: Version, + contentTopic: this.contentTopic, + timestamp: BigInt(timestamp.valueOf()) * OneMillion, + rateLimitProof: message.rateLimitProof, + ephemeral: this.ephemeral, + }; + } +} + +export function createAsymEncoder( + contentTopic: string, + publicKey: Uint8Array, + sigPrivKey?: Uint8Array, + ephemeral = false +): AsymEncoder { + return new AsymEncoder(contentTopic, publicKey, sigPrivKey, ephemeral); +} + +class AsymDecoder extends DecoderV0 implements IDecoder { + constructor(contentTopic: string, private privateKey: Uint8Array) { + super(contentTopic); + } + + async fromProtoObj( + protoMessage: ProtoMessage + ): Promise { + const cipherPayload = protoMessage.payload; + + if (protoMessage.version !== Version) { + log( + "Failed to decrypt due to incorrect version, expected:", + Version, + ", actual:", + protoMessage.version + ); + return; + } + + let payload; + if (!cipherPayload) { + log(`No payload to decrypt for contentTopic ${this.contentTopic}`); + return; + } + + try { + payload = await decryptAsymmetric(cipherPayload, this.privateKey); + } catch (e) { + log( + `Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`, + e + ); + return; + } + + if (!payload) { + log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`); + return; + } + + const res = await postCipher(payload); + + if (!res) { + log(`Failed to decode payload for contentTopic ${this.contentTopic}`); + return; + } + + log("Message decrypted", protoMessage); + return new DecodedMessage( + protoMessage, + res.payload, + res.sig?.signature, + res.sig?.publicKey + ); + } +} + +export function createAsymDecoder( + contentTopic: string, + privateKey: Uint8Array +): AsymDecoder { + return new AsymDecoder(contentTopic, privateKey); +} diff --git a/packages/message-encryption/src/index.spec.ts b/packages/message-encryption/src/index.spec.ts deleted file mode 100644 index eebfbc4c80..0000000000 --- a/packages/message-encryption/src/index.spec.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { expect } from "chai"; -import fc from "fast-check"; - -import { getPublicKey } from "./crypto.js"; - -import { - createAsymDecoder, - createAsymEncoder, - createSymDecoder, - createSymEncoder, - decryptAsymmetric, - decryptSymmetric, - encryptAsymmetric, - encryptSymmetric, - postCipher, - preCipher, -} from "./index.js"; - -const TestContentTopic = "/test/1/waku-message/utf8"; - -describe("Waku Message version 1", function () { - it("Round trip binary encryption [asymmetric, no signature]", async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - async (payload, privateKey) => { - const publicKey = getPublicKey(privateKey); - - const encoder = createAsymEncoder(TestContentTopic, publicKey); - const bytes = await encoder.toWire({ payload }); - - const decoder = createAsymDecoder(TestContentTopic, privateKey); - const protoResult = await decoder.fromWireToProtoObj(bytes!); - if (!protoResult) throw "Failed to proto decode"; - const result = await decoder.fromProtoObj(protoResult); - if (!result) throw "Failed to decode"; - - expect(result.contentTopic).to.equal(TestContentTopic); - expect(result.version).to.equal(1); - expect(result?.payload).to.deep.equal(payload); - expect(result.signature).to.be.undefined; - expect(result.signaturePublicKey).to.be.undefined; - } - ) - ); - }); - - it("R trip binary encryption [asymmetric, signature]", async function () { - this.timeout(4000); - - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - async (payload, alicePrivateKey, bobPrivateKey) => { - const alicePublicKey = getPublicKey(alicePrivateKey); - const bobPublicKey = getPublicKey(bobPrivateKey); - - const encoder = createAsymEncoder( - TestContentTopic, - bobPublicKey, - alicePrivateKey - ); - const bytes = await encoder.toWire({ payload }); - - const decoder = createAsymDecoder(TestContentTopic, bobPrivateKey); - const protoResult = await decoder.fromWireToProtoObj(bytes!); - if (!protoResult) throw "Failed to proto decode"; - const result = await decoder.fromProtoObj(protoResult); - if (!result) throw "Failed to decode"; - - expect(result.contentTopic).to.equal(TestContentTopic); - expect(result.version).to.equal(1); - expect(result?.payload).to.deep.equal(payload); - expect(result.signature).to.not.be.undefined; - expect(result.signaturePublicKey).to.deep.eq(alicePublicKey); - } - ) - ); - }); - - it("Round trip binary encryption [symmetric, no signature]", async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - async (payload, symKey) => { - const encoder = createSymEncoder(TestContentTopic, symKey); - const bytes = await encoder.toWire({ payload }); - - const decoder = createSymDecoder(TestContentTopic, symKey); - const protoResult = await decoder.fromWireToProtoObj(bytes!); - if (!protoResult) throw "Failed to proto decode"; - const result = await decoder.fromProtoObj(protoResult); - if (!result) throw "Failed to decode"; - - expect(result.contentTopic).to.equal(TestContentTopic); - expect(result.version).to.equal(1); - expect(result?.payload).to.deep.equal(payload); - expect(result.signature).to.be.undefined; - expect(result.signaturePublicKey).to.be.undefined; - } - ) - ); - }); - - it("Round trip binary encryption [symmetric, signature]", async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - async (payload, sigPrivKey, symKey) => { - const sigPubKey = getPublicKey(sigPrivKey); - - const encoder = createSymEncoder( - TestContentTopic, - symKey, - sigPrivKey - ); - const bytes = await encoder.toWire({ payload }); - - const decoder = createSymDecoder(TestContentTopic, symKey); - const protoResult = await decoder.fromWireToProtoObj(bytes!); - if (!protoResult) throw "Failed to proto decode"; - const result = await decoder.fromProtoObj(protoResult); - if (!result) throw "Failed to decode"; - - expect(result.contentTopic).to.equal(TestContentTopic); - expect(result.version).to.equal(1); - expect(result?.payload).to.deep.equal(payload); - expect(result.signature).to.not.be.undefined; - expect(result.signaturePublicKey).to.deep.eq(sigPubKey); - } - ) - ); - }); -}); - -describe("Encryption helpers", () => { - it("Asymmetric encrypt & decrypt", async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ min: 1, 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); - } - ) - ); - }); - - 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); - } - ) - ); - }); - - it("pre and post cipher", async function () { - await fc.assert( - fc.asyncProperty(fc.uint8Array(), async (message) => { - const enc = await preCipher(message); - const res = postCipher(enc); - - expect(res?.payload).deep.equal( - message, - "Payload was not encrypted then decrypted correctly" - ); - }) - ); - }); - - it("Sign & Recover", async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array(), - fc.uint8Array({ minLength: 32, maxLength: 32 }), - async (message, sigPrivKey) => { - const sigPubKey = getPublicKey(sigPrivKey); - - const enc = await preCipher(message, sigPrivKey); - const res = postCipher(enc); - - expect(res?.payload).deep.equal( - message, - "Payload was not encrypted then decrypted correctly" - ); - expect(res?.sig?.publicKey).deep.equal( - sigPubKey, - "signature Public key was not recovered from encrypted then decrypted signature" - ); - } - ) - ); - }); -}); diff --git a/packages/message-encryption/src/index.ts b/packages/message-encryption/src/index.ts index 2f59584acc..d10b2ab004 100644 --- a/packages/message-encryption/src/index.ts +++ b/packages/message-encryption/src/index.ts @@ -1,34 +1,16 @@ import { DecodedMessage as DecodedMessageV0, - Decoder as DecoderV0, proto, } from "@waku/core/lib/waku_message/version_0"; -import type { - DecodedMessage as IDecodedMessage, - Decoder as IDecoder, - Encoder as IEncoder, - Message, - ProtoMessage, -} from "@waku/interfaces"; -import debug from "debug"; +import type { DecodedMessage as IDecodedMessage } from "@waku/interfaces"; import { generatePrivateKey, generateSymmetricKey, getPublicKey, } from "./crypto/index.js"; -import { - decryptAsymmetric, - decryptSymmetric, - encryptAsymmetric, - encryptSymmetric, - postCipher, - preCipher, -} from "./waku_payload.js"; -const log = debug("waku:message:version-1"); - -const OneMillion = BigInt(1_000_000); +export const OneMillion = BigInt(1_000_000); export { generatePrivateKey, generateSymmetricKey, getPublicKey }; @@ -59,226 +41,3 @@ export class DecodedMessage return this._decodedPayload; } } - -class AsymEncoder implements IEncoder { - constructor( - public contentTopic: string, - private publicKey: Uint8Array, - private sigPrivKey?: Uint8Array, - public ephemeral: boolean = false - ) {} - - async toWire(message: Partial): Promise { - const protoMessage = await this.toProtoObj(message); - if (!protoMessage) return; - - return proto.WakuMessage.encode(protoMessage); - } - - async toProtoObj( - message: Partial - ): Promise { - const timestamp = message.timestamp ?? new Date(); - if (!message.payload) { - log("No payload to encrypt, skipping: ", message); - return; - } - const preparedPayload = await preCipher(message.payload, this.sigPrivKey); - - const payload = await encryptAsymmetric(preparedPayload, this.publicKey); - - return { - payload, - version: Version, - contentTopic: this.contentTopic, - timestamp: BigInt(timestamp.valueOf()) * OneMillion, - rateLimitProof: message.rateLimitProof, - ephemeral: this.ephemeral, - }; - } -} - -export function createAsymEncoder( - contentTopic: string, - publicKey: Uint8Array, - sigPrivKey?: Uint8Array, - ephemeral = false -): AsymEncoder { - return new AsymEncoder(contentTopic, publicKey, sigPrivKey, ephemeral); -} - -class SymEncoder implements IEncoder { - constructor( - public contentTopic: string, - private symKey: Uint8Array, - private sigPrivKey?: Uint8Array, - public ephemeral: boolean = false - ) {} - - async toWire(message: Partial): Promise { - const protoMessage = await this.toProtoObj(message); - if (!protoMessage) return; - - return proto.WakuMessage.encode(protoMessage); - } - - async toProtoObj( - message: Partial - ): Promise { - const timestamp = message.timestamp ?? new Date(); - if (!message.payload) { - log("No payload to encrypt, skipping: ", message); - return; - } - const preparedPayload = await preCipher(message.payload, this.sigPrivKey); - - const payload = await encryptSymmetric(preparedPayload, this.symKey); - return { - payload, - version: Version, - contentTopic: this.contentTopic, - timestamp: BigInt(timestamp.valueOf()) * OneMillion, - rateLimitProof: message.rateLimitProof, - ephemeral: this.ephemeral, - }; - } -} - -export function createSymEncoder( - contentTopic: string, - symKey: Uint8Array, - sigPrivKey?: Uint8Array, - ephemeral = false -): SymEncoder { - return new SymEncoder(contentTopic, symKey, sigPrivKey, ephemeral); -} - -class AsymDecoder extends DecoderV0 implements IDecoder { - constructor(contentTopic: string, private privateKey: Uint8Array) { - super(contentTopic); - } - - async fromProtoObj( - protoMessage: ProtoMessage - ): Promise { - const cipherPayload = protoMessage.payload; - - if (protoMessage.version !== Version) { - log( - "Failed to decrypt due to incorrect version, expected:", - Version, - ", actual:", - protoMessage.version - ); - return; - } - - let payload; - if (!cipherPayload) { - log(`No payload to decrypt for contentTopic ${this.contentTopic}`); - return; - } - - try { - payload = await decryptAsymmetric(cipherPayload, this.privateKey); - } catch (e) { - log( - `Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`, - e - ); - return; - } - - if (!payload) { - log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`); - return; - } - - const res = await postCipher(payload); - - if (!res) { - log(`Failed to decode payload for contentTopic ${this.contentTopic}`); - return; - } - - log("Message decrypted", protoMessage); - return new DecodedMessage( - protoMessage, - res.payload, - res.sig?.signature, - res.sig?.publicKey - ); - } -} - -export function createAsymDecoder( - contentTopic: string, - privateKey: Uint8Array -): AsymDecoder { - return new AsymDecoder(contentTopic, privateKey); -} - -class SymDecoder extends DecoderV0 implements IDecoder { - constructor(contentTopic: string, private symKey: Uint8Array) { - super(contentTopic); - } - - async fromProtoObj( - protoMessage: ProtoMessage - ): Promise { - const cipherPayload = protoMessage.payload; - - if (protoMessage.version !== Version) { - log( - "Failed to decrypt due to incorrect version, expected:", - Version, - ", actual:", - protoMessage.version - ); - return; - } - - let payload; - if (!cipherPayload) { - log(`No payload to decrypt for contentTopic ${this.contentTopic}`); - return; - } - - try { - payload = await decryptSymmetric(cipherPayload, this.symKey); - } catch (e) { - log( - `Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`, - e - ); - return; - } - - if (!payload) { - log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`); - return; - } - - const res = await postCipher(payload); - - if (!res) { - log(`Failed to decode payload for contentTopic ${this.contentTopic}`); - return; - } - - log("Message decrypted", protoMessage); - return new DecodedMessage( - protoMessage, - res.payload, - res.sig?.signature, - res.sig?.publicKey - ); - } -} - -export function createSymDecoder( - contentTopic: string, - symKey: Uint8Array -): SymDecoder { - return new SymDecoder(contentTopic, symKey); -} diff --git a/packages/message-encryption/src/symmetric.spec.ts b/packages/message-encryption/src/symmetric.spec.ts new file mode 100644 index 0000000000..94374a606a --- /dev/null +++ b/packages/message-encryption/src/symmetric.spec.ts @@ -0,0 +1,66 @@ +import { expect } from "chai"; +import fc from "fast-check"; + +import { getPublicKey } from "./crypto/index.js"; +import { createSymDecoder, createSymEncoder } from "./symmetric.js"; + +const TestContentTopic = "/test/1/waku-message/utf8"; + +describe("Symmetric Encryption", function () { + it("Round trip binary encryption [symmetric, no signature]", async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (payload, symKey) => { + const encoder = createSymEncoder(TestContentTopic, symKey); + const bytes = await encoder.toWire({ payload }); + + const decoder = createSymDecoder(TestContentTopic, symKey); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(protoResult); + if (!result) throw "Failed to decode"; + + expect(result.contentTopic).to.equal(TestContentTopic); + expect(result.version).to.equal(1); + expect(result?.payload).to.deep.equal(payload); + expect(result.signature).to.be.undefined; + expect(result.signaturePublicKey).to.be.undefined; + } + ) + ); + }); + + it("Round trip binary encryption [symmetric, signature]", async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (payload, sigPrivKey, symKey) => { + const sigPubKey = getPublicKey(sigPrivKey); + + const encoder = createSymEncoder( + TestContentTopic, + symKey, + sigPrivKey + ); + const bytes = await encoder.toWire({ payload }); + + const decoder = createSymDecoder(TestContentTopic, symKey); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(protoResult); + if (!result) throw "Failed to decode"; + + expect(result.contentTopic).to.equal(TestContentTopic); + expect(result.version).to.equal(1); + expect(result?.payload).to.deep.equal(payload); + expect(result.signature).to.not.be.undefined; + expect(result.signaturePublicKey).to.deep.eq(sigPubKey); + } + ) + ); + }); +}); diff --git a/packages/message-encryption/src/symmetric.ts b/packages/message-encryption/src/symmetric.ts new file mode 100644 index 0000000000..cf990ce669 --- /dev/null +++ b/packages/message-encryption/src/symmetric.ts @@ -0,0 +1,133 @@ +import { + Decoder as DecoderV0, + proto, +} from "@waku/core/lib/waku_message/version_0"; +import type { + Decoder as IDecoder, + Encoder as IEncoder, + Message, + ProtoMessage, +} from "@waku/interfaces"; +import debug from "debug"; + +import { + decryptSymmetric, + encryptSymmetric, + postCipher, + preCipher, +} from "./waku_payload.js"; + +import { DecodedMessage, OneMillion, Version } from "./index.js"; + +const log = debug("waku:message-encryption:symmetric"); + +class SymEncoder implements IEncoder { + constructor( + public contentTopic: string, + private symKey: Uint8Array, + private sigPrivKey?: Uint8Array, + public ephemeral: boolean = false + ) {} + + async toWire(message: Partial): Promise { + const protoMessage = await this.toProtoObj(message); + if (!protoMessage) return; + + return proto.WakuMessage.encode(protoMessage); + } + + async toProtoObj( + message: Partial + ): Promise { + const timestamp = message.timestamp ?? new Date(); + if (!message.payload) { + log("No payload to encrypt, skipping: ", message); + return; + } + const preparedPayload = await preCipher(message.payload, this.sigPrivKey); + + const payload = await encryptSymmetric(preparedPayload, this.symKey); + return { + payload, + version: Version, + contentTopic: this.contentTopic, + timestamp: BigInt(timestamp.valueOf()) * OneMillion, + rateLimitProof: message.rateLimitProof, + ephemeral: this.ephemeral, + }; + } +} + +export function createSymEncoder( + contentTopic: string, + symKey: Uint8Array, + sigPrivKey?: Uint8Array, + ephemeral = false +): SymEncoder { + return new SymEncoder(contentTopic, symKey, sigPrivKey, ephemeral); +} + +class SymDecoder extends DecoderV0 implements IDecoder { + constructor(contentTopic: string, private symKey: Uint8Array) { + super(contentTopic); + } + + async fromProtoObj( + protoMessage: ProtoMessage + ): Promise { + const cipherPayload = protoMessage.payload; + + if (protoMessage.version !== Version) { + log( + "Failed to decrypt due to incorrect version, expected:", + Version, + ", actual:", + protoMessage.version + ); + return; + } + + let payload; + if (!cipherPayload) { + log(`No payload to decrypt for contentTopic ${this.contentTopic}`); + return; + } + + try { + payload = await decryptSymmetric(cipherPayload, this.symKey); + } catch (e) { + log( + `Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`, + e + ); + return; + } + + if (!payload) { + log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`); + return; + } + + const res = await postCipher(payload); + + if (!res) { + log(`Failed to decode payload for contentTopic ${this.contentTopic}`); + return; + } + + log("Message decrypted", protoMessage); + return new DecodedMessage( + protoMessage, + res.payload, + res.sig?.signature, + res.sig?.publicKey + ); + } +} + +export function createSymDecoder( + contentTopic: string, + symKey: Uint8Array +): SymDecoder { + return new SymDecoder(contentTopic, symKey); +} diff --git a/packages/message-encryption/src/waku_payload.spec.ts b/packages/message-encryption/src/waku_payload.spec.ts new file mode 100644 index 0000000000..4fa1cd2e35 --- /dev/null +++ b/packages/message-encryption/src/waku_payload.spec.ts @@ -0,0 +1,84 @@ +import { expect } from "chai"; +import fc from "fast-check"; + +import { getPublicKey } from "./crypto/index.js"; +import { + decryptAsymmetric, + decryptSymmetric, + encryptAsymmetric, + encryptSymmetric, + postCipher, + preCipher, +} from "./waku_payload.js"; + +describe("Waku Payload", () => { + it("Asymmetric encrypt & decrypt", async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, 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); + } + ) + ); + }); + + 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); + } + ) + ); + }); + + it("pre and post cipher", async function () { + await fc.assert( + fc.asyncProperty(fc.uint8Array(), async (message) => { + const enc = await preCipher(message); + const res = postCipher(enc); + + expect(res?.payload).deep.equal( + message, + "Payload was not encrypted then decrypted correctly" + ); + }) + ); + }); + + it("Sign & Recover", async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array(), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (message, sigPrivKey) => { + const sigPubKey = getPublicKey(sigPrivKey); + + const enc = await preCipher(message, sigPrivKey); + const res = postCipher(enc); + + expect(res?.payload).deep.equal( + message, + "Payload was not encrypted then decrypted correctly" + ); + expect(res?.sig?.publicKey).deep.equal( + sigPubKey, + "signature Public key was not recovered from encrypted then decrypted signature" + ); + } + ) + ); + }); +});