@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,120 @@
|
|
|
1
|
+
/** Receiver-side SenderKey session for group `skmsg` decrypt. */
|
|
2
|
+
import { aesCbcDecrypt, hkdf, hmacSha256Sign, toRawPubKey, toSerializedPubKey, xeddsaVerify } from '../../transport/crypto.js';
|
|
3
|
+
import { proto } from '../../transport/protos.js';
|
|
4
|
+
import { TEXT_ENCODER } from '../../transport/util.js';
|
|
5
|
+
const SIGNAL_GROUP_VERSION = 3;
|
|
6
|
+
const SIGNAL_SIGNATURE_LENGTH = 64;
|
|
7
|
+
const MESSAGE_KEY_LABEL = new Uint8Array([1]);
|
|
8
|
+
const CHAIN_KEY_LABEL = new Uint8Array([2]);
|
|
9
|
+
const WHISPER_GROUP_INFO = TEXT_ENCODER.encode('WhisperGroup');
|
|
10
|
+
export class FakePeerGroupRecvSessionError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'FakePeerGroupRecvSessionError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class FakePeerGroupRecvSession {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.records = new Map();
|
|
19
|
+
}
|
|
20
|
+
addDistribution(groupId, senderJid, axolotlBytes) {
|
|
21
|
+
if (axolotlBytes.byteLength < 1) {
|
|
22
|
+
throw new FakePeerGroupRecvSessionError('SKDM payload empty');
|
|
23
|
+
}
|
|
24
|
+
const version = axolotlBytes[0] >>> 4;
|
|
25
|
+
if (version !== SIGNAL_GROUP_VERSION) {
|
|
26
|
+
throw new FakePeerGroupRecvSessionError(`unsupported SKDM version ${version}`);
|
|
27
|
+
}
|
|
28
|
+
const body = axolotlBytes.subarray(1);
|
|
29
|
+
const decoded = proto.SenderKeyDistributionMessage.decode(body);
|
|
30
|
+
if (decoded.id === null ||
|
|
31
|
+
decoded.id === undefined ||
|
|
32
|
+
decoded.iteration === null ||
|
|
33
|
+
decoded.iteration === undefined ||
|
|
34
|
+
!decoded.chainKey ||
|
|
35
|
+
!decoded.signingKey) {
|
|
36
|
+
throw new FakePeerGroupRecvSessionError('invalid SKDM');
|
|
37
|
+
}
|
|
38
|
+
if (decoded.chainKey.byteLength !== 32) {
|
|
39
|
+
throw new FakePeerGroupRecvSessionError(`SKDM chainKey must be 32 bytes, got ${decoded.chainKey.byteLength}`);
|
|
40
|
+
}
|
|
41
|
+
this.records.set(recordKey(groupId, senderJid), {
|
|
42
|
+
keyId: decoded.id,
|
|
43
|
+
nextIteration: decoded.iteration,
|
|
44
|
+
chainKey: decoded.chainKey,
|
|
45
|
+
signingPublicKey: toSerializedPubKey(decoded.signingKey)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async decryptGroupMessage(groupId, senderJid, skmsgBytes) {
|
|
49
|
+
const record = this.records.get(recordKey(groupId, senderJid));
|
|
50
|
+
if (!record) {
|
|
51
|
+
throw new FakePeerGroupRecvSessionError(`no senderkey state for group=${groupId} sender=${senderJid}`);
|
|
52
|
+
}
|
|
53
|
+
if (skmsgBytes.byteLength < 1 + SIGNAL_SIGNATURE_LENGTH) {
|
|
54
|
+
throw new FakePeerGroupRecvSessionError('skmsg too short');
|
|
55
|
+
}
|
|
56
|
+
const versionByte = skmsgBytes[0];
|
|
57
|
+
if (versionByte >>> 4 !== SIGNAL_GROUP_VERSION) {
|
|
58
|
+
throw new FakePeerGroupRecvSessionError(`unsupported skmsg version ${versionByte >>> 4}`);
|
|
59
|
+
}
|
|
60
|
+
const sigStart = skmsgBytes.byteLength - SIGNAL_SIGNATURE_LENGTH;
|
|
61
|
+
// xeddsaVerify mutates the signature buffer; isolate with copies.
|
|
62
|
+
const versionedContent = skmsgBytes.subarray(0, sigStart).slice();
|
|
63
|
+
const signature = skmsgBytes.subarray(sigStart).slice();
|
|
64
|
+
const protoBody = versionedContent.subarray(1);
|
|
65
|
+
const decoded = proto.SenderKeyMessage.decode(protoBody);
|
|
66
|
+
if (decoded.id === null ||
|
|
67
|
+
decoded.id === undefined ||
|
|
68
|
+
decoded.iteration === null ||
|
|
69
|
+
decoded.iteration === undefined ||
|
|
70
|
+
!decoded.ciphertext) {
|
|
71
|
+
throw new FakePeerGroupRecvSessionError('invalid SenderKeyMessage');
|
|
72
|
+
}
|
|
73
|
+
if (decoded.id !== record.keyId) {
|
|
74
|
+
throw new FakePeerGroupRecvSessionError(`senderKey id mismatch: got ${decoded.id}, expected ${record.keyId}`);
|
|
75
|
+
}
|
|
76
|
+
const validSignature = await xeddsaVerify(toRawPubKey(record.signingPublicKey), versionedContent, signature);
|
|
77
|
+
if (!validSignature) {
|
|
78
|
+
throw new FakePeerGroupRecvSessionError('invalid sender key signature');
|
|
79
|
+
}
|
|
80
|
+
const targetIteration = decoded.iteration;
|
|
81
|
+
if (targetIteration < record.nextIteration) {
|
|
82
|
+
throw new FakePeerGroupRecvSessionError(`out-of-order skmsg iteration ${targetIteration} < ${record.nextIteration}`);
|
|
83
|
+
}
|
|
84
|
+
let chainKey = record.chainKey;
|
|
85
|
+
let seed = null;
|
|
86
|
+
let iteration = record.nextIteration;
|
|
87
|
+
while (iteration <= targetIteration) {
|
|
88
|
+
const nextChainRaw = hmacSha256Sign(chainKey, CHAIN_KEY_LABEL);
|
|
89
|
+
const messageInputKey = hmacSha256Sign(chainKey, MESSAGE_KEY_LABEL);
|
|
90
|
+
const messageSeed = hkdf(messageInputKey, null, WHISPER_GROUP_INFO, 50);
|
|
91
|
+
if (iteration === targetIteration) {
|
|
92
|
+
seed = messageSeed;
|
|
93
|
+
}
|
|
94
|
+
chainKey = nextChainRaw.subarray(0, 32);
|
|
95
|
+
iteration += 1;
|
|
96
|
+
}
|
|
97
|
+
if (!seed) {
|
|
98
|
+
throw new FakePeerGroupRecvSessionError('failed to derive message seed');
|
|
99
|
+
}
|
|
100
|
+
record.chainKey = chainKey;
|
|
101
|
+
record.nextIteration = iteration;
|
|
102
|
+
const iv = seed.subarray(0, 16);
|
|
103
|
+
const keyBytes = seed.subarray(16, 48);
|
|
104
|
+
const padded = aesCbcDecrypt(keyBytes, iv, decoded.ciphertext);
|
|
105
|
+
return unpadPkcs7(padded);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function recordKey(groupId, senderJid) {
|
|
109
|
+
return `${groupId}\u0000${senderJid}`;
|
|
110
|
+
}
|
|
111
|
+
function unpadPkcs7(padded) {
|
|
112
|
+
if (padded.byteLength === 0)
|
|
113
|
+
return padded;
|
|
114
|
+
const padLen = padded[padded.byteLength - 1];
|
|
115
|
+
if (padLen === 0 || padLen > 16)
|
|
116
|
+
return padded;
|
|
117
|
+
if (padLen > padded.byteLength)
|
|
118
|
+
return padded;
|
|
119
|
+
return padded.subarray(0, padded.byteLength - padLen);
|
|
120
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Generates Signal prekey bundles for fake peers. */
|
|
2
|
+
import { toSerializedPubKey, X25519, xeddsaSign } from '../../transport/crypto.js';
|
|
3
|
+
export async function generateFakePeerKeyBundle(options = {}) {
|
|
4
|
+
const identityKeyPair = options.identityKeyPair ?? (await X25519.generateKeyPair());
|
|
5
|
+
const signedPreKeyKeyPair = await X25519.generateKeyPair();
|
|
6
|
+
const signedPreKeyId = options.signedPreKeyId ?? 1;
|
|
7
|
+
const signedPreKeySerialized = toSerializedPubKey(signedPreKeyKeyPair.pubKey);
|
|
8
|
+
const signedPreKeySignature = await xeddsaSign(identityKeyPair.privKey, signedPreKeySerialized);
|
|
9
|
+
const oneTimeCount = options.oneTimePreKeyCount ?? 4;
|
|
10
|
+
const firstId = options.firstOneTimePreKeyId ?? 1;
|
|
11
|
+
const oneTimePreKeys = await Promise.all(Array.from({ length: oneTimeCount }, async (_, idx) => ({
|
|
12
|
+
id: firstId + idx,
|
|
13
|
+
keyPair: await X25519.generateKeyPair()
|
|
14
|
+
})));
|
|
15
|
+
return {
|
|
16
|
+
registrationId: options.registrationId ?? Math.floor(Math.random() * 0x3fff) + 1,
|
|
17
|
+
identityKeyPair,
|
|
18
|
+
signedPreKey: {
|
|
19
|
+
id: signedPreKeyId,
|
|
20
|
+
keyPair: signedPreKeyKeyPair,
|
|
21
|
+
signature: signedPreKeySignature
|
|
22
|
+
},
|
|
23
|
+
oneTimePreKeys
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** SenderKey state used by fake peers for group encryption. */
|
|
2
|
+
import { aesCbcEncrypt, hkdf, hmacSha256Sign, prependVersion, randomBytesAsync, toSerializedPubKey, X25519, xeddsaSign } from '../../transport/crypto.js';
|
|
3
|
+
import { proto } from '../../transport/protos.js';
|
|
4
|
+
const SIGNAL_GROUP_VERSION = 3;
|
|
5
|
+
const WHISPER_GROUP_INFO = new TextEncoder().encode('WhisperGroup');
|
|
6
|
+
const MESSAGE_KEY_LABEL = new Uint8Array([1]);
|
|
7
|
+
const CHAIN_KEY_LABEL = new Uint8Array([2]);
|
|
8
|
+
export class FakeSenderKey {
|
|
9
|
+
constructor(state) {
|
|
10
|
+
this.state = state;
|
|
11
|
+
}
|
|
12
|
+
static async generate() {
|
|
13
|
+
const [chainKey, signingKeyPair, idBytes] = await Promise.all([
|
|
14
|
+
randomBytesAsync(32),
|
|
15
|
+
X25519.generateKeyPair(),
|
|
16
|
+
randomBytesAsync(4)
|
|
17
|
+
]);
|
|
18
|
+
const id = ((idBytes[0] << 24) | (idBytes[1] << 16) | (idBytes[2] << 8) | idBytes[3]) >>> 0;
|
|
19
|
+
return new FakeSenderKey({
|
|
20
|
+
id,
|
|
21
|
+
chainKey,
|
|
22
|
+
iteration: 0,
|
|
23
|
+
signingKeyPair
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
get id() {
|
|
27
|
+
return this.state.id;
|
|
28
|
+
}
|
|
29
|
+
async encrypt(plaintext) {
|
|
30
|
+
const distributionMessage = this.buildDistributionMessage();
|
|
31
|
+
const { messageKey, nextChainKey } = await deriveSenderKeyMessageKey(this.state.iteration, this.state.chainKey);
|
|
32
|
+
const { iv, cipherKey } = splitSenderKeyMessageSeed(messageKey.seed);
|
|
33
|
+
const ciphertext = aesCbcEncrypt(cipherKey, iv, plaintext);
|
|
34
|
+
const senderKeyMessage = proto.SenderKeyMessage.encode({
|
|
35
|
+
id: this.state.id,
|
|
36
|
+
iteration: messageKey.iteration,
|
|
37
|
+
ciphertext
|
|
38
|
+
}).finish();
|
|
39
|
+
const versioned = prependVersion(senderKeyMessage, SIGNAL_GROUP_VERSION);
|
|
40
|
+
const signature = await xeddsaSign(this.state.signingKeyPair.privKey, versioned);
|
|
41
|
+
const finalCiphertext = new Uint8Array(versioned.byteLength + signature.byteLength);
|
|
42
|
+
finalCiphertext.set(versioned, 0);
|
|
43
|
+
finalCiphertext.set(signature, versioned.byteLength);
|
|
44
|
+
this.state = {
|
|
45
|
+
...this.state,
|
|
46
|
+
chainKey: nextChainKey,
|
|
47
|
+
iteration: messageKey.iteration + 1
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
ciphertext: finalCiphertext,
|
|
51
|
+
distributionMessage,
|
|
52
|
+
keyId: this.state.id
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
buildDistributionMessage() {
|
|
56
|
+
const proto1 = proto.SenderKeyDistributionMessage.encode({
|
|
57
|
+
id: this.state.id,
|
|
58
|
+
iteration: this.state.iteration,
|
|
59
|
+
chainKey: this.state.chainKey,
|
|
60
|
+
signingKey: toSerializedPubKey(this.state.signingKeyPair.pubKey)
|
|
61
|
+
}).finish();
|
|
62
|
+
return prependVersion(proto1, SIGNAL_GROUP_VERSION);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
66
|
+
async function deriveSenderKeyMessageKey(iteration, chainKey) {
|
|
67
|
+
const messageInputKey = hmacSha256Sign(chainKey, MESSAGE_KEY_LABEL);
|
|
68
|
+
const nextChainRaw = hmacSha256Sign(chainKey, CHAIN_KEY_LABEL);
|
|
69
|
+
const seed = hkdf(messageInputKey, null, WHISPER_GROUP_INFO, 50);
|
|
70
|
+
return {
|
|
71
|
+
messageKey: { iteration, seed },
|
|
72
|
+
nextChainKey: nextChainRaw.subarray(0, 32)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function splitSenderKeyMessageSeed(seed) {
|
|
76
|
+
if (seed.byteLength < 48) {
|
|
77
|
+
throw new Error(`sender key message seed too short: ${seed.byteLength}`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
iv: seed.subarray(0, 16),
|
|
81
|
+
cipherKey: seed.subarray(16, 48)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Parser for client prekey upload IQs (`xmlns=encrypt`, `type=set`). */
|
|
2
|
+
export class PreKeyUploadParseError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'PreKeyUploadParseError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function parsePreKeyUploadIq(iq) {
|
|
9
|
+
if (iq.tag !== 'iq') {
|
|
10
|
+
throw new PreKeyUploadParseError(`expected iq stanza, got ${iq.tag}`);
|
|
11
|
+
}
|
|
12
|
+
if (iq.attrs.xmlns !== 'encrypt' || iq.attrs.type !== 'set') {
|
|
13
|
+
throw new PreKeyUploadParseError(`expected iq with xmlns=encrypt type=set, got xmlns=${iq.attrs.xmlns} type=${iq.attrs.type}`);
|
|
14
|
+
}
|
|
15
|
+
const children = iq.content;
|
|
16
|
+
if (!Array.isArray(children)) {
|
|
17
|
+
throw new PreKeyUploadParseError('iq has no children');
|
|
18
|
+
}
|
|
19
|
+
const registrationNode = findChild(children, 'registration');
|
|
20
|
+
const identityNode = findChild(children, 'identity');
|
|
21
|
+
const listNode = findChild(children, 'list');
|
|
22
|
+
const skeyNode = findChild(children, 'skey');
|
|
23
|
+
const registrationId = readBigEndianInt(requireBytes(registrationNode?.content, 'registration'));
|
|
24
|
+
const identityKey = requireBytes(identityNode?.content, 'identity');
|
|
25
|
+
if (identityKey.byteLength !== 32) {
|
|
26
|
+
throw new PreKeyUploadParseError(`identity key must be 32 bytes, got ${identityKey.byteLength}`);
|
|
27
|
+
}
|
|
28
|
+
const signedPreKey = parseSignedPreKey(skeyNode);
|
|
29
|
+
const preKeys = parsePreKeyList(listNode);
|
|
30
|
+
return {
|
|
31
|
+
registrationId,
|
|
32
|
+
identityKey,
|
|
33
|
+
signedPreKey,
|
|
34
|
+
preKeys
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function findChild(children, tag) {
|
|
38
|
+
for (const child of children) {
|
|
39
|
+
if (child.tag === tag)
|
|
40
|
+
return child;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function parseSignedPreKey(node) {
|
|
45
|
+
if (!node) {
|
|
46
|
+
throw new PreKeyUploadParseError('skey child missing');
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(node.content)) {
|
|
49
|
+
throw new PreKeyUploadParseError('skey has no inner nodes');
|
|
50
|
+
}
|
|
51
|
+
const idNode = findChild(node.content, 'id');
|
|
52
|
+
const valueNode = findChild(node.content, 'value');
|
|
53
|
+
const signatureNode = findChild(node.content, 'signature');
|
|
54
|
+
const keyId = readBigEndianInt(requireBytes(idNode?.content, 'skey.id'));
|
|
55
|
+
const publicKey = requireBytes(valueNode?.content, 'skey.value');
|
|
56
|
+
const signature = requireBytes(signatureNode?.content, 'skey.signature');
|
|
57
|
+
if (publicKey.byteLength !== 32) {
|
|
58
|
+
throw new PreKeyUploadParseError(`skey value must be 32 bytes, got ${publicKey.byteLength}`);
|
|
59
|
+
}
|
|
60
|
+
if (signature.byteLength !== 64) {
|
|
61
|
+
throw new PreKeyUploadParseError(`skey signature must be 64 bytes, got ${signature.byteLength}`);
|
|
62
|
+
}
|
|
63
|
+
return { keyId, publicKey, signature };
|
|
64
|
+
}
|
|
65
|
+
function parsePreKeyList(node) {
|
|
66
|
+
if (!node) {
|
|
67
|
+
throw new PreKeyUploadParseError('list child missing');
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(node.content)) {
|
|
70
|
+
throw new PreKeyUploadParseError('list has no inner key entries');
|
|
71
|
+
}
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const entry of node.content) {
|
|
74
|
+
if (entry.tag !== 'key')
|
|
75
|
+
continue;
|
|
76
|
+
if (!Array.isArray(entry.content))
|
|
77
|
+
continue;
|
|
78
|
+
const idNode = findChild(entry.content, 'id');
|
|
79
|
+
const valueNode = findChild(entry.content, 'value');
|
|
80
|
+
const keyId = readBigEndianInt(requireBytes(idNode?.content, 'key.id'));
|
|
81
|
+
const publicKey = requireBytes(valueNode?.content, 'key.value');
|
|
82
|
+
if (publicKey.byteLength !== 32) {
|
|
83
|
+
throw new PreKeyUploadParseError(`key value must be 32 bytes, got ${publicKey.byteLength}`);
|
|
84
|
+
}
|
|
85
|
+
out.push({ keyId, publicKey });
|
|
86
|
+
}
|
|
87
|
+
if (out.length === 0) {
|
|
88
|
+
throw new PreKeyUploadParseError('list is empty');
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function requireBytes(content, label) {
|
|
93
|
+
if (content === undefined || content === null) {
|
|
94
|
+
throw new PreKeyUploadParseError(`${label} child has no content`);
|
|
95
|
+
}
|
|
96
|
+
if (!(content instanceof Uint8Array)) {
|
|
97
|
+
throw new PreKeyUploadParseError(`${label} content must be bytes`);
|
|
98
|
+
}
|
|
99
|
+
return content;
|
|
100
|
+
}
|
|
101
|
+
function readBigEndianInt(bytes) {
|
|
102
|
+
let value = 0;
|
|
103
|
+
for (let i = 0; i < bytes.byteLength; i += 1) {
|
|
104
|
+
value = value * 256 + bytes[i];
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Builders for `<stream:error/>` stanzas pushed to the client. */
|
|
2
|
+
const STREAM_ERROR_TAG = 'stream:error';
|
|
3
|
+
export function buildStreamErrorCode(code) {
|
|
4
|
+
return {
|
|
5
|
+
tag: STREAM_ERROR_TAG,
|
|
6
|
+
attrs: { code: String(code) }
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function buildStreamErrorReplaced() {
|
|
10
|
+
return {
|
|
11
|
+
tag: STREAM_ERROR_TAG,
|
|
12
|
+
attrs: {},
|
|
13
|
+
content: [
|
|
14
|
+
{
|
|
15
|
+
tag: 'conflict',
|
|
16
|
+
attrs: { type: 'replaced' }
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function buildStreamErrorDeviceRemoved() {
|
|
22
|
+
return {
|
|
23
|
+
tag: STREAM_ERROR_TAG,
|
|
24
|
+
attrs: {},
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
tag: 'conflict',
|
|
28
|
+
attrs: { type: 'device_removed' }
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function buildStreamErrorAck(id) {
|
|
34
|
+
return {
|
|
35
|
+
tag: STREAM_ERROR_TAG,
|
|
36
|
+
attrs: {},
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
tag: 'ack',
|
|
40
|
+
attrs: { id }
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function buildStreamErrorXmlNotWellFormed() {
|
|
46
|
+
return {
|
|
47
|
+
tag: STREAM_ERROR_TAG,
|
|
48
|
+
attrs: {},
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
tag: 'xml-not-well-formed',
|
|
52
|
+
attrs: {}
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/** In-memory app-state collection tracker (version/LTHash + patch queue). */
|
|
2
|
+
import { APP_STATE_EMPTY_LT_HASH, FakeAppStateCrypto } from '../protocol/signal/fake-app-state-crypto.js';
|
|
3
|
+
import { proto } from '../transport/protos.js';
|
|
4
|
+
export class FakeAppStateCollection {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.currentVersion = 0;
|
|
7
|
+
this.currentLtHash = APP_STATE_EMPTY_LT_HASH;
|
|
8
|
+
this.recordsByIndexMacHex = new Map();
|
|
9
|
+
this.pendingPatchMutations = [];
|
|
10
|
+
this.hasUnflushedSnapshotChanges = false;
|
|
11
|
+
this.name = options.name;
|
|
12
|
+
this.keyId = options.keyId;
|
|
13
|
+
this.keyData = options.keyData;
|
|
14
|
+
this.crypto = options.crypto ?? new FakeAppStateCrypto();
|
|
15
|
+
}
|
|
16
|
+
get version() {
|
|
17
|
+
return this.currentVersion;
|
|
18
|
+
}
|
|
19
|
+
get ltHash() {
|
|
20
|
+
return this.currentLtHash;
|
|
21
|
+
}
|
|
22
|
+
async applyMutation(input) {
|
|
23
|
+
const baseInput = {
|
|
24
|
+
operation: input.operation,
|
|
25
|
+
keyId: this.keyId,
|
|
26
|
+
keyData: this.keyData,
|
|
27
|
+
index: input.index,
|
|
28
|
+
value: input.value,
|
|
29
|
+
version: input.version
|
|
30
|
+
};
|
|
31
|
+
const encrypted = await this.crypto.encryptMutation(baseInput);
|
|
32
|
+
const indexMacHex = bytesToHex(encrypted.indexMac);
|
|
33
|
+
const existing = this.recordsByIndexMacHex.get(indexMacHex);
|
|
34
|
+
const removeValues = [];
|
|
35
|
+
const addValues = [];
|
|
36
|
+
if (existing) {
|
|
37
|
+
removeValues.push(existing.valueMac);
|
|
38
|
+
this.recordsByIndexMacHex.delete(indexMacHex);
|
|
39
|
+
}
|
|
40
|
+
if (input.operation === 'set') {
|
|
41
|
+
const record = {
|
|
42
|
+
indexMacHex,
|
|
43
|
+
indexMac: encrypted.indexMac,
|
|
44
|
+
valueMac: encrypted.valueMac,
|
|
45
|
+
valueBlob: encrypted.valueBlob
|
|
46
|
+
};
|
|
47
|
+
this.recordsByIndexMacHex.set(indexMacHex, record);
|
|
48
|
+
addValues.push(encrypted.valueMac);
|
|
49
|
+
this.pendingPatchMutations.push({ operation: 'set', record });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.pendingPatchMutations.push({
|
|
53
|
+
operation: 'remove',
|
|
54
|
+
record: {
|
|
55
|
+
indexMacHex,
|
|
56
|
+
indexMac: encrypted.indexMac,
|
|
57
|
+
valueMac: encrypted.valueMac,
|
|
58
|
+
valueBlob: encrypted.valueBlob
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const nextHash = await this.applyLtHash(this.currentLtHash, addValues, removeValues);
|
|
63
|
+
this.currentLtHash = nextHash;
|
|
64
|
+
this.hasUnflushedSnapshotChanges = true;
|
|
65
|
+
}
|
|
66
|
+
async encodeSnapshot() {
|
|
67
|
+
this.currentVersion += 1;
|
|
68
|
+
const records = [];
|
|
69
|
+
for (const record of this.recordsByIndexMacHex.values()) {
|
|
70
|
+
records.push({
|
|
71
|
+
index: { blob: record.indexMac },
|
|
72
|
+
value: { blob: record.valueBlob },
|
|
73
|
+
keyId: { id: this.keyId }
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const mac = await this.crypto.generateSnapshotMac(this.keyData, this.currentLtHash, this.currentVersion, this.name);
|
|
77
|
+
this.pendingPatchMutations.length = 0;
|
|
78
|
+
this.hasUnflushedSnapshotChanges = false;
|
|
79
|
+
return proto.SyncdSnapshot.encode({
|
|
80
|
+
version: { version: this.currentVersion },
|
|
81
|
+
records,
|
|
82
|
+
mac,
|
|
83
|
+
keyId: { id: this.keyId }
|
|
84
|
+
}).finish();
|
|
85
|
+
}
|
|
86
|
+
async encodePendingPatch() {
|
|
87
|
+
if (this.pendingPatchMutations.length === 0) {
|
|
88
|
+
throw new Error(`fake app-state collection ${this.name}: no pending mutations to encode`);
|
|
89
|
+
}
|
|
90
|
+
this.currentVersion += 1;
|
|
91
|
+
const patchVersion = this.currentVersion;
|
|
92
|
+
const mutations = [];
|
|
93
|
+
const valueMacs = [];
|
|
94
|
+
for (const pending of this.pendingPatchMutations) {
|
|
95
|
+
mutations.push({
|
|
96
|
+
operation: pending.operation === 'set'
|
|
97
|
+
? proto.SyncdMutation.SyncdOperation.SET
|
|
98
|
+
: proto.SyncdMutation.SyncdOperation.REMOVE,
|
|
99
|
+
record: {
|
|
100
|
+
keyId: { id: this.keyId },
|
|
101
|
+
index: { blob: pending.record.indexMac },
|
|
102
|
+
value: { blob: pending.record.valueBlob }
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
valueMacs.push(pending.record.valueMac);
|
|
106
|
+
}
|
|
107
|
+
const snapshotMac = await this.crypto.generateSnapshotMac(this.keyData, this.currentLtHash, patchVersion, this.name);
|
|
108
|
+
const patchMac = await this.crypto.generatePatchMac(this.keyData, snapshotMac, valueMacs, patchVersion, this.name);
|
|
109
|
+
this.pendingPatchMutations.length = 0;
|
|
110
|
+
this.hasUnflushedSnapshotChanges = false;
|
|
111
|
+
return proto.SyncdPatch.encode({
|
|
112
|
+
version: { version: patchVersion },
|
|
113
|
+
mutations,
|
|
114
|
+
snapshotMac,
|
|
115
|
+
patchMac,
|
|
116
|
+
keyId: { id: this.keyId }
|
|
117
|
+
}).finish();
|
|
118
|
+
}
|
|
119
|
+
hasPending() {
|
|
120
|
+
return this.pendingPatchMutations.length > 0 || this.hasUnflushedSnapshotChanges;
|
|
121
|
+
}
|
|
122
|
+
async applyLtHash(base, addValues, removeValues) {
|
|
123
|
+
const afterRemove = await this.crypto.ltHashSubtract(base, removeValues);
|
|
124
|
+
return this.crypto.ltHashAdd(afterRemove, addValues);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function bytesToHex(bytes) {
|
|
128
|
+
let out = '';
|
|
129
|
+
for (let index = 0; index < bytes.byteLength; index += 1) {
|
|
130
|
+
const value = bytes[index];
|
|
131
|
+
out += value < 16 ? `0${value.toString(16)}` : value.toString(16);
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** In-memory media blob store served by the fake media endpoint. */
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { WaMediaCrypto } from '../transport/crypto.js';
|
|
4
|
+
import { bytesToHex } from '../transport/util.js';
|
|
5
|
+
export class FakeMediaStore {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.blobs = new Map();
|
|
8
|
+
}
|
|
9
|
+
async publish(input) {
|
|
10
|
+
const mediaKey = input.mediaKey ?? new Uint8Array(randomBytes(32));
|
|
11
|
+
const encrypted = await WaMediaCrypto.encryptBytes(input.mediaType, mediaKey, input.plaintext);
|
|
12
|
+
const path = input.path ?? this.randomPath(input.mediaType);
|
|
13
|
+
const stored = {
|
|
14
|
+
mediaType: input.mediaType,
|
|
15
|
+
encryptedBytes: encrypted.ciphertextHmac,
|
|
16
|
+
mediaKey,
|
|
17
|
+
fileSha256: encrypted.fileSha256,
|
|
18
|
+
fileEncSha256: encrypted.fileEncSha256
|
|
19
|
+
};
|
|
20
|
+
this.blobs.set(path, stored);
|
|
21
|
+
return {
|
|
22
|
+
path,
|
|
23
|
+
mediaKey,
|
|
24
|
+
fileSha256: encrypted.fileSha256,
|
|
25
|
+
fileEncSha256: encrypted.fileEncSha256,
|
|
26
|
+
fileLength: encrypted.ciphertextHmac.byteLength,
|
|
27
|
+
mediaType: input.mediaType
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Stores pre-encrypted bytes uploaded by the lib so the download path can serve them back. */
|
|
31
|
+
setRaw(path, encryptedBytes, mediaType = 'image') {
|
|
32
|
+
this.blobs.set(path, {
|
|
33
|
+
mediaType,
|
|
34
|
+
encryptedBytes,
|
|
35
|
+
mediaKey: new Uint8Array(32),
|
|
36
|
+
fileSha256: new Uint8Array(32),
|
|
37
|
+
fileEncSha256: new Uint8Array(32)
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
get(path) {
|
|
41
|
+
return this.blobs.get(path);
|
|
42
|
+
}
|
|
43
|
+
delete(path) {
|
|
44
|
+
return this.blobs.delete(path);
|
|
45
|
+
}
|
|
46
|
+
clear() {
|
|
47
|
+
this.blobs.clear();
|
|
48
|
+
}
|
|
49
|
+
randomPath(mediaType) {
|
|
50
|
+
const slug = bytesToHex(randomBytes(16));
|
|
51
|
+
return `/fake-media/${mediaType}/${slug}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 1 – binary stanza codec wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the bit-exact WhatsApp WAP token encoder/decoder from zapo-js.
|
|
5
|
+
* This file is the only sanctioned import path for binary node serialization
|
|
6
|
+
* inside the fake server.
|
|
7
|
+
*/
|
|
8
|
+
export { decodeBinaryNode, decodeBinaryNodeStanza, encodeBinaryNode, encodeBinaryNodeStanza, verifyNoiseCertificateChain } from 'zapo-js/transport';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 1 – crypto primitives wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports bit-exact cryptographic primitives from zapo-js. No protocol
|
|
5
|
+
* interpretation, so reusing them does not create a test-vs-impl tautology.
|
|
6
|
+
*/
|
|
7
|
+
export { aesCbcDecrypt, aesCbcEncrypt, aesGcmDecrypt, aesGcmEncrypt, Ed25519, hkdf, hkdfSplit, hmacSha256Sign, hmacSha512Sign, prependVersion, randomBytesAsync, readVersionedContent, sha256, sha512, toRawPubKey, toSerializedPubKey, X25519, xeddsaSign, xeddsaVerify } from 'zapo-js/crypto';
|
|
8
|
+
export { WaMediaCrypto } from 'zapo-js/media';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 1 – byte/encoding primitives wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports bit-exact byte helpers from zapo-js so Layer 2/3 of the fake
|
|
5
|
+
* server can use them without importing zapo-js directly.
|
|
6
|
+
*/
|
|
7
|
+
export { bytesToBase64, bytesToBase64UrlSafe, bytesToHex, decodeBase64Url, toError } from 'zapo-js/util';
|
|
8
|
+
export const TEXT_ENCODER = new TextEncoder();
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ export { FakePairingDriver } from './api/FakePairingDriver';
|
|
|
2
2
|
export type { CompanionPairingMaterial, FakePairingDriverDeps, FakePairingDriverOptions } from './api/FakePairingDriver';
|
|
3
3
|
export { FakePeer } from './api/FakePeer';
|
|
4
4
|
export type { CreateFakePeerOptions, ExpectGroupMessageOptions, ExpectMessageOptions, ReceivedMessage, SendMessageOptions } from './api/FakePeer';
|
|
5
|
-
export { FakeWaServer } from './api/FakeWaServer';
|
|
6
|
-
export type { BinaryNode, ExpectIqOptions, ExpectStanzaOptions, FakeWaServerNoiseRootCa, FakeWaServerOptions, FakeWaServerPipelineListener, StanzaMatcher, WaFakeAuthenticatedInfo, WaFakeConnectionPipeline } from './api/FakeWaServer';
|
|
5
|
+
export { FakeServerSession, FakeWaServer } from './api/FakeWaServer';
|
|
6
|
+
export type { BinaryNode, ExpectIqOptions, ExpectStanzaOptions, FakeSessionKeyInfo, FakeWaServerNoiseRootCa, FakeWaServerOptions, FakeWaServerPipelineListener, StanzaMatcher, WaFakeAuthenticatedInfo, WaFakeConnectionPipeline } from './api/FakeWaServer';
|
|
7
7
|
export { IqExpectation, Scenario } from './api/Scenario';
|
|
8
8
|
export type { AuthenticatedPipelineListener, ScenarioServer } from './api/Scenario';
|
|
9
9
|
export { WaFakeConnection } from './infra/WaFakeConnection';
|
|
@@ -48,4 +48,3 @@ export type { BuildPairDeviceIqInput, BuildPairSuccessIqInput, ParsedPairingQr }
|
|
|
48
48
|
export { parsePreKeyUploadIq, PreKeyUploadParseError } from './protocol/signal/prekey-upload';
|
|
49
49
|
export type { ClientPreKeyBundle, ClientPreKeyEntry, ClientSignedPreKey } from './protocol/signal/prekey-upload';
|
|
50
50
|
export { buildStreamErrorAck, buildStreamErrorCode, buildStreamErrorDeviceRemoved, buildStreamErrorReplaced, buildStreamErrorXmlNotWellFormed } from './protocol/stream/stream-error';
|
|
51
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildStreamErrorXmlNotWellFormed = exports.buildStreamErrorReplaced = exports.buildStreamErrorDeviceRemoved = exports.buildStreamErrorCode = exports.buildStreamErrorAck = exports.PreKeyUploadParseError = exports.parsePreKeyUploadIq = exports.parsePairingQrString = exports.buildPairSuccessIq = exports.buildPairDeviceIq = exports.generateFakePrimaryDevice = exports.buildAdvSignedDeviceIdentity = exports.FakeSenderKey = exports.FakePeerDoubleRatchet = exports.generateFakePeerIdentity = exports.buildReceipt = exports.buildIncomingPresence = exports.buildNotification = exports.buildGroupNotification = exports.buildMessage = exports.FakeMediaStore = exports.FakeAppStateCollection = exports.FakeAppStateCrypto = exports.APP_STATE_EMPTY_LT_HASH = exports.encodeHistorySyncPlaintext = exports.buildHistorySyncMessage = exports.buildHistorySyncExternalMessage = exports.buildAppStateSyncKeyShareMessage = exports.buildIncomingErrorStanza = exports.buildChatstate = exports.buildFailure = exports.buildCall = exports.buildIqResult = exports.buildIqError = exports.parseAppStateSyncRequest = exports.buildServerSyncNotification = exports.buildAppStateSyncResult = exports.buildAppStateSyncFullResult = exports.WaFakeConnection = exports.Scenario = exports.IqExpectation = exports.FakeWaServer = exports.FakePeer = exports.FakePairingDriver = void 0;
|
|
3
|
+
exports.buildStreamErrorXmlNotWellFormed = exports.buildStreamErrorReplaced = exports.buildStreamErrorDeviceRemoved = exports.buildStreamErrorCode = exports.buildStreamErrorAck = exports.PreKeyUploadParseError = exports.parsePreKeyUploadIq = exports.parsePairingQrString = exports.buildPairSuccessIq = exports.buildPairDeviceIq = exports.generateFakePrimaryDevice = exports.buildAdvSignedDeviceIdentity = exports.FakeSenderKey = exports.FakePeerDoubleRatchet = exports.generateFakePeerIdentity = exports.buildReceipt = exports.buildIncomingPresence = exports.buildNotification = exports.buildGroupNotification = exports.buildMessage = exports.FakeMediaStore = exports.FakeAppStateCollection = exports.FakeAppStateCrypto = exports.APP_STATE_EMPTY_LT_HASH = exports.encodeHistorySyncPlaintext = exports.buildHistorySyncMessage = exports.buildHistorySyncExternalMessage = exports.buildAppStateSyncKeyShareMessage = exports.buildIncomingErrorStanza = exports.buildChatstate = exports.buildFailure = exports.buildCall = exports.buildIqResult = exports.buildIqError = exports.parseAppStateSyncRequest = exports.buildServerSyncNotification = exports.buildAppStateSyncResult = exports.buildAppStateSyncFullResult = exports.WaFakeConnection = exports.Scenario = exports.IqExpectation = exports.FakeWaServer = exports.FakeServerSession = exports.FakePeer = exports.FakePairingDriver = void 0;
|
|
4
4
|
var FakePairingDriver_1 = require("./api/FakePairingDriver");
|
|
5
5
|
Object.defineProperty(exports, "FakePairingDriver", { enumerable: true, get: function () { return FakePairingDriver_1.FakePairingDriver; } });
|
|
6
6
|
var FakePeer_1 = require("./api/FakePeer");
|
|
7
7
|
Object.defineProperty(exports, "FakePeer", { enumerable: true, get: function () { return FakePeer_1.FakePeer; } });
|
|
8
8
|
var FakeWaServer_1 = require("./api/FakeWaServer");
|
|
9
|
+
Object.defineProperty(exports, "FakeServerSession", { enumerable: true, get: function () { return FakeWaServer_1.FakeServerSession; } });
|
|
9
10
|
Object.defineProperty(exports, "FakeWaServer", { enumerable: true, get: function () { return FakeWaServer_1.FakeWaServer; } });
|
|
10
11
|
var Scenario_1 = require("./api/Scenario");
|
|
11
12
|
Object.defineProperty(exports, "IqExpectation", { enumerable: true, get: function () { return Scenario_1.IqExpectation; } });
|
|
@@ -71,4 +72,3 @@ Object.defineProperty(exports, "buildStreamErrorCode", { enumerable: true, get:
|
|
|
71
72
|
Object.defineProperty(exports, "buildStreamErrorDeviceRemoved", { enumerable: true, get: function () { return stream_error_1.buildStreamErrorDeviceRemoved; } });
|
|
72
73
|
Object.defineProperty(exports, "buildStreamErrorReplaced", { enumerable: true, get: function () { return stream_error_1.buildStreamErrorReplaced; } });
|
|
73
74
|
Object.defineProperty(exports, "buildStreamErrorXmlNotWellFormed", { enumerable: true, get: function () { return stream_error_1.buildStreamErrorXmlNotWellFormed; } });
|
|
74
|
-
//# sourceMappingURL=index.js.map
|