@zapo-js/fake-server 0.3.0 → 1.1.0
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/README.md +96 -8
- package/dist/api/AppStateSyncManager.d.ts +0 -1
- package/dist/api/AppStateSyncManager.js +0 -1
- package/dist/api/FakePairingDriver.d.ts +0 -1
- package/dist/api/FakePairingDriver.js +0 -1
- package/dist/api/FakePeer.d.ts +77 -4
- package/dist/api/FakePeer.js +208 -2
- package/dist/api/FakeServerSession.d.ts +67 -0
- package/dist/api/FakeServerSession.js +293 -0
- package/dist/api/FakeWaServer.d.ts +77 -31
- package/dist/api/FakeWaServer.js +140 -259
- package/dist/api/PreKeyDispenser.d.ts +0 -1
- package/dist/api/PreKeyDispenser.js +0 -1
- package/dist/api/Scenario.d.ts +0 -1
- package/dist/api/Scenario.js +0 -1
- package/dist/api/ServerRegistries.d.ts +0 -1
- package/dist/api/ServerRegistries.js +0 -1
- package/dist/api/iq-handlers.d.ts +1 -1
- package/dist/api/iq-handlers.js +23 -1
- package/dist/{cli.d.ts → bin.d.ts} +0 -1
- package/dist/bin.js +296 -0
- package/dist/esm/api/AppStateSyncManager.js +146 -0
- package/dist/esm/api/FakePairingDriver.js +31 -0
- package/dist/esm/api/FakePeer.js +610 -0
- package/dist/esm/api/FakeServerSession.js +287 -0
- package/dist/esm/api/FakeWaServer.js +528 -0
- package/dist/esm/api/PreKeyDispenser.js +93 -0
- package/dist/esm/api/Scenario.js +32 -0
- package/dist/esm/api/ServerRegistries.js +261 -0
- package/dist/esm/api/iq-handlers.js +515 -0
- package/dist/{cli.js → esm/bin.js} +70 -25
- package/dist/esm/index.js +26 -0
- package/dist/esm/infra/WaFakeConnection.js +49 -0
- package/dist/esm/infra/WaFakeConnectionPipeline.js +235 -0
- package/dist/esm/infra/WaFakeFrameSocket.js +117 -0
- package/dist/esm/infra/WaFakeMediaHttpsServer.js +126 -0
- package/dist/esm/infra/WaFakeNoiseHandshake.js +67 -0
- package/dist/esm/infra/WaFakeTransport.js +31 -0
- package/dist/esm/infra/WaFakeWsServer.js +96 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/protocol/auth/cert-chain.js +50 -0
- package/dist/esm/protocol/auth/client-payload-validate.js +55 -0
- package/dist/esm/protocol/auth/fake-primary-device.js +47 -0
- package/dist/esm/protocol/auth/pair-device.js +87 -0
- package/dist/esm/protocol/auth/success-node.js +26 -0
- package/dist/esm/protocol/iq/abprops.js +30 -0
- package/dist/esm/protocol/iq/appstate-sync.js +146 -0
- package/dist/esm/protocol/iq/business.js +59 -0
- package/dist/esm/protocol/iq/dirty-bits.js +19 -0
- package/dist/esm/protocol/iq/group-ops.js +154 -0
- package/dist/esm/protocol/iq/newsletter.js +14 -0
- package/dist/esm/protocol/iq/prekey-fetch.js +88 -0
- package/dist/esm/protocol/iq/privacy-token.js +26 -0
- package/dist/esm/protocol/iq/privacy.js +123 -0
- package/dist/esm/protocol/iq/profile.js +72 -0
- package/dist/esm/protocol/iq/router.js +117 -0
- package/dist/esm/protocol/iq/usync.js +64 -0
- package/dist/esm/protocol/push/app-state-key-share.js +26 -0
- package/dist/esm/protocol/push/call-failure.js +27 -0
- package/dist/esm/protocol/push/chatstate.js +23 -0
- package/dist/esm/protocol/push/error-stanza.js +13 -0
- package/dist/esm/protocol/push/history-sync.js +111 -0
- package/dist/esm/protocol/push/message.js +38 -0
- package/dist/esm/protocol/push/notification.js +29 -0
- package/dist/esm/protocol/push/presence.js +14 -0
- package/dist/esm/protocol/push/receipt.js +21 -0
- package/dist/esm/protocol/signal/fake-app-state-crypto.js +196 -0
- package/dist/esm/protocol/signal/fake-peer-double-ratchet.js +398 -0
- package/dist/esm/protocol/signal/fake-peer-group-recv-session.js +120 -0
- package/dist/esm/protocol/signal/fake-peer-identity.js +8 -0
- package/dist/esm/protocol/signal/fake-peer-key-bundle.js +25 -0
- package/dist/esm/protocol/signal/fake-sender-key.js +83 -0
- package/dist/esm/protocol/signal/prekey-upload.js +107 -0
- package/dist/esm/protocol/stream/stream-error.js +56 -0
- package/dist/esm/state/fake-app-state-collection.js +134 -0
- package/dist/esm/state/fake-media-store.js +53 -0
- package/dist/esm/transport/codec.js +8 -0
- package/dist/esm/transport/crypto.js +8 -0
- package/dist/esm/transport/protos.js +7 -0
- package/dist/esm/transport/util.js +8 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -2
- package/dist/infra/WaFakeConnection.d.ts +0 -1
- package/dist/infra/WaFakeConnection.js +0 -1
- package/dist/infra/WaFakeConnectionPipeline.d.ts +10 -3
- package/dist/infra/WaFakeConnectionPipeline.js +47 -32
- package/dist/infra/WaFakeFrameSocket.d.ts +0 -1
- package/dist/infra/WaFakeFrameSocket.js +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.d.ts +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.js +0 -1
- package/dist/infra/WaFakeNoiseHandshake.d.ts +8 -10
- package/dist/infra/WaFakeNoiseHandshake.js +19 -24
- package/dist/infra/WaFakeTransport.d.ts +2 -4
- package/dist/infra/WaFakeTransport.js +2 -1
- package/dist/infra/WaFakeWsServer.d.ts +0 -1
- package/dist/infra/WaFakeWsServer.js +0 -1
- package/dist/protocol/auth/cert-chain.d.ts +4 -1
- package/dist/protocol/auth/cert-chain.js +13 -3
- package/dist/protocol/auth/client-payload-validate.d.ts +0 -1
- package/dist/protocol/auth/client-payload-validate.js +0 -1
- package/dist/protocol/auth/fake-primary-device.d.ts +0 -1
- package/dist/protocol/auth/fake-primary-device.js +1 -3
- package/dist/protocol/auth/pair-device.d.ts +0 -1
- package/dist/protocol/auth/pair-device.js +4 -16
- package/dist/protocol/auth/success-node.d.ts +0 -1
- package/dist/protocol/auth/success-node.js +0 -1
- package/dist/protocol/iq/abprops.d.ts +0 -1
- package/dist/protocol/iq/abprops.js +0 -1
- package/dist/protocol/iq/appstate-sync.d.ts +0 -1
- package/dist/protocol/iq/appstate-sync.js +0 -1
- package/dist/protocol/iq/business.d.ts +0 -1
- package/dist/protocol/iq/business.js +0 -1
- package/dist/protocol/iq/dirty-bits.d.ts +0 -1
- package/dist/protocol/iq/dirty-bits.js +0 -1
- package/dist/protocol/iq/group-ops.d.ts +0 -1
- package/dist/protocol/iq/group-ops.js +0 -1
- package/dist/protocol/iq/newsletter.d.ts +0 -1
- package/dist/protocol/iq/newsletter.js +0 -1
- package/dist/protocol/iq/prekey-fetch.d.ts +0 -1
- package/dist/protocol/iq/prekey-fetch.js +0 -1
- package/dist/protocol/iq/privacy-token.d.ts +0 -1
- package/dist/protocol/iq/privacy-token.js +0 -1
- package/dist/protocol/iq/privacy.d.ts +0 -1
- package/dist/protocol/iq/privacy.js +15 -16
- package/dist/protocol/iq/profile.d.ts +0 -1
- package/dist/protocol/iq/profile.js +0 -1
- package/dist/protocol/iq/router.d.ts +7 -2
- package/dist/protocol/iq/router.js +8 -3
- package/dist/protocol/iq/usync.d.ts +0 -1
- package/dist/protocol/iq/usync.js +0 -1
- package/dist/protocol/push/app-state-key-share.d.ts +0 -1
- package/dist/protocol/push/app-state-key-share.js +0 -1
- package/dist/protocol/push/call-failure.d.ts +0 -1
- package/dist/protocol/push/call-failure.js +0 -1
- package/dist/protocol/push/chatstate.d.ts +0 -1
- package/dist/protocol/push/chatstate.js +0 -1
- package/dist/protocol/push/error-stanza.d.ts +0 -1
- package/dist/protocol/push/error-stanza.js +0 -1
- package/dist/protocol/push/history-sync.d.ts +0 -1
- package/dist/protocol/push/history-sync.js +0 -1
- package/dist/protocol/push/message.d.ts +0 -1
- package/dist/protocol/push/message.js +0 -1
- package/dist/protocol/push/notification.d.ts +0 -1
- package/dist/protocol/push/notification.js +0 -1
- package/dist/protocol/push/presence.d.ts +0 -1
- package/dist/protocol/push/presence.js +0 -1
- package/dist/protocol/push/receipt.d.ts +0 -1
- package/dist/protocol/push/receipt.js +0 -1
- package/dist/protocol/signal/fake-app-state-crypto.d.ts +7 -9
- package/dist/protocol/signal/fake-app-state-crypto.js +31 -33
- package/dist/protocol/signal/fake-peer-double-ratchet.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-double-ratchet.js +23 -28
- package/dist/protocol/signal/fake-peer-group-recv-session.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-group-recv-session.js +6 -10
- package/dist/protocol/signal/fake-peer-identity.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-identity.js +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.js +0 -1
- package/dist/protocol/signal/fake-sender-key.d.ts +0 -1
- package/dist/protocol/signal/fake-sender-key.js +5 -9
- package/dist/protocol/signal/prekey-upload.d.ts +0 -1
- package/dist/protocol/signal/prekey-upload.js +0 -1
- package/dist/protocol/stream/stream-error.d.ts +0 -1
- package/dist/protocol/stream/stream-error.js +0 -1
- package/dist/state/fake-app-state-collection.d.ts +0 -1
- package/dist/state/fake-app-state-collection.js +0 -1
- package/dist/state/fake-media-store.d.ts +0 -1
- package/dist/state/fake-media-store.js +2 -2
- package/dist/transport/codec.d.ts +2 -3
- package/dist/transport/codec.js +2 -3
- package/dist/transport/crypto.d.ts +5 -7
- package/dist/transport/crypto.js +6 -11
- package/dist/transport/protos.d.ts +2 -4
- package/dist/transport/protos.js +2 -4
- package/dist/transport/util.d.ts +8 -0
- package/dist/transport/util.js +16 -0
- package/package.json +26 -6
- package/dist/api/AppStateSyncManager.d.ts.map +0 -1
- package/dist/api/AppStateSyncManager.js.map +0 -1
- package/dist/api/FakePairingDriver.d.ts.map +0 -1
- package/dist/api/FakePairingDriver.js.map +0 -1
- package/dist/api/FakePeer.d.ts.map +0 -1
- package/dist/api/FakePeer.js.map +0 -1
- package/dist/api/FakeWaServer.d.ts.map +0 -1
- package/dist/api/FakeWaServer.js.map +0 -1
- package/dist/api/PreKeyDispenser.d.ts.map +0 -1
- package/dist/api/PreKeyDispenser.js.map +0 -1
- package/dist/api/Scenario.d.ts.map +0 -1
- package/dist/api/Scenario.js.map +0 -1
- package/dist/api/ServerRegistries.d.ts.map +0 -1
- package/dist/api/ServerRegistries.js.map +0 -1
- package/dist/api/iq-handlers.d.ts.map +0 -1
- package/dist/api/iq-handlers.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infra/WaFakeConnection.d.ts.map +0 -1
- package/dist/infra/WaFakeConnection.js.map +0 -1
- package/dist/infra/WaFakeConnectionPipeline.d.ts.map +0 -1
- package/dist/infra/WaFakeConnectionPipeline.js.map +0 -1
- package/dist/infra/WaFakeFrameSocket.d.ts.map +0 -1
- package/dist/infra/WaFakeFrameSocket.js.map +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.d.ts.map +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.js.map +0 -1
- package/dist/infra/WaFakeNoiseHandshake.d.ts.map +0 -1
- package/dist/infra/WaFakeNoiseHandshake.js.map +0 -1
- package/dist/infra/WaFakeTransport.d.ts.map +0 -1
- package/dist/infra/WaFakeTransport.js.map +0 -1
- package/dist/infra/WaFakeWsServer.d.ts.map +0 -1
- package/dist/infra/WaFakeWsServer.js.map +0 -1
- package/dist/protocol/auth/cert-chain.d.ts.map +0 -1
- package/dist/protocol/auth/cert-chain.js.map +0 -1
- package/dist/protocol/auth/client-payload-validate.d.ts.map +0 -1
- package/dist/protocol/auth/client-payload-validate.js.map +0 -1
- package/dist/protocol/auth/fake-primary-device.d.ts.map +0 -1
- package/dist/protocol/auth/fake-primary-device.js.map +0 -1
- package/dist/protocol/auth/pair-device.d.ts.map +0 -1
- package/dist/protocol/auth/pair-device.js.map +0 -1
- package/dist/protocol/auth/success-node.d.ts.map +0 -1
- package/dist/protocol/auth/success-node.js.map +0 -1
- package/dist/protocol/iq/abprops.d.ts.map +0 -1
- package/dist/protocol/iq/abprops.js.map +0 -1
- package/dist/protocol/iq/appstate-sync.d.ts.map +0 -1
- package/dist/protocol/iq/appstate-sync.js.map +0 -1
- package/dist/protocol/iq/business.d.ts.map +0 -1
- package/dist/protocol/iq/business.js.map +0 -1
- package/dist/protocol/iq/dirty-bits.d.ts.map +0 -1
- package/dist/protocol/iq/dirty-bits.js.map +0 -1
- package/dist/protocol/iq/group-ops.d.ts.map +0 -1
- package/dist/protocol/iq/group-ops.js.map +0 -1
- package/dist/protocol/iq/newsletter.d.ts.map +0 -1
- package/dist/protocol/iq/newsletter.js.map +0 -1
- package/dist/protocol/iq/prekey-fetch.d.ts.map +0 -1
- package/dist/protocol/iq/prekey-fetch.js.map +0 -1
- package/dist/protocol/iq/privacy-token.d.ts.map +0 -1
- package/dist/protocol/iq/privacy-token.js.map +0 -1
- package/dist/protocol/iq/privacy.d.ts.map +0 -1
- package/dist/protocol/iq/privacy.js.map +0 -1
- package/dist/protocol/iq/profile.d.ts.map +0 -1
- package/dist/protocol/iq/profile.js.map +0 -1
- package/dist/protocol/iq/router.d.ts.map +0 -1
- package/dist/protocol/iq/router.js.map +0 -1
- package/dist/protocol/iq/usync.d.ts.map +0 -1
- package/dist/protocol/iq/usync.js.map +0 -1
- package/dist/protocol/push/app-state-key-share.d.ts.map +0 -1
- package/dist/protocol/push/app-state-key-share.js.map +0 -1
- package/dist/protocol/push/call-failure.d.ts.map +0 -1
- package/dist/protocol/push/call-failure.js.map +0 -1
- package/dist/protocol/push/chatstate.d.ts.map +0 -1
- package/dist/protocol/push/chatstate.js.map +0 -1
- package/dist/protocol/push/error-stanza.d.ts.map +0 -1
- package/dist/protocol/push/error-stanza.js.map +0 -1
- package/dist/protocol/push/history-sync.d.ts.map +0 -1
- package/dist/protocol/push/history-sync.js.map +0 -1
- package/dist/protocol/push/message.d.ts.map +0 -1
- package/dist/protocol/push/message.js.map +0 -1
- package/dist/protocol/push/notification.d.ts.map +0 -1
- package/dist/protocol/push/notification.js.map +0 -1
- package/dist/protocol/push/presence.d.ts.map +0 -1
- package/dist/protocol/push/presence.js.map +0 -1
- package/dist/protocol/push/receipt.d.ts.map +0 -1
- package/dist/protocol/push/receipt.js.map +0 -1
- package/dist/protocol/signal/fake-app-state-crypto.d.ts.map +0 -1
- package/dist/protocol/signal/fake-app-state-crypto.js.map +0 -1
- package/dist/protocol/signal/fake-peer-double-ratchet.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-double-ratchet.js.map +0 -1
- package/dist/protocol/signal/fake-peer-group-recv-session.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-group-recv-session.js.map +0 -1
- package/dist/protocol/signal/fake-peer-identity.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-identity.js.map +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.js.map +0 -1
- package/dist/protocol/signal/fake-peer-recv-session.d.ts +0 -60
- package/dist/protocol/signal/fake-peer-recv-session.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-recv-session.js +0 -284
- package/dist/protocol/signal/fake-peer-recv-session.js.map +0 -1
- package/dist/protocol/signal/fake-peer-session.d.ts +0 -87
- package/dist/protocol/signal/fake-peer-session.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-session.js +0 -207
- package/dist/protocol/signal/fake-peer-session.js.map +0 -1
- package/dist/protocol/signal/fake-sender-key.d.ts.map +0 -1
- package/dist/protocol/signal/fake-sender-key.js.map +0 -1
- package/dist/protocol/signal/prekey-upload.d.ts.map +0 -1
- package/dist/protocol/signal/prekey-upload.js.map +0 -1
- package/dist/protocol/stream/stream-error.d.ts.map +0 -1
- package/dist/protocol/stream/stream-error.js.map +0 -1
- package/dist/state/fake-app-state-collection.d.ts.map +0 -1
- package/dist/state/fake-app-state-collection.js.map +0 -1
- package/dist/state/fake-media-store.d.ts.map +0 -1
- package/dist/state/fake-media-store.js.map +0 -1
- package/dist/transport/codec.d.ts.map +0 -1
- package/dist/transport/codec.js.map +0 -1
- package/dist/transport/crypto.d.ts.map +0 -1
- package/dist/transport/crypto.js.map +0 -1
- package/dist/transport/index.d.ts +0 -4
- package/dist/transport/index.d.ts.map +0 -1
- package/dist/transport/index.js +0 -20
- package/dist/transport/index.js.map +0 -1
- package/dist/transport/protos.d.ts.map +0 -1
- package/dist/transport/protos.js.map +0 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/** App-state crypto helpers mirrored from WhatsApp Web behavior. */
|
|
2
|
+
import { aesCbcDecrypt, aesCbcEncrypt, hkdf, hmacSha256Sign, hmacSha512Sign, randomBytesAsync } from '../../transport/crypto.js';
|
|
3
|
+
import { proto } from '../../transport/protos.js';
|
|
4
|
+
import { TEXT_ENCODER } from '../../transport/util.js';
|
|
5
|
+
const APP_STATE_DERIVED_KEY_LENGTH = 160;
|
|
6
|
+
const APP_STATE_DERIVED_INDEX_KEY_END = 32;
|
|
7
|
+
const APP_STATE_DERIVED_VALUE_ENCRYPTION_KEY_END = 64;
|
|
8
|
+
const APP_STATE_DERIVED_VALUE_MAC_KEY_END = 96;
|
|
9
|
+
const APP_STATE_DERIVED_SNAPSHOT_MAC_KEY_END = 128;
|
|
10
|
+
const APP_STATE_DERIVED_PATCH_MAC_KEY_END = 160;
|
|
11
|
+
const APP_STATE_VALUE_MAC_LENGTH = 32;
|
|
12
|
+
const APP_STATE_MAC_OCTET_LENGTH = 8;
|
|
13
|
+
const APP_STATE_IV_LENGTH = 16;
|
|
14
|
+
const APP_STATE_LT_HASH_SIZE = 128;
|
|
15
|
+
const APP_STATE_POINT_SIZE = 2;
|
|
16
|
+
const KDF_INFO_MUTATION_KEYS = TEXT_ENCODER.encode('WhatsApp Mutation Keys');
|
|
17
|
+
const KDF_INFO_PATCH_INTEGRITY = TEXT_ENCODER.encode('WhatsApp Patch Integrity');
|
|
18
|
+
export const APP_STATE_EMPTY_LT_HASH = new Uint8Array(APP_STATE_LT_HASH_SIZE);
|
|
19
|
+
export class FakeAppStateCrypto {
|
|
20
|
+
deriveKeys(keyData) {
|
|
21
|
+
const derived = hkdf(keyData, null, KDF_INFO_MUTATION_KEYS, APP_STATE_DERIVED_KEY_LENGTH);
|
|
22
|
+
return {
|
|
23
|
+
indexHmacKey: derived.subarray(0, APP_STATE_DERIVED_INDEX_KEY_END),
|
|
24
|
+
valueEncryptionAesKey: derived.subarray(APP_STATE_DERIVED_INDEX_KEY_END, APP_STATE_DERIVED_VALUE_ENCRYPTION_KEY_END),
|
|
25
|
+
valueMacHmacKey: derived.subarray(APP_STATE_DERIVED_VALUE_ENCRYPTION_KEY_END, APP_STATE_DERIVED_VALUE_MAC_KEY_END),
|
|
26
|
+
snapshotMacHmacKey: derived.subarray(APP_STATE_DERIVED_VALUE_MAC_KEY_END, APP_STATE_DERIVED_SNAPSHOT_MAC_KEY_END),
|
|
27
|
+
patchMacHmacKey: derived.subarray(APP_STATE_DERIVED_SNAPSHOT_MAC_KEY_END, APP_STATE_DERIVED_PATCH_MAC_KEY_END)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async encryptMutation(input) {
|
|
31
|
+
const derivedKeys = this.deriveKeys(input.keyData);
|
|
32
|
+
const indexBytes = new TextEncoder().encode(input.index);
|
|
33
|
+
const encoded = proto.SyncActionData.encode({
|
|
34
|
+
index: indexBytes,
|
|
35
|
+
value: input.value ?? undefined,
|
|
36
|
+
padding: new Uint8Array(0),
|
|
37
|
+
version: input.version
|
|
38
|
+
}).finish();
|
|
39
|
+
const iv = input.iv ?? (await randomBytesAsync(APP_STATE_IV_LENGTH));
|
|
40
|
+
if (iv.byteLength !== APP_STATE_IV_LENGTH) {
|
|
41
|
+
throw new Error(`invalid IV length ${iv.byteLength}`);
|
|
42
|
+
}
|
|
43
|
+
const indexMac = hmacSha256Sign(derivedKeys.indexHmacKey, indexBytes);
|
|
44
|
+
const cipherText = aesCbcEncrypt(derivedKeys.valueEncryptionAesKey, iv, encoded);
|
|
45
|
+
const cipherWithIv = concatBytes([iv, cipherText]);
|
|
46
|
+
const associatedData = generateAssociatedData(input.operation, input.keyId);
|
|
47
|
+
const valueMac = this.generateValueMac(derivedKeys.valueMacHmacKey, associatedData, cipherWithIv);
|
|
48
|
+
return {
|
|
49
|
+
indexMac,
|
|
50
|
+
valueBlob: concatBytes([cipherWithIv, valueMac]),
|
|
51
|
+
valueMac
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
55
|
+
async decryptMutation(input) {
|
|
56
|
+
if (input.valueBlob.byteLength < APP_STATE_IV_LENGTH + APP_STATE_VALUE_MAC_LENGTH) {
|
|
57
|
+
throw new Error('invalid mutation value blob');
|
|
58
|
+
}
|
|
59
|
+
const derivedKeys = this.deriveKeys(input.keyData);
|
|
60
|
+
const iv = input.valueBlob.subarray(0, APP_STATE_IV_LENGTH);
|
|
61
|
+
const macStart = input.valueBlob.byteLength - APP_STATE_VALUE_MAC_LENGTH;
|
|
62
|
+
const valueMac = input.valueBlob.subarray(macStart);
|
|
63
|
+
const cipherText = input.valueBlob.subarray(APP_STATE_IV_LENGTH, macStart);
|
|
64
|
+
const cipherWithIv = input.valueBlob.subarray(0, macStart);
|
|
65
|
+
const associatedData = generateAssociatedData(input.operation, input.keyId);
|
|
66
|
+
const expectedMac = this.generateValueMac(derivedKeys.valueMacHmacKey, associatedData, cipherWithIv);
|
|
67
|
+
if (!uint8Equal(expectedMac, valueMac)) {
|
|
68
|
+
throw new Error('mutation value MAC mismatch');
|
|
69
|
+
}
|
|
70
|
+
const plaintext = aesCbcDecrypt(derivedKeys.valueEncryptionAesKey, iv, cipherText);
|
|
71
|
+
const syncActionData = proto.SyncActionData.decode(plaintext);
|
|
72
|
+
if (!syncActionData.index) {
|
|
73
|
+
throw new Error('missing sync action index');
|
|
74
|
+
}
|
|
75
|
+
if (syncActionData.version === null || syncActionData.version === undefined) {
|
|
76
|
+
throw new Error('missing sync action version');
|
|
77
|
+
}
|
|
78
|
+
const generatedIndexMac = hmacSha256Sign(derivedKeys.indexHmacKey, syncActionData.index);
|
|
79
|
+
if (!uint8Equal(generatedIndexMac, input.indexMac)) {
|
|
80
|
+
throw new Error('mutation index MAC mismatch');
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
index: new TextDecoder().decode(syncActionData.index),
|
|
84
|
+
value: syncActionData.value ?? null,
|
|
85
|
+
version: syncActionData.version,
|
|
86
|
+
indexMac: input.indexMac,
|
|
87
|
+
valueMac
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
91
|
+
async generateSnapshotMac(keyData, ltHash, version, collectionName) {
|
|
92
|
+
const derivedKeys = this.deriveKeys(keyData);
|
|
93
|
+
const payload = concatBytes([
|
|
94
|
+
ltHash,
|
|
95
|
+
intToBytesBe(8, version),
|
|
96
|
+
new TextEncoder().encode(collectionName)
|
|
97
|
+
]);
|
|
98
|
+
return hmacSha256Sign(derivedKeys.snapshotMacHmacKey, payload);
|
|
99
|
+
}
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
101
|
+
async generatePatchMac(keyData, snapshotMac, valueMacs, version, collectionName) {
|
|
102
|
+
const derivedKeys = this.deriveKeys(keyData);
|
|
103
|
+
const payload = concatBytes([
|
|
104
|
+
snapshotMac,
|
|
105
|
+
...valueMacs,
|
|
106
|
+
intToBytesBe(8, version),
|
|
107
|
+
new TextEncoder().encode(collectionName)
|
|
108
|
+
]);
|
|
109
|
+
return hmacSha256Sign(derivedKeys.patchMacHmacKey, payload);
|
|
110
|
+
}
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
112
|
+
async ltHashAdd(base, addValues) {
|
|
113
|
+
return this.ltHashApply(base, addValues, (left, right) => left + right);
|
|
114
|
+
}
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
116
|
+
async ltHashSubtract(base, removeValues) {
|
|
117
|
+
return this.ltHashApply(base, removeValues, (left, right) => left - right);
|
|
118
|
+
}
|
|
119
|
+
ltHashApply(base, values, combine) {
|
|
120
|
+
if (values.length === 0) {
|
|
121
|
+
return base.slice();
|
|
122
|
+
}
|
|
123
|
+
const expandedValues = values.map((value) => hkdf(value, null, KDF_INFO_PATCH_INTEGRITY, APP_STATE_LT_HASH_SIZE));
|
|
124
|
+
const out = new Uint8Array(base.byteLength);
|
|
125
|
+
pointwise(base, expandedValues[0], combine, out);
|
|
126
|
+
for (let index = 1; index < expandedValues.length; index += 1) {
|
|
127
|
+
pointwise(out, expandedValues[index], combine, out);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
generateValueMac(valueMacHmacKey, associatedData, cipherWithIv) {
|
|
132
|
+
const octetLength = new Uint8Array(APP_STATE_MAC_OCTET_LENGTH);
|
|
133
|
+
octetLength[octetLength.length - 1] = associatedData.byteLength & 0xff;
|
|
134
|
+
const full = hmacSha512Sign(valueMacHmacKey, concatBytes([associatedData, cipherWithIv, octetLength]));
|
|
135
|
+
return full.subarray(0, APP_STATE_VALUE_MAC_LENGTH);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function generateAssociatedData(operation, keyId) {
|
|
139
|
+
// byte 0 is operation+1 (set=1, remove=2), then keyId bytes.
|
|
140
|
+
const opCode = operation === 'set' ? 1 : 2;
|
|
141
|
+
const out = new Uint8Array(1 + keyId.byteLength);
|
|
142
|
+
out[0] = opCode;
|
|
143
|
+
out.set(keyId, 1);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
function pointwise(left, right, combine, out) {
|
|
147
|
+
if (left.byteLength !== right.byteLength) {
|
|
148
|
+
throw new Error('lt hash input length mismatch');
|
|
149
|
+
}
|
|
150
|
+
if (left.byteLength % APP_STATE_POINT_SIZE !== 0) {
|
|
151
|
+
throw new Error('lt hash input alignment mismatch');
|
|
152
|
+
}
|
|
153
|
+
if (out.byteLength !== left.byteLength) {
|
|
154
|
+
throw new Error('lt hash output length mismatch');
|
|
155
|
+
}
|
|
156
|
+
const leftView = new DataView(left.buffer, left.byteOffset, left.byteLength);
|
|
157
|
+
const rightView = new DataView(right.buffer, right.byteOffset, right.byteLength);
|
|
158
|
+
const outView = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
159
|
+
for (let offset = 0; offset < left.byteLength; offset += APP_STATE_POINT_SIZE) {
|
|
160
|
+
const value = combine(leftView.getUint16(offset, true), rightView.getUint16(offset, true));
|
|
161
|
+
outView.setUint16(offset, value & 0xffff, true);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function intToBytesBe(byteLength, value) {
|
|
165
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
166
|
+
throw new Error(`invalid integer value: ${value}`);
|
|
167
|
+
}
|
|
168
|
+
const out = new Uint8Array(byteLength);
|
|
169
|
+
let current = value;
|
|
170
|
+
for (let i = byteLength - 1; i >= 0; i -= 1) {
|
|
171
|
+
out[i] = current & 0xff;
|
|
172
|
+
current = Math.floor(current / 256);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
function uint8Equal(a, b) {
|
|
177
|
+
if (a.byteLength !== b.byteLength)
|
|
178
|
+
return false;
|
|
179
|
+
let diff = 0;
|
|
180
|
+
for (let index = 0; index < a.byteLength; index += 1) {
|
|
181
|
+
diff |= a[index] ^ b[index];
|
|
182
|
+
}
|
|
183
|
+
return diff === 0;
|
|
184
|
+
}
|
|
185
|
+
function concatBytes(parts) {
|
|
186
|
+
let total = 0;
|
|
187
|
+
for (const part of parts)
|
|
188
|
+
total += part.byteLength;
|
|
189
|
+
const out = new Uint8Array(total);
|
|
190
|
+
let offset = 0;
|
|
191
|
+
for (const part of parts) {
|
|
192
|
+
out.set(part, offset);
|
|
193
|
+
offset += part.byteLength;
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/** Bidirectional Signal Double Ratchet used by fake peers. */
|
|
2
|
+
import { aesCbcDecrypt, aesCbcEncrypt, hkdf, hkdfSplit, hmacSha256Sign, prependVersion, toRawPubKey, toSerializedPubKey, X25519 } from '../../transport/crypto.js';
|
|
3
|
+
import { proto } from '../../transport/protos.js';
|
|
4
|
+
import { TEXT_ENCODER } from '../../transport/util.js';
|
|
5
|
+
const SIGNAL_VERSION = 3;
|
|
6
|
+
const SIGNAL_MAC_SIZE = 8;
|
|
7
|
+
const SIGNAL_PREFIX_FF = new Uint8Array(32).fill(0xff);
|
|
8
|
+
const MESSAGE_KEY_LABEL = new Uint8Array([1]);
|
|
9
|
+
const CHAIN_KEY_LABEL = new Uint8Array([2]);
|
|
10
|
+
const WHISPER_TEXT_INFO = TEXT_ENCODER.encode('WhisperText');
|
|
11
|
+
const WHISPER_RATCHET_INFO = TEXT_ENCODER.encode('WhisperRatchet');
|
|
12
|
+
const WHISPER_MESSAGE_KEYS_INFO = TEXT_ENCODER.encode('WhisperMessageKeys');
|
|
13
|
+
export class FakePeerDoubleRatchetError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'FakePeerDoubleRatchetError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const MAX_FUTURE_RECV_KEYS = 2000;
|
|
20
|
+
export class FakePeerDoubleRatchet {
|
|
21
|
+
constructor(keyBundle) {
|
|
22
|
+
this.remoteIdentityPubSerialized = null;
|
|
23
|
+
this.rootKey = null;
|
|
24
|
+
this.sendChain = null;
|
|
25
|
+
this.recvChain = null;
|
|
26
|
+
this.pendingPreKeyHeader = null;
|
|
27
|
+
this.bootstrapChainKey = null;
|
|
28
|
+
this.hasOutboundSendChain = false;
|
|
29
|
+
this.keyBundle = keyBundle;
|
|
30
|
+
this.localIdentityPubSerialized = toSerializedPubKey(keyBundle.identityKeyPair.pubKey);
|
|
31
|
+
}
|
|
32
|
+
get registrationId() {
|
|
33
|
+
return this.keyBundle.registrationId;
|
|
34
|
+
}
|
|
35
|
+
hasSendChain() {
|
|
36
|
+
return this.sendChain !== null && this.hasOutboundSendChain;
|
|
37
|
+
}
|
|
38
|
+
async initiateOutbound(bundle, options = {}) {
|
|
39
|
+
if (this.rootKey || this.sendChain || this.recvChain) {
|
|
40
|
+
throw new FakePeerDoubleRatchetError('session already initialized');
|
|
41
|
+
}
|
|
42
|
+
const remoteIdentity = toSerializedPubKey(bundle.identityKey);
|
|
43
|
+
const remoteSigned = toSerializedPubKey(bundle.signedPreKey.publicKey);
|
|
44
|
+
const localBase = await X25519.generateKeyPair();
|
|
45
|
+
const localBaseSerialized = toSerializedPubKey(localBase.pubKey);
|
|
46
|
+
// Do not fallback to bundle.preKeys[0], otherwise concurrent peers reuse one key id.
|
|
47
|
+
const consumedOneTime = options.skipOneTimePreKey ? undefined : options.oneTimePreKey;
|
|
48
|
+
const remoteOneTime = consumedOneTime ? toSerializedPubKey(consumedOneTime.publicKey) : null;
|
|
49
|
+
const [dh1, dh2, dh3, dh4] = await Promise.all([
|
|
50
|
+
X25519.scalarMult(this.keyBundle.identityKeyPair.privKey, toRawPubKey(remoteSigned)),
|
|
51
|
+
X25519.scalarMult(localBase.privKey, toRawPubKey(remoteIdentity)),
|
|
52
|
+
X25519.scalarMult(localBase.privKey, toRawPubKey(remoteSigned)),
|
|
53
|
+
remoteOneTime
|
|
54
|
+
? X25519.scalarMult(localBase.privKey, toRawPubKey(remoteOneTime))
|
|
55
|
+
: Promise.resolve(null)
|
|
56
|
+
]);
|
|
57
|
+
const sharedParts = [SIGNAL_PREFIX_FF, dh1, dh2, dh3];
|
|
58
|
+
if (dh4)
|
|
59
|
+
sharedParts.push(dh4);
|
|
60
|
+
const shared = concatBytes(sharedParts);
|
|
61
|
+
const [rootKey, _chainKey] = hkdfSplit(shared, null, WHISPER_TEXT_INFO);
|
|
62
|
+
void _chainKey;
|
|
63
|
+
const sendRatchet = await X25519.generateKeyPair();
|
|
64
|
+
const ratchetSecret = await X25519.scalarMult(sendRatchet.privKey, toRawPubKey(remoteSigned));
|
|
65
|
+
const [nextRootKey, sendChainKey] = hkdfSplit(ratchetSecret, rootKey, WHISPER_RATCHET_INFO);
|
|
66
|
+
this.rootKey = nextRootKey;
|
|
67
|
+
this.remoteIdentityPubSerialized = remoteIdentity;
|
|
68
|
+
this.sendChain = {
|
|
69
|
+
ratchetKeyPair: sendRatchet,
|
|
70
|
+
ratchetPubSerialized: toSerializedPubKey(sendRatchet.pubKey),
|
|
71
|
+
chainKey: sendChainKey,
|
|
72
|
+
nextIndex: 0
|
|
73
|
+
};
|
|
74
|
+
this.hasOutboundSendChain = true;
|
|
75
|
+
this.pendingPreKeyHeader = {
|
|
76
|
+
preKeyId: consumedOneTime?.keyId,
|
|
77
|
+
signedPreKeyId: bundle.signedPreKey.keyId,
|
|
78
|
+
baseKey: localBaseSerialized
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
82
|
+
async encrypt(plaintext) {
|
|
83
|
+
if (!this.sendChain) {
|
|
84
|
+
throw new FakePeerDoubleRatchetError('cannot encrypt: session has no send chain (call initiateOutbound first)');
|
|
85
|
+
}
|
|
86
|
+
if (!this.remoteIdentityPubSerialized) {
|
|
87
|
+
throw new FakePeerDoubleRatchetError('remote identity not set');
|
|
88
|
+
}
|
|
89
|
+
const { nextChainKey, messageKey } = deriveMessageKey(this.sendChain.nextIndex, this.sendChain.chainKey);
|
|
90
|
+
const ciphertext = aesCbcEncrypt(messageKey.cipherKey, messageKey.iv, plaintext);
|
|
91
|
+
const signalPayload = proto.SignalMessage.encode({
|
|
92
|
+
ratchetKey: this.sendChain.ratchetPubSerialized,
|
|
93
|
+
counter: messageKey.index,
|
|
94
|
+
previousCounter: 0,
|
|
95
|
+
ciphertext
|
|
96
|
+
}).finish();
|
|
97
|
+
const versioned = prependVersion(signalPayload, SIGNAL_VERSION);
|
|
98
|
+
const macInput = concatBytes([
|
|
99
|
+
this.localIdentityPubSerialized,
|
|
100
|
+
this.remoteIdentityPubSerialized,
|
|
101
|
+
versioned
|
|
102
|
+
]);
|
|
103
|
+
const fullMac = hmacSha256Sign(messageKey.macKey, macInput);
|
|
104
|
+
const mac = fullMac.subarray(0, SIGNAL_MAC_SIZE);
|
|
105
|
+
const inner = concatBytes([versioned, mac]);
|
|
106
|
+
this.sendChain = {
|
|
107
|
+
...this.sendChain,
|
|
108
|
+
chainKey: nextChainKey,
|
|
109
|
+
nextIndex: messageKey.index + 1
|
|
110
|
+
};
|
|
111
|
+
if (this.pendingPreKeyHeader) {
|
|
112
|
+
const header = this.pendingPreKeyHeader;
|
|
113
|
+
this.pendingPreKeyHeader = null;
|
|
114
|
+
const preKeyMessage = proto.PreKeySignalMessage.encode({
|
|
115
|
+
registrationId: this.keyBundle.registrationId,
|
|
116
|
+
preKeyId: header.preKeyId,
|
|
117
|
+
signedPreKeyId: header.signedPreKeyId,
|
|
118
|
+
baseKey: header.baseKey,
|
|
119
|
+
identityKey: this.localIdentityPubSerialized,
|
|
120
|
+
message: inner
|
|
121
|
+
}).finish();
|
|
122
|
+
return {
|
|
123
|
+
type: 'pkmsg',
|
|
124
|
+
ciphertext: prependVersion(preKeyMessage, SIGNAL_VERSION)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return { type: 'msg', ciphertext: inner };
|
|
128
|
+
}
|
|
129
|
+
async decryptPreKeyMessage(envelope) {
|
|
130
|
+
const body = readVersionedBody(envelope);
|
|
131
|
+
const preKeyMessage = proto.PreKeySignalMessage.decode(body);
|
|
132
|
+
const baseKey = requireBytes(preKeyMessage.baseKey, 'baseKey');
|
|
133
|
+
const remoteIdentity = requireBytes(preKeyMessage.identityKey, 'identityKey');
|
|
134
|
+
const innerMessage = requireBytes(preKeyMessage.message, 'message');
|
|
135
|
+
const signedPreKeyId = preKeyMessage.signedPreKeyId;
|
|
136
|
+
if (signedPreKeyId === null || signedPreKeyId === undefined) {
|
|
137
|
+
throw new FakePeerDoubleRatchetError('PreKeySignalMessage missing signedPreKeyId');
|
|
138
|
+
}
|
|
139
|
+
if (signedPreKeyId !== this.keyBundle.signedPreKey.id) {
|
|
140
|
+
throw new FakePeerDoubleRatchetError(`signedPreKeyId mismatch: got ${signedPreKeyId}, expected ${this.keyBundle.signedPreKey.id}`);
|
|
141
|
+
}
|
|
142
|
+
const oneTimePreKeyId = preKeyMessage.preKeyId;
|
|
143
|
+
let oneTimePrivKey = null;
|
|
144
|
+
if (oneTimePreKeyId !== null && oneTimePreKeyId !== undefined) {
|
|
145
|
+
const entry = this.keyBundle.oneTimePreKeys.find((k) => k.id === oneTimePreKeyId);
|
|
146
|
+
if (!entry) {
|
|
147
|
+
throw new FakePeerDoubleRatchetError(`unknown one-time preKeyId ${oneTimePreKeyId}`);
|
|
148
|
+
}
|
|
149
|
+
oneTimePrivKey = entry.keyPair.privKey;
|
|
150
|
+
}
|
|
151
|
+
const remoteIdentityRaw = toRawPubKey(remoteIdentity);
|
|
152
|
+
const baseKeyRaw = toRawPubKey(baseKey);
|
|
153
|
+
const signedPriv = this.keyBundle.signedPreKey.keyPair.privKey;
|
|
154
|
+
const identityPriv = this.keyBundle.identityKeyPair.privKey;
|
|
155
|
+
const [dh1, dh2, dh3, dh4] = await Promise.all([
|
|
156
|
+
X25519.scalarMult(signedPriv, remoteIdentityRaw),
|
|
157
|
+
X25519.scalarMult(identityPriv, baseKeyRaw),
|
|
158
|
+
X25519.scalarMult(signedPriv, baseKeyRaw),
|
|
159
|
+
oneTimePrivKey
|
|
160
|
+
? X25519.scalarMult(oneTimePrivKey, baseKeyRaw)
|
|
161
|
+
: Promise.resolve(null)
|
|
162
|
+
]);
|
|
163
|
+
const sharedParts = [SIGNAL_PREFIX_FF, dh1, dh2, dh3];
|
|
164
|
+
if (dh4)
|
|
165
|
+
sharedParts.push(dh4);
|
|
166
|
+
const shared = concatBytes(sharedParts);
|
|
167
|
+
const [rootKey, chainKey] = hkdfSplit(shared, null, WHISPER_TEXT_INFO);
|
|
168
|
+
this.remoteIdentityPubSerialized = toSerializedPubKey(remoteIdentity);
|
|
169
|
+
this.rootKey = rootKey;
|
|
170
|
+
this.bootstrapChainKey = chainKey;
|
|
171
|
+
return this.decryptInnerSignalMessage(innerMessage, /* isBootstrap */ true);
|
|
172
|
+
}
|
|
173
|
+
async decryptMessage(envelope) {
|
|
174
|
+
return this.decryptInnerSignalMessage(envelope, /* isBootstrap */ false);
|
|
175
|
+
}
|
|
176
|
+
async decryptInnerSignalMessage(signalMessageBytes, isBootstrap) {
|
|
177
|
+
if (signalMessageBytes.byteLength < 1 + SIGNAL_MAC_SIZE) {
|
|
178
|
+
throw new FakePeerDoubleRatchetError('signal message too short');
|
|
179
|
+
}
|
|
180
|
+
const versionByte = signalMessageBytes[0];
|
|
181
|
+
if (versionByte >>> 4 !== SIGNAL_VERSION) {
|
|
182
|
+
throw new FakePeerDoubleRatchetError(`unsupported signal version ${versionByte >>> 4}`);
|
|
183
|
+
}
|
|
184
|
+
const macStart = signalMessageBytes.byteLength - SIGNAL_MAC_SIZE;
|
|
185
|
+
const versionedBody = signalMessageBytes.subarray(0, macStart);
|
|
186
|
+
const macBytes = signalMessageBytes.subarray(macStart);
|
|
187
|
+
const protoBody = versionedBody.subarray(1);
|
|
188
|
+
const signalMessage = proto.SignalMessage.decode(protoBody);
|
|
189
|
+
const ratchetKey = requireBytes(signalMessage.ratchetKey, 'ratchetKey');
|
|
190
|
+
const counter = signalMessage.counter;
|
|
191
|
+
if (counter === null || counter === undefined) {
|
|
192
|
+
throw new FakePeerDoubleRatchetError('SignalMessage missing counter');
|
|
193
|
+
}
|
|
194
|
+
const ciphertext = requireBytes(signalMessage.ciphertext, 'ciphertext');
|
|
195
|
+
const ratchetSerialized = toSerializedPubKey(ratchetKey);
|
|
196
|
+
if (isBootstrap) {
|
|
197
|
+
if (!this.rootKey || !this.bootstrapChainKey) {
|
|
198
|
+
throw new FakePeerDoubleRatchetError('bootstrap state missing');
|
|
199
|
+
}
|
|
200
|
+
const ratchetShared = await X25519.scalarMult(this.keyBundle.signedPreKey.keyPair.privKey, toRawPubKey(ratchetKey));
|
|
201
|
+
const [nextRootKey, recvChainKey] = hkdfSplit(ratchetShared, this.rootKey, WHISPER_RATCHET_INFO);
|
|
202
|
+
this.rootKey = nextRootKey;
|
|
203
|
+
this.bootstrapChainKey = null;
|
|
204
|
+
this.recvChain = {
|
|
205
|
+
ratchetPubKey: ratchetSerialized,
|
|
206
|
+
chainKey: recvChainKey,
|
|
207
|
+
nextIndex: 0,
|
|
208
|
+
unusedKeys: []
|
|
209
|
+
};
|
|
210
|
+
await this.rotateSendRatchet(ratchetKey);
|
|
211
|
+
}
|
|
212
|
+
else if (!this.recvChain ||
|
|
213
|
+
!uint8Equal(this.recvChain.ratchetPubKey, ratchetSerialized)) {
|
|
214
|
+
await this.runRecvRatchetStep(ratchetKey);
|
|
215
|
+
}
|
|
216
|
+
if (!this.recvChain) {
|
|
217
|
+
throw new FakePeerDoubleRatchetError('recv chain not initialized');
|
|
218
|
+
}
|
|
219
|
+
let messageKey = null;
|
|
220
|
+
if (counter < this.recvChain.nextIndex) {
|
|
221
|
+
const stashedIndex = this.recvChain.unusedKeys.findIndex((entry) => entry.index === counter);
|
|
222
|
+
if (stashedIndex === -1) {
|
|
223
|
+
throw new FakePeerDoubleRatchetError(`recv counter ${counter} is stale (next=${this.recvChain.nextIndex})`);
|
|
224
|
+
}
|
|
225
|
+
const stashed = this.recvChain.unusedKeys[stashedIndex];
|
|
226
|
+
messageKey = {
|
|
227
|
+
cipherKey: stashed.cipherKey,
|
|
228
|
+
macKey: stashed.macKey,
|
|
229
|
+
iv: stashed.iv
|
|
230
|
+
};
|
|
231
|
+
this.recvChain = {
|
|
232
|
+
ratchetPubKey: this.recvChain.ratchetPubKey,
|
|
233
|
+
chainKey: this.recvChain.chainKey,
|
|
234
|
+
nextIndex: this.recvChain.nextIndex,
|
|
235
|
+
unusedKeys: this.recvChain.unusedKeys.filter((_, i) => i !== stashedIndex)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
const skipDistance = counter - this.recvChain.nextIndex;
|
|
240
|
+
if (skipDistance > MAX_FUTURE_RECV_KEYS) {
|
|
241
|
+
throw new FakePeerDoubleRatchetError(`recv counter ${counter} is too far in the future (skip=${skipDistance})`);
|
|
242
|
+
}
|
|
243
|
+
let chainKey = this.recvChain.chainKey;
|
|
244
|
+
const newlyStashed = [];
|
|
245
|
+
let walkIndex = this.recvChain.nextIndex;
|
|
246
|
+
while (walkIndex <= counter) {
|
|
247
|
+
const derived = deriveMessageKeyFromChain(chainKey);
|
|
248
|
+
chainKey = derived.nextChainKey;
|
|
249
|
+
if (walkIndex === counter) {
|
|
250
|
+
messageKey = derived.messageKey;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
newlyStashed.push({
|
|
254
|
+
index: walkIndex,
|
|
255
|
+
cipherKey: derived.messageKey.cipherKey,
|
|
256
|
+
macKey: derived.messageKey.macKey,
|
|
257
|
+
iv: derived.messageKey.iv
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
walkIndex += 1;
|
|
261
|
+
}
|
|
262
|
+
const allUnused = [...this.recvChain.unusedKeys, ...newlyStashed];
|
|
263
|
+
const trimmed = allUnused.length > MAX_FUTURE_RECV_KEYS
|
|
264
|
+
? allUnused.slice(allUnused.length - MAX_FUTURE_RECV_KEYS)
|
|
265
|
+
: allUnused;
|
|
266
|
+
this.recvChain = {
|
|
267
|
+
ratchetPubKey: this.recvChain.ratchetPubKey,
|
|
268
|
+
chainKey,
|
|
269
|
+
nextIndex: walkIndex,
|
|
270
|
+
unusedKeys: trimmed
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (!messageKey) {
|
|
274
|
+
throw new FakePeerDoubleRatchetError('failed to derive recv message key');
|
|
275
|
+
}
|
|
276
|
+
if (!this.remoteIdentityPubSerialized) {
|
|
277
|
+
throw new FakePeerDoubleRatchetError('remoteIdentityPub not set');
|
|
278
|
+
}
|
|
279
|
+
const macInput = concatBytes([
|
|
280
|
+
this.remoteIdentityPubSerialized,
|
|
281
|
+
this.localIdentityPubSerialized,
|
|
282
|
+
versionedBody
|
|
283
|
+
]);
|
|
284
|
+
const expectedFullMac = hmacSha256Sign(messageKey.macKey, macInput);
|
|
285
|
+
const expectedMac = expectedFullMac.subarray(0, SIGNAL_MAC_SIZE);
|
|
286
|
+
if (!uint8Equal(expectedMac, macBytes)) {
|
|
287
|
+
throw new FakePeerDoubleRatchetError('signal message MAC mismatch');
|
|
288
|
+
}
|
|
289
|
+
const padded = aesCbcDecrypt(messageKey.cipherKey, messageKey.iv, ciphertext);
|
|
290
|
+
return unpadPkcs7(padded);
|
|
291
|
+
}
|
|
292
|
+
async runRecvRatchetStep(remoteRatchetPub) {
|
|
293
|
+
if (!this.rootKey) {
|
|
294
|
+
throw new FakePeerDoubleRatchetError('cannot run recv DR step: no root key');
|
|
295
|
+
}
|
|
296
|
+
if (!this.sendChain) {
|
|
297
|
+
throw new FakePeerDoubleRatchetError('cannot run recv DR step: no current local ratchet (alice has not initiated)');
|
|
298
|
+
}
|
|
299
|
+
const remoteRatchetRaw = toRawPubKey(remoteRatchetPub);
|
|
300
|
+
const ratchetShared = await X25519.scalarMult(this.sendChain.ratchetKeyPair.privKey, remoteRatchetRaw);
|
|
301
|
+
const [nextRootKey, recvChainKey] = hkdfSplit(ratchetShared, this.rootKey, WHISPER_RATCHET_INFO);
|
|
302
|
+
this.rootKey = nextRootKey;
|
|
303
|
+
this.recvChain = {
|
|
304
|
+
ratchetPubKey: toSerializedPubKey(remoteRatchetPub),
|
|
305
|
+
chainKey: recvChainKey,
|
|
306
|
+
nextIndex: 0,
|
|
307
|
+
unusedKeys: []
|
|
308
|
+
};
|
|
309
|
+
await this.rotateSendRatchet(remoteRatchetPub);
|
|
310
|
+
}
|
|
311
|
+
async rotateSendRatchet(remoteRatchetPub) {
|
|
312
|
+
if (!this.rootKey) {
|
|
313
|
+
throw new FakePeerDoubleRatchetError('cannot rotate send ratchet: no root key');
|
|
314
|
+
}
|
|
315
|
+
const newRatchet = await X25519.generateKeyPair();
|
|
316
|
+
const ratchetShared = await X25519.scalarMult(newRatchet.privKey, toRawPubKey(remoteRatchetPub));
|
|
317
|
+
const [nextRootKey, sendChainKey] = hkdfSplit(ratchetShared, this.rootKey, WHISPER_RATCHET_INFO);
|
|
318
|
+
this.rootKey = nextRootKey;
|
|
319
|
+
this.sendChain = {
|
|
320
|
+
ratchetKeyPair: newRatchet,
|
|
321
|
+
ratchetPubSerialized: toSerializedPubKey(newRatchet.pubKey),
|
|
322
|
+
chainKey: sendChainKey,
|
|
323
|
+
nextIndex: 0
|
|
324
|
+
};
|
|
325
|
+
this.hasOutboundSendChain = true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function deriveMessageKey(index, chainKey) {
|
|
329
|
+
const messageInputKey = hmacSha256Sign(chainKey, MESSAGE_KEY_LABEL);
|
|
330
|
+
const nextChainRaw = hmacSha256Sign(chainKey, CHAIN_KEY_LABEL);
|
|
331
|
+
const expanded = hkdf(messageInputKey, null, WHISPER_MESSAGE_KEYS_INFO, 80);
|
|
332
|
+
return {
|
|
333
|
+
nextChainKey: nextChainRaw.subarray(0, 32),
|
|
334
|
+
messageKey: {
|
|
335
|
+
index,
|
|
336
|
+
cipherKey: expanded.subarray(0, 32),
|
|
337
|
+
macKey: expanded.subarray(32, 64),
|
|
338
|
+
iv: expanded.subarray(64, 80)
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function deriveMessageKeyFromChain(chainKey) {
|
|
343
|
+
const result = deriveMessageKey(0, chainKey);
|
|
344
|
+
return {
|
|
345
|
+
nextChainKey: result.nextChainKey,
|
|
346
|
+
messageKey: {
|
|
347
|
+
cipherKey: result.messageKey.cipherKey,
|
|
348
|
+
macKey: result.messageKey.macKey,
|
|
349
|
+
iv: result.messageKey.iv
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function readVersionedBody(envelope) {
|
|
354
|
+
if (envelope.byteLength < 1) {
|
|
355
|
+
throw new FakePeerDoubleRatchetError('signal envelope is empty');
|
|
356
|
+
}
|
|
357
|
+
const version = envelope[0] >>> 4;
|
|
358
|
+
if (version !== SIGNAL_VERSION) {
|
|
359
|
+
throw new FakePeerDoubleRatchetError(`unsupported signal version ${version}`);
|
|
360
|
+
}
|
|
361
|
+
return envelope.subarray(1);
|
|
362
|
+
}
|
|
363
|
+
function unpadPkcs7(padded) {
|
|
364
|
+
if (padded.byteLength === 0)
|
|
365
|
+
return padded;
|
|
366
|
+
const padLen = padded[padded.byteLength - 1];
|
|
367
|
+
if (padLen === 0 || padLen > 16)
|
|
368
|
+
return padded;
|
|
369
|
+
if (padLen > padded.byteLength)
|
|
370
|
+
return padded;
|
|
371
|
+
return padded.subarray(0, padded.byteLength - padLen);
|
|
372
|
+
}
|
|
373
|
+
function uint8Equal(a, b) {
|
|
374
|
+
if (a.byteLength !== b.byteLength)
|
|
375
|
+
return false;
|
|
376
|
+
for (let i = 0; i < a.byteLength; i += 1) {
|
|
377
|
+
if (a[i] !== b[i])
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
function concatBytes(parts) {
|
|
383
|
+
let total = 0;
|
|
384
|
+
for (const p of parts)
|
|
385
|
+
total += p.byteLength;
|
|
386
|
+
const out = new Uint8Array(total);
|
|
387
|
+
let offset = 0;
|
|
388
|
+
for (const p of parts) {
|
|
389
|
+
out.set(p, offset);
|
|
390
|
+
offset += p.byteLength;
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
function requireBytes(value, label) {
|
|
395
|
+
if (!value)
|
|
396
|
+
throw new FakePeerDoubleRatchetError(`${label} missing`);
|
|
397
|
+
return value;
|
|
398
|
+
}
|