mirror of https://github.com/status-im/js-waku.git
refactor: separate symmetric and asymmetric encoders
This commit is contained in:
parent
1d727b2bc0
commit
563b66eab5
|
@ -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);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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<Message>): Promise<Uint8Array | undefined> {
|
||||
const protoMessage = await this.toProtoObj(message);
|
||||
if (!protoMessage) return;
|
||||
|
||||
return proto.WakuMessage.encode(protoMessage);
|
||||
}
|
||||
|
||||
async toProtoObj(
|
||||
message: Partial<Message>
|
||||
): Promise<ProtoMessage | undefined> {
|
||||
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<DecodedMessage> {
|
||||
constructor(contentTopic: string, private privateKey: Uint8Array) {
|
||||
super(contentTopic);
|
||||
}
|
||||
|
||||
async fromProtoObj(
|
||||
protoMessage: ProtoMessage
|
||||
): Promise<DecodedMessage | undefined> {
|
||||
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);
|
||||
}
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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<Message>): Promise<Uint8Array | undefined> {
|
||||
const protoMessage = await this.toProtoObj(message);
|
||||
if (!protoMessage) return;
|
||||
|
||||
return proto.WakuMessage.encode(protoMessage);
|
||||
}
|
||||
|
||||
async toProtoObj(
|
||||
message: Partial<Message>
|
||||
): Promise<ProtoMessage | undefined> {
|
||||
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<Message>): Promise<Uint8Array | undefined> {
|
||||
const protoMessage = await this.toProtoObj(message);
|
||||
if (!protoMessage) return;
|
||||
|
||||
return proto.WakuMessage.encode(protoMessage);
|
||||
}
|
||||
|
||||
async toProtoObj(
|
||||
message: Partial<Message>
|
||||
): Promise<ProtoMessage | undefined> {
|
||||
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<DecodedMessage> {
|
||||
constructor(contentTopic: string, private privateKey: Uint8Array) {
|
||||
super(contentTopic);
|
||||
}
|
||||
|
||||
async fromProtoObj(
|
||||
protoMessage: ProtoMessage
|
||||
): Promise<DecodedMessage | undefined> {
|
||||
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<DecodedMessage> {
|
||||
constructor(contentTopic: string, private symKey: Uint8Array) {
|
||||
super(contentTopic);
|
||||
}
|
||||
|
||||
async fromProtoObj(
|
||||
protoMessage: ProtoMessage
|
||||
): Promise<DecodedMessage | undefined> {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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<Message>): Promise<Uint8Array | undefined> {
|
||||
const protoMessage = await this.toProtoObj(message);
|
||||
if (!protoMessage) return;
|
||||
|
||||
return proto.WakuMessage.encode(protoMessage);
|
||||
}
|
||||
|
||||
async toProtoObj(
|
||||
message: Partial<Message>
|
||||
): Promise<ProtoMessage | undefined> {
|
||||
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<DecodedMessage> {
|
||||
constructor(contentTopic: string, private symKey: Uint8Array) {
|
||||
super(contentTopic);
|
||||
}
|
||||
|
||||
async fromProtoObj(
|
||||
protoMessage: ProtoMessage
|
||||
): Promise<DecodedMessage | undefined> {
|
||||
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);
|
||||
}
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue