mirror of https://github.com/waku-org/js-waku.git
Implement Waku Message Version 1 encoding and signature
This commit is contained in:
parent
7b5c8d6094
commit
f97dc4de81
|
@ -55,9 +55,11 @@
|
|||
"protobuf",
|
||||
"protoc",
|
||||
"reactjs",
|
||||
"recid",
|
||||
"rlnrelay",
|
||||
"sandboxed",
|
||||
"secio",
|
||||
"secp",
|
||||
"staticnode",
|
||||
"statusim",
|
||||
"submodule",
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"debug": "^4.3.1",
|
||||
"it-concat": "^2.0.0",
|
||||
"it-length-prefixed": "^5.0.2",
|
||||
"js-sha3": "^0.8.0",
|
||||
"libp2p": "^0.31.7",
|
||||
"libp2p-gossipsub": "^0.10.0",
|
||||
"libp2p-mplex": "^0.10.3",
|
||||
|
@ -19,6 +20,7 @@
|
|||
"libp2p-tcp": "^0.15.4",
|
||||
"libp2p-websockets": "^0.15.6",
|
||||
"multiaddr": "^9.0.1",
|
||||
"secp256k1": "^4.0.2",
|
||||
"ts-proto": "^1.79.7",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
|
@ -30,6 +32,7 @@
|
|||
"@types/google-protobuf": "^3.7.4",
|
||||
"@types/mocha": "^8.2.2",
|
||||
"@types/node": "^14.14.31",
|
||||
"@types/secp256k1": "^4.0.2",
|
||||
"@types/tail": "^2.0.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.1",
|
||||
|
@ -3586,6 +3589,15 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
|
||||
},
|
||||
"node_modules/@types/secp256k1": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.2.tgz",
|
||||
"integrity": "sha512-QMg+9v0bbNJ2peLuHRWxzmy0HRJIG6gFZNhaRSp7S3ggSbCCxiqQB2/ybvhXyhHOCequpNkrx7OavNhrWOsW0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tail": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/tail/-/tail-2.2.0.tgz",
|
||||
|
@ -22021,6 +22033,15 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
|
||||
},
|
||||
"@types/secp256k1": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.2.tgz",
|
||||
"integrity": "sha512-QMg+9v0bbNJ2peLuHRWxzmy0HRJIG6gFZNhaRSp7S3ggSbCCxiqQB2/ybvhXyhHOCequpNkrx7OavNhrWOsW0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/tail": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/tail/-/tail-2.2.0.tgz",
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
"debug": "^4.3.1",
|
||||
"it-concat": "^2.0.0",
|
||||
"it-length-prefixed": "^5.0.2",
|
||||
"js-sha3": "^0.8.0",
|
||||
"libp2p": "^0.31.7",
|
||||
"libp2p-gossipsub": "^0.10.0",
|
||||
"libp2p-mplex": "^0.10.3",
|
||||
|
@ -66,6 +67,7 @@
|
|||
"libp2p-tcp": "^0.15.4",
|
||||
"libp2p-websockets": "^0.15.6",
|
||||
"multiaddr": "^9.0.1",
|
||||
"secp256k1": "^4.0.2",
|
||||
"ts-proto": "^1.79.7",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
|
@ -77,6 +79,7 @@
|
|||
"@types/google-protobuf": "^3.7.4",
|
||||
"@types/mocha": "^8.2.2",
|
||||
"@types/node": "^14.14.31",
|
||||
"@types/secp256k1": "^4.0.2",
|
||||
"@types/tail": "^2.0.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.1",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export function hexToBuf(str: string): Buffer {
|
||||
return Buffer.from(str.replace(/0x/i, ''), 'hex');
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { expect } from 'chai';
|
||||
import fc from 'fast-check';
|
||||
import * as secp256k1 from 'secp256k1';
|
||||
|
||||
import { decode, encode } from './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 = encode(message, privKey);
|
||||
const res = decode(enc);
|
||||
|
||||
const pubKey = secp256k1.publicKeyCreate(privKey, false);
|
||||
|
||||
expect(res?.payload).deep.equal(message);
|
||||
expect(res?.sig?.publicKey).deep.equal(pubKey);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,148 @@
|
|||
import { Buffer } from 'buffer';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
import { keccak256 } from 'js-sha3';
|
||||
import * as secp256k1 from 'secp256k1';
|
||||
|
||||
import { hexToBuf } from '../utils';
|
||||
|
||||
const FlagsLength = 1;
|
||||
const FlagMask = 3; // 0011
|
||||
const IsSignedMask = 4; // 0100
|
||||
const PaddingTarget = 256;
|
||||
const SignatureLength = 65;
|
||||
|
||||
/**
|
||||
* Encode a Waku Message Payload using version 1. Payload get encrypted and
|
||||
* a signature may be included
|
||||
* @param messagePayload: The payload to include in the message
|
||||
* @param sigPrivKey: If set, a signature using this private key is added.
|
||||
*/
|
||||
|
||||
export function encode(
|
||||
messagePayload: Uint8Array,
|
||||
sigPrivKey?: Uint8Array
|
||||
): Uint8Array {
|
||||
let envelope = Buffer.from([0]); // No flags
|
||||
envelope = addPayloadSizeField(envelope, messagePayload);
|
||||
envelope = Buffer.concat([envelope, messagePayload]);
|
||||
|
||||
// Calculate padding:
|
||||
let rawSize =
|
||||
FlagsLength +
|
||||
getSizeOfPayloadSizeField(messagePayload) +
|
||||
messagePayload.length;
|
||||
|
||||
if (sigPrivKey) {
|
||||
rawSize += SignatureLength;
|
||||
}
|
||||
|
||||
const remainder = rawSize % PaddingTarget;
|
||||
const paddingSize = PaddingTarget - remainder;
|
||||
const pad = randomBytes(paddingSize);
|
||||
|
||||
if (!validateDataIntegrity(pad, paddingSize)) {
|
||||
throw new Error('failed to generate random padding of size ' + paddingSize);
|
||||
}
|
||||
|
||||
envelope = Buffer.concat([envelope, pad]);
|
||||
|
||||
if (sigPrivKey) {
|
||||
envelope[0] |= IsSignedMask;
|
||||
const hash = keccak256(envelope);
|
||||
const s = secp256k1.ecdsaSign(hexToBuf(hash), sigPrivKey);
|
||||
envelope = Buffer.concat([envelope, s.signature, Buffer.from([s.recid])]);
|
||||
}
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
export type DecodeResult = {
|
||||
payload: Uint8Array;
|
||||
sig?: {
|
||||
signature: Uint8Array;
|
||||
publicKey: Uint8Array;
|
||||
};
|
||||
};
|
||||
|
||||
export function decode(message: Uint8Array | Buffer): DecodeResult | undefined {
|
||||
const buf = Buffer.from(message);
|
||||
|
||||
let start = 1;
|
||||
let sig;
|
||||
|
||||
const sizeOfPayloadSizeField = buf.readUIntLE(0, 1) & FlagMask;
|
||||
|
||||
if (sizeOfPayloadSizeField === 0) return;
|
||||
|
||||
const payloadSize = buf.readUIntLE(start, sizeOfPayloadSizeField);
|
||||
start += sizeOfPayloadSizeField;
|
||||
const payload = buf.slice(start, start + payloadSize);
|
||||
|
||||
const isSigned = (buf.readUIntLE(0, 1) & IsSignedMask) == IsSignedMask;
|
||||
if (isSigned) {
|
||||
const signature = getSignature(buf);
|
||||
const hash = getHash(buf, isSigned);
|
||||
const publicKey = ecRecoverPubKey(hash, signature);
|
||||
sig = { signature, publicKey };
|
||||
}
|
||||
|
||||
return { payload, sig };
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the flags & auxiliary-field as per [26/WAKU-PAYLOAD](rfc.vac.dev/spec/26/).
|
||||
*/
|
||||
function addPayloadSizeField(msg: Buffer, payload: Uint8Array): Buffer {
|
||||
const fieldSize = getSizeOfPayloadSizeField(payload);
|
||||
let field = Buffer.alloc(4);
|
||||
field.writeUInt32LE(payload.length, 0);
|
||||
field = field.slice(0, fieldSize);
|
||||
msg = Buffer.concat([msg, field]);
|
||||
msg[0] |= fieldSize;
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the auxiliary-field which in turns contains the payload size
|
||||
*/
|
||||
function getSizeOfPayloadSizeField(payload: Uint8Array): number {
|
||||
let s = 1;
|
||||
for (let i = payload.length; i >= 256; i /= 256) {
|
||||
s++;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function validateDataIntegrity(value: Buffer, expectedSize: number): boolean {
|
||||
if (value.length !== expectedSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedSize > 3 && value.equals(Buffer.alloc(value.length))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getSignature(message: Buffer): Buffer {
|
||||
return message.slice(message.length - SignatureLength, message.length);
|
||||
}
|
||||
|
||||
function getHash(message: Buffer, isSigned: boolean): string {
|
||||
if (isSigned) {
|
||||
return keccak256(message.slice(0, message.length - SignatureLength));
|
||||
}
|
||||
return keccak256(message);
|
||||
}
|
||||
|
||||
function ecRecoverPubKey(messageHash: string, signature: Buffer): Uint8Array {
|
||||
const recovery = signature.slice(64).readIntBE(0, 1);
|
||||
return secp256k1.ecdsaRecover(
|
||||
signature.slice(0, 64),
|
||||
recovery,
|
||||
hexToBuf(messageHash),
|
||||
false
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue