@waku/message-encryption 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ecies.ts CHANGED
@@ -1,194 +1,168 @@
1
- import * as secp from "@noble/secp256k1";
2
- import { concat, hexToBytes } from "@waku/byte-utils";
3
-
4
- import { getSubtle, randomBytes, sha256 } from "./crypto.js";
5
- /**
6
- * HKDF as implemented in go-ethereum.
7
- */
8
- function kdf(secret: Uint8Array, outputLength: number): Promise<Uint8Array> {
9
- let ctr = 1;
10
- let written = 0;
11
- let willBeResult = Promise.resolve(new Uint8Array());
12
- while (written < outputLength) {
13
- const counters = new Uint8Array([ctr >> 24, ctr >> 16, ctr >> 8, ctr]);
14
- const countersSecret = concat(
15
- [counters, secret],
16
- counters.length + secret.length
17
- );
18
- const willBeHashResult = sha256(countersSecret);
19
- willBeResult = willBeResult.then((result) =>
20
- willBeHashResult.then((hashResult) => {
21
- const _hashResult = new Uint8Array(hashResult);
22
- return concat(
23
- [result, _hashResult],
24
- result.length + _hashResult.length
25
- );
26
- })
27
- );
28
- written += 32;
29
- ctr += 1;
1
+ import { Decoder as DecoderV0, proto } from "@waku/core/lib/message/version_0";
2
+ import type {
3
+ IDecoder,
4
+ IEncoder,
5
+ IMessage,
6
+ IProtoMessage,
7
+ } from "@waku/interfaces";
8
+ import debug from "debug";
9
+
10
+ import {
11
+ decryptAsymmetric,
12
+ encryptAsymmetric,
13
+ postCipher,
14
+ preCipher,
15
+ } from "./waku_payload.js";
16
+
17
+ import {
18
+ DecodedMessage,
19
+ generatePrivateKey,
20
+ getPublicKey,
21
+ OneMillion,
22
+ Version,
23
+ } from "./index.js";
24
+
25
+ export { DecodedMessage, generatePrivateKey, getPublicKey };
26
+
27
+ const log = debug("waku:message-encryption:ecies");
28
+
29
+ class Encoder implements IEncoder {
30
+ constructor(
31
+ public contentTopic: string,
32
+ private publicKey: Uint8Array,
33
+ private sigPrivKey?: Uint8Array,
34
+ public ephemeral: boolean = false
35
+ ) {}
36
+
37
+ async toWire(message: IMessage): Promise<Uint8Array | undefined> {
38
+ const protoMessage = await this.toProtoObj(message);
39
+ if (!protoMessage) return;
40
+
41
+ return proto.WakuMessage.encode(protoMessage);
30
42
  }
31
- return willBeResult;
32
- }
33
-
34
- function aesCtrEncrypt(
35
- counter: Uint8Array,
36
- key: ArrayBufferLike,
37
- data: ArrayBufferLike
38
- ): Promise<Uint8Array> {
39
- return getSubtle()
40
- .importKey("raw", key, "AES-CTR", false, ["encrypt"])
41
- .then((cryptoKey) =>
42
- getSubtle().encrypt(
43
- { name: "AES-CTR", counter: counter, length: 128 },
44
- cryptoKey,
45
- data
46
- )
47
- )
48
- .then((bytes) => new Uint8Array(bytes));
49
- }
50
-
51
- function aesCtrDecrypt(
52
- counter: Uint8Array,
53
- key: ArrayBufferLike,
54
- data: ArrayBufferLike
55
- ): Promise<Uint8Array> {
56
- return getSubtle()
57
- .importKey("raw", key, "AES-CTR", false, ["decrypt"])
58
- .then((cryptoKey) =>
59
- getSubtle().decrypt(
60
- { name: "AES-CTR", counter: counter, length: 128 },
61
- cryptoKey,
62
- data
63
- )
64
- )
65
- .then((bytes) => new Uint8Array(bytes));
66
- }
67
43
 
68
- function hmacSha256Sign(
69
- key: ArrayBufferLike,
70
- msg: ArrayBufferLike
71
- ): PromiseLike<Uint8Array> {
72
- const algorithm = { name: "HMAC", hash: { name: "SHA-256" } };
73
- return getSubtle()
74
- .importKey("raw", key, algorithm, false, ["sign"])
75
- .then((cryptoKey) => getSubtle().sign(algorithm, cryptoKey, msg))
76
- .then((bytes) => new Uint8Array(bytes));
77
- }
78
-
79
- function hmacSha256Verify(
80
- key: ArrayBufferLike,
81
- msg: ArrayBufferLike,
82
- sig: ArrayBufferLike
83
- ): Promise<boolean> {
84
- const algorithm = { name: "HMAC", hash: { name: "SHA-256" } };
85
- const _key = getSubtle().importKey("raw", key, algorithm, false, ["verify"]);
86
- return _key.then((cryptoKey) =>
87
- getSubtle().verify(algorithm, cryptoKey, sig, msg)
88
- );
44
+ async toProtoObj(message: IMessage): Promise<IProtoMessage | undefined> {
45
+ const timestamp = message.timestamp ?? new Date();
46
+ if (!message.payload) {
47
+ log("No payload to encrypt, skipping: ", message);
48
+ return;
49
+ }
50
+ const preparedPayload = await preCipher(message.payload, this.sigPrivKey);
51
+
52
+ const payload = await encryptAsymmetric(preparedPayload, this.publicKey);
53
+
54
+ return {
55
+ payload,
56
+ version: Version,
57
+ contentTopic: this.contentTopic,
58
+ timestamp: BigInt(timestamp.valueOf()) * OneMillion,
59
+ rateLimitProof: message.rateLimitProof,
60
+ ephemeral: this.ephemeral,
61
+ };
62
+ }
89
63
  }
90
64
 
91
65
  /**
92
- * Derive shared secret for given private and public keys.
66
+ * Creates an encoder that encrypts messages using ECIES for the given public,
67
+ * as defined in [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/).
68
+ *
69
+ * An encoder is used to encode messages in the [`14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/)
70
+ * format to be sent over the Waku network. The resulting encoder can then be
71
+ * pass to { @link @waku/interfaces.LightPush.push } or
72
+ * { @link @waku/interfaces.Relay.send } to automatically encrypt
73
+ * and encode outgoing messages.
93
74
  *
94
- * @param privateKeyA Sender's private key (32 bytes)
95
- * @param publicKeyB Recipient's public key (65 bytes)
96
- * @returns A promise that resolves with the derived shared secret (Px, 32 bytes)
97
- * @throws Error If arguments are invalid
75
+ * The payload can optionally be signed with the given private key as defined
76
+ * in [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/).
77
+ *
78
+ * @param contentTopic The content topic to set on outgoing messages.
79
+ * @param publicKey The public key to encrypt the payload for.
80
+ * @param sigPrivKey An optional private key to used to sign the payload before encryption.
81
+ * @param ephemeral An optional flag to mark message as ephemeral, ie, not to be stored by Waku Store nodes.
98
82
  */
99
- function derive(privateKeyA: Uint8Array, publicKeyB: Uint8Array): Uint8Array {
100
- if (privateKeyA.length !== 32) {
101
- throw new Error(
102
- `Bad private key, it should be 32 bytes but it's actually ${privateKeyA.length} bytes long`
103
- );
104
- } else if (publicKeyB.length !== 65) {
105
- throw new Error(
106
- `Bad public key, it should be 65 bytes but it's actually ${publicKeyB.length} bytes long`
107
- );
108
- } else if (publicKeyB[0] !== 4) {
109
- throw new Error("Bad public key, a valid public key would begin with 4");
110
- } else {
111
- const px = secp.getSharedSecret(privateKeyA, publicKeyB, true);
112
- // Remove the compression prefix
113
- return new Uint8Array(hexToBytes(px).slice(1));
114
- }
83
+ export function createEncoder(
84
+ contentTopic: string,
85
+ publicKey: Uint8Array,
86
+ sigPrivKey?: Uint8Array,
87
+ ephemeral = false
88
+ ): Encoder {
89
+ return new Encoder(contentTopic, publicKey, sigPrivKey, ephemeral);
115
90
  }
116
91
 
117
- /**
118
- * Encrypt message for given recipient's public key.
119
- *
120
- * @param publicKeyTo Recipient's public key (65 bytes)
121
- * @param msg The message being encrypted
122
- * @return A promise that resolves with the ECIES structure serialized
123
- */
124
- export async function encrypt(
125
- publicKeyTo: Uint8Array,
126
- msg: Uint8Array
127
- ): Promise<Uint8Array> {
128
- const ephemPrivateKey = randomBytes(32);
92
+ class Decoder extends DecoderV0 implements IDecoder<DecodedMessage> {
93
+ constructor(contentTopic: string, private privateKey: Uint8Array) {
94
+ super(contentTopic);
95
+ }
129
96
 
130
- const sharedPx = await derive(ephemPrivateKey, publicKeyTo);
97
+ async fromProtoObj(
98
+ protoMessage: IProtoMessage
99
+ ): Promise<DecodedMessage | undefined> {
100
+ const cipherPayload = protoMessage.payload;
101
+
102
+ if (protoMessage.version !== Version) {
103
+ log(
104
+ "Failed to decrypt due to incorrect version, expected:",
105
+ Version,
106
+ ", actual:",
107
+ protoMessage.version
108
+ );
109
+ return;
110
+ }
131
111
 
132
- const hash = await kdf(sharedPx, 32);
112
+ let payload;
113
+ if (!cipherPayload) {
114
+ log(`No payload to decrypt for contentTopic ${this.contentTopic}`);
115
+ return;
116
+ }
133
117
 
134
- const iv = randomBytes(16);
135
- const encryptionKey = hash.slice(0, 16);
136
- const cipherText = await aesCtrEncrypt(iv, encryptionKey, msg);
118
+ try {
119
+ payload = await decryptAsymmetric(cipherPayload, this.privateKey);
120
+ } catch (e) {
121
+ log(
122
+ `Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`,
123
+ e
124
+ );
125
+ return;
126
+ }
137
127
 
138
- const ivCipherText = concat([iv, cipherText], iv.length + cipherText.length);
128
+ if (!payload) {
129
+ log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`);
130
+ return;
131
+ }
139
132
 
140
- const macKey = await sha256(hash.slice(16));
141
- const hmac = await hmacSha256Sign(macKey, ivCipherText);
142
- const ephemPublicKey = secp.getPublicKey(ephemPrivateKey, false);
133
+ const res = await postCipher(payload);
143
134
 
144
- return concat(
145
- [ephemPublicKey, ivCipherText, hmac],
146
- ephemPublicKey.length + ivCipherText.length + hmac.length
147
- );
148
- }
135
+ if (!res) {
136
+ log(`Failed to decode payload for contentTopic ${this.contentTopic}`);
137
+ return;
138
+ }
149
139
 
150
- const metaLength = 1 + 64 + 16 + 32;
140
+ log("Message decrypted", protoMessage);
141
+ return new DecodedMessage(
142
+ protoMessage,
143
+ res.payload,
144
+ res.sig?.signature,
145
+ res.sig?.publicKey
146
+ );
147
+ }
148
+ }
151
149
 
152
150
  /**
153
- * Decrypt message using given private key.
151
+ * Creates a decoder that decrypts messages using ECIES, using the given private
152
+ * key as defined in [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/).
153
+ *
154
+ * A decoder is used to decode messages from the [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/)
155
+ * format when received from the Waku network. The resulting decoder can then be
156
+ * pass to { @link @waku/interfaces.Filter.subscribe } or
157
+ * { @link @waku/interfaces.Relay.subscribe } to automatically decrypt and
158
+ * decode incoming messages.
154
159
  *
155
- * @param privateKey A 32-byte private key of recipient of the message
156
- * @param encrypted ECIES serialized structure (result of ECIES encryption)
157
- * @returns The clear text
158
- * @throws Error If decryption fails
160
+ * @param contentTopic The resulting decoder will only decode messages with this content topic.
161
+ * @param privateKey The private key used to decrypt the message.
159
162
  */
160
- export async function decrypt(
161
- privateKey: Uint8Array,
162
- encrypted: Uint8Array
163
- ): Promise<Uint8Array> {
164
- if (encrypted.length <= metaLength) {
165
- throw new Error(
166
- `Invalid Ciphertext. Data is too small. It should ba at least ${metaLength} bytes`
167
- );
168
- } else if (encrypted[0] !== 4) {
169
- throw new Error(
170
- `Not a valid ciphertext. It should begin with 4 but actually begin with ${encrypted[0]}`
171
- );
172
- } else {
173
- // deserialize
174
- const ephemPublicKey = encrypted.slice(0, 65);
175
- const cipherTextLength = encrypted.length - metaLength;
176
- const iv = encrypted.slice(65, 65 + 16);
177
- const cipherAndIv = encrypted.slice(65, 65 + 16 + cipherTextLength);
178
- const ciphertext = cipherAndIv.slice(16);
179
- const msgMac = encrypted.slice(65 + 16 + cipherTextLength);
180
-
181
- // check HMAC
182
- const px = derive(privateKey, ephemPublicKey);
183
- const hash = await kdf(px, 32);
184
- const [encryptionKey, macKey] = await sha256(hash.slice(16)).then(
185
- (macKey) => [hash.slice(0, 16), macKey]
186
- );
187
-
188
- if (!(await hmacSha256Verify(macKey, cipherAndIv, msgMac))) {
189
- throw new Error("Incorrect MAC");
190
- }
191
-
192
- return aesCtrDecrypt(iv, encryptionKey, ciphertext);
193
- }
163
+ export function createDecoder(
164
+ contentTopic: string,
165
+ privateKey: Uint8Array
166
+ ): Decoder {
167
+ return new Decoder(contentTopic, privateKey);
194
168
  }