mirror of
https://github.com/logos-messaging/logos-messaging-js.git
synced 2026-05-17 10:10:00 +00:00
Use message encryption with subscription API
This commit is contained in:
parent
7fdd15e725
commit
066bc7fa7a
@ -74,3 +74,32 @@ export async function sign(
|
|||||||
export function keccak256(input: Uint8Array): Uint8Array {
|
export function keccak256(input: Uint8Array): Uint8Array {
|
||||||
return new Uint8Array(sha3.keccak256.arrayBuffer(input));
|
return new Uint8Array(sha3.keccak256.arrayBuffer(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two public keys, can be used to verify that a given signature matches
|
||||||
|
* expectations.
|
||||||
|
*
|
||||||
|
* @param publicKeyA - The first public key to compare
|
||||||
|
* @param publicKeyB - The second public key to compare
|
||||||
|
* @returns true if the public keys are the same
|
||||||
|
*/
|
||||||
|
export function comparePublicKeys(
|
||||||
|
publicKeyA: Uint8Array | undefined,
|
||||||
|
publicKeyB: Uint8Array | undefined
|
||||||
|
): boolean {
|
||||||
|
if (!publicKeyA || !publicKeyB) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicKeyA.length !== publicKeyB.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < publicKeyA.length; i++) {
|
||||||
|
if (publicKeyA[i] !== publicKeyB[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
|
comparePublicKeys,
|
||||||
generatePrivateKey,
|
generatePrivateKey,
|
||||||
generateSymmetricKey,
|
generateSymmetricKey,
|
||||||
getPublicKey
|
getPublicKey
|
||||||
} from "./crypto/index.js";
|
} from "./crypto/index.js";
|
||||||
import { DecodedMessage } from "./decoded_message.js";
|
import { DecodedMessage } from "./decoded_message.js";
|
||||||
|
|
||||||
export { generatePrivateKey, generateSymmetricKey, getPublicKey };
|
export {
|
||||||
|
generatePrivateKey,
|
||||||
|
generateSymmetricKey,
|
||||||
|
getPublicKey,
|
||||||
|
comparePublicKeys
|
||||||
|
};
|
||||||
export type { DecodedMessage };
|
export type { DecodedMessage };
|
||||||
|
|
||||||
export * as ecies from "./ecies.js";
|
export * as ecies from "./ecies.js";
|
||||||
|
|||||||
@ -206,3 +206,105 @@ export function createDecoder(
|
|||||||
): Decoder {
|
): Decoder {
|
||||||
return new Decoder(contentTopic, routingInfo, symKey);
|
return new Decoder(contentTopic, routingInfo, symKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of decrypting a message with AES symmetric encryption.
|
||||||
|
*/
|
||||||
|
export interface SymmetricDecryptionResult {
|
||||||
|
/** The decrypted payload */
|
||||||
|
payload: Uint8Array;
|
||||||
|
/** The signature if the message was signed */
|
||||||
|
signature?: Uint8Array;
|
||||||
|
/** The recovered public key if the message was signed */
|
||||||
|
signaturePublicKey?: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES symmetric encryption.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Follows [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/) encryption standard.
|
||||||
|
*/
|
||||||
|
export class SymmetricEncryption {
|
||||||
|
/**
|
||||||
|
* Creates an AES Symmetric encryption instance.
|
||||||
|
*
|
||||||
|
* @param symKey - The symmetric key for encryption (32 bytes recommended)
|
||||||
|
* @param sigPrivKey - Optional private key to sign messages before encryption
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private symKey: Uint8Array,
|
||||||
|
private sigPrivKey?: Uint8Array
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts a byte array payload.
|
||||||
|
*
|
||||||
|
* The encryption process:
|
||||||
|
* 1. Optionally signs the payload with the private key
|
||||||
|
* 2. Adds padding to obscure payload size
|
||||||
|
* 3. Encrypts using AES-256-GCM
|
||||||
|
*
|
||||||
|
* @param payload - The data to encrypt
|
||||||
|
* @returns The encrypted payload
|
||||||
|
*/
|
||||||
|
public async encrypt(payload: Uint8Array): Promise<Uint8Array> {
|
||||||
|
const preparedPayload = await preCipher(payload, this.sigPrivKey);
|
||||||
|
return encryptSymmetric(preparedPayload, this.symKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES symmetric decryption.
|
||||||
|
*
|
||||||
|
* Follows [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/) encryption standard.
|
||||||
|
*/
|
||||||
|
export class SymmetricDecryption {
|
||||||
|
/**
|
||||||
|
* Creates an AES Symmetric decryption instance.
|
||||||
|
*
|
||||||
|
* @param symKey - The symmetric key for decryption (must match encryption key)
|
||||||
|
*/
|
||||||
|
public constructor(private symKey: Uint8Array) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted byte array payload.
|
||||||
|
*
|
||||||
|
* The decryption process:
|
||||||
|
* 1. Decrypts using AES-256-GCM
|
||||||
|
* 2. Removes padding
|
||||||
|
* 3. Verifies and recovers signature if present
|
||||||
|
*
|
||||||
|
* @param encryptedPayload - The encrypted data (from [[SymmetricEncryption.encrypt]])
|
||||||
|
* @returns Object containing the decrypted payload and signature info, or undefined if decryption fails
|
||||||
|
*/
|
||||||
|
public async decrypt(
|
||||||
|
encryptedPayload: Uint8Array
|
||||||
|
): Promise<SymmetricDecryptionResult | undefined> {
|
||||||
|
try {
|
||||||
|
const decryptedData = await decryptSymmetric(
|
||||||
|
encryptedPayload,
|
||||||
|
this.symKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!decryptedData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = postCipher(decryptedData);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
payload: result.payload,
|
||||||
|
signature: result.sig?.signature,
|
||||||
|
signaturePublicKey: result.sig?.publicKey
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to decrypt payload", error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7,10 +7,17 @@ import type {
|
|||||||
RelayNode
|
RelayNode
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { Protocols } from "@waku/interfaces";
|
import { Protocols } from "@waku/interfaces";
|
||||||
import { generateSymmetricKey } from "@waku/message-encryption";
|
import {
|
||||||
|
comparePublicKeys,
|
||||||
|
generatePrivateKey,
|
||||||
|
generateSymmetricKey,
|
||||||
|
getPublicKey
|
||||||
|
} from "@waku/message-encryption";
|
||||||
import {
|
import {
|
||||||
createDecoder,
|
createDecoder,
|
||||||
createEncoder
|
createEncoder,
|
||||||
|
SymmetricDecryption,
|
||||||
|
SymmetricDecryptionResult
|
||||||
} from "@waku/message-encryption/symmetric";
|
} from "@waku/message-encryption/symmetric";
|
||||||
import { createRelayNode } from "@waku/relay";
|
import { createRelayNode } from "@waku/relay";
|
||||||
import {
|
import {
|
||||||
@ -384,5 +391,58 @@ describe("Waku API", function () {
|
|||||||
expectedPubsubTopic: TestRoutingInfo.pubsubTopic
|
expectedPubsubTopic: TestRoutingInfo.pubsubTopic
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Subscribe and receive messages encrypted with AES", async function () {
|
||||||
|
const symKey = generateSymmetricKey();
|
||||||
|
const senderPrivKey = generatePrivateKey();
|
||||||
|
// TODO: For now, still using encoder
|
||||||
|
const newEncoder = createEncoder({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
routingInfo: TestRoutingInfo,
|
||||||
|
symKey,
|
||||||
|
sigPrivKey: senderPrivKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup payload decryption
|
||||||
|
const symDecryption = new SymmetricDecryption(symKey);
|
||||||
|
|
||||||
|
// subscribe to second content topic
|
||||||
|
waku.messageEmitter.addEventListener(TestContentTopic, (event) => {
|
||||||
|
const encryptedPayload = event.detail;
|
||||||
|
void symDecryption
|
||||||
|
.decrypt(encryptedPayload)
|
||||||
|
.then((decryptionResult: SymmetricDecryptionResult | undefined) => {
|
||||||
|
if (!decryptionResult) return;
|
||||||
|
serviceNodes.messageCollector.callback({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
payload: decryptionResult.payload
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: probably best to adapt the message collector
|
||||||
|
expect(decryptionResult?.signature).to.not.be.undefined;
|
||||||
|
expect(
|
||||||
|
comparePublicKeys(
|
||||||
|
getPublicKey(senderPrivKey),
|
||||||
|
decryptionResult?.signaturePublicKey
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// usually best to ignore decryption failure
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waku.subscribe([TestContentTopic]);
|
||||||
|
|
||||||
|
await waku.lightPush.send(newEncoder, {
|
||||||
|
payload: utf8ToBytes(messageText)
|
||||||
|
});
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true,
|
||||||
|
"Waiting for the message"
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(1, {
|
||||||
|
expectedContentTopic: TestContentTopic,
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedPubsubTopic: TestRoutingInfo.pubsubTopic
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user