@zapo-js/fake-server 0.3.0 → 1.0.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 +3 -3
- 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 +201 -1
- package/dist/api/FakeWaServer.d.ts +11 -1
- package/dist/api/FakeWaServer.js +34 -2
- 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 +0 -1
- package/dist/api/iq-handlers.js +0 -1
- package/dist/{cli.d.ts → bin.d.ts} +0 -1
- package/dist/{cli.js → bin.js} +1 -2
- package/dist/esm/api/AppStateSyncManager.js +146 -0
- package/dist/esm/api/FakePairingDriver.js +31 -0
- package/dist/esm/api/FakePeer.js +604 -0
- package/dist/esm/api/FakeWaServer.js +680 -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 +492 -0
- package/dist/esm/bin.js +246 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/infra/WaFakeConnection.js +49 -0
- package/dist/esm/infra/WaFakeConnectionPipeline.js +223 -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 +39 -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 +111 -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 +0 -1
- package/dist/index.js +0 -1
- package/dist/infra/WaFakeConnection.d.ts +0 -1
- package/dist/infra/WaFakeConnection.js +0 -1
- package/dist/infra/WaFakeConnectionPipeline.d.ts +0 -1
- package/dist/infra/WaFakeConnectionPipeline.js +27 -24
- 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 +0 -1
- package/dist/protocol/auth/cert-chain.js +0 -1
- 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 +0 -1
- package/dist/protocol/iq/router.js +0 -1
- 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 +15 -0
- package/package.json +21 -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,604 @@
|
|
|
1
|
+
/** Fake peer used by tests to encrypt and push Signal messages to the lib. */
|
|
2
|
+
import { buildAppStateSyncKeyShareMessage } from '../protocol/push/app-state-key-share.js';
|
|
3
|
+
import { buildHistorySyncExternalMessage, buildHistorySyncMessage } from '../protocol/push/history-sync.js';
|
|
4
|
+
import { buildMessage } from '../protocol/push/message.js';
|
|
5
|
+
import { FakePeerDoubleRatchet } from '../protocol/signal/fake-peer-double-ratchet.js';
|
|
6
|
+
import { FakePeerGroupRecvSession } from '../protocol/signal/fake-peer-group-recv-session.js';
|
|
7
|
+
import { generateFakePeerKeyBundle } from '../protocol/signal/fake-peer-key-bundle.js';
|
|
8
|
+
import { FakeSenderKey } from '../protocol/signal/fake-sender-key.js';
|
|
9
|
+
import { proto } from '../transport/protos.js';
|
|
10
|
+
export class FakePeer {
|
|
11
|
+
constructor(keyBundle, options, deps) {
|
|
12
|
+
this.ratchetInitiated = false;
|
|
13
|
+
this.nextMessageCounter = 0;
|
|
14
|
+
this.sentPlaintextById = new Map();
|
|
15
|
+
this.sentEncryptedById = new Map();
|
|
16
|
+
this.senderKeysByGroup = new Map();
|
|
17
|
+
this.groupsBootstrapped = new Set();
|
|
18
|
+
this.groupRecvSession = new FakePeerGroupRecvSession();
|
|
19
|
+
this.keyBundle = keyBundle;
|
|
20
|
+
this.identity = {
|
|
21
|
+
identityKeyPair: keyBundle.identityKeyPair,
|
|
22
|
+
registrationId: keyBundle.registrationId
|
|
23
|
+
};
|
|
24
|
+
this.jid = options.jid;
|
|
25
|
+
this.displayName = options.displayName;
|
|
26
|
+
this.deps = deps;
|
|
27
|
+
this.skipOneTimePreKey = options.skipOneTimePreKey === true;
|
|
28
|
+
this.replayCacheEnabled = options.enableReplayCache === true;
|
|
29
|
+
this.reservedOneTimePreKey =
|
|
30
|
+
!this.skipOneTimePreKey && deps.reserveOneTimePreKey
|
|
31
|
+
? deps.reserveOneTimePreKey()
|
|
32
|
+
: null;
|
|
33
|
+
this.ratchet = new FakePeerDoubleRatchet(keyBundle);
|
|
34
|
+
}
|
|
35
|
+
static async create(options, deps) {
|
|
36
|
+
const keyBundle = options.keyBundle ?? (await generateFakePeerKeyBundle());
|
|
37
|
+
return new FakePeer(keyBundle, options, deps);
|
|
38
|
+
}
|
|
39
|
+
async sendMessage(message, options = {}) {
|
|
40
|
+
await this.ensureRatchetInitiated();
|
|
41
|
+
const plaintext = encodePlaintextWithPadding(message);
|
|
42
|
+
const { type, ciphertext } = await this.ratchet.encrypt(plaintext);
|
|
43
|
+
const finalCiphertext = options.tamperCiphertext
|
|
44
|
+
? options.tamperCiphertext(ciphertext)
|
|
45
|
+
: ciphertext;
|
|
46
|
+
const enc = { type, ciphertext: finalCiphertext };
|
|
47
|
+
const id = options.id ?? this.nextId();
|
|
48
|
+
if (this.replayCacheEnabled) {
|
|
49
|
+
// Stash the plaintext so the bench / a test can replay this
|
|
50
|
+
// exact payload later in response to an incoming retry
|
|
51
|
+
// receipt from the lib (replaySentMessage). Also stash the
|
|
52
|
+
// PRISTINE ciphertext (pre-tamper) + signal type so a
|
|
53
|
+
// replay can put the original frame back on the wire –
|
|
54
|
+
// matches how a real Signal sender re-sends after a
|
|
55
|
+
// recipient asks for retry: same counter + ratchet key,
|
|
56
|
+
// just an uncorrupted body.
|
|
57
|
+
this.sentPlaintextById.set(id, message);
|
|
58
|
+
this.sentEncryptedById.set(id, { type, ciphertext });
|
|
59
|
+
}
|
|
60
|
+
const stanza = buildMessage({
|
|
61
|
+
id,
|
|
62
|
+
from: options.from ?? this.jid,
|
|
63
|
+
t: options.t,
|
|
64
|
+
type: options.type ?? 'text',
|
|
65
|
+
notify: this.displayName,
|
|
66
|
+
participant: options.participant,
|
|
67
|
+
enc: [enc]
|
|
68
|
+
});
|
|
69
|
+
await this.deps.pushStanza(stanza);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Re-encrypts and re-pushes a previously sent message identified by
|
|
73
|
+
* `originalMsgId`. Used by tests / benches that exercise the lib's
|
|
74
|
+
* incoming-retry path: after the lib emits `<receipt type="retry">`
|
|
75
|
+
* back to this peer, the peer is expected to re-send the same
|
|
76
|
+
* plaintext so the lib can decrypt it successfully on the second
|
|
77
|
+
* attempt.
|
|
78
|
+
*
|
|
79
|
+
* The replay reuses the original stanza id by default so the lib's
|
|
80
|
+
* dedup logic (and tests that wait for a specific id) see the resend
|
|
81
|
+
* as a continuation of the same outbound. Pass `resendId` to use a
|
|
82
|
+
* different id.
|
|
83
|
+
*/
|
|
84
|
+
async replaySentMessage(originalMsgId, options = {}) {
|
|
85
|
+
const stored = this.sentEncryptedById.get(originalMsgId);
|
|
86
|
+
if (stored) {
|
|
87
|
+
// Re-send the pristine ciphertext – same Signal counter +
|
|
88
|
+
// ratchet key as the original send, just without the
|
|
89
|
+
// tamper. The lib's recv chain still has the message key
|
|
90
|
+
// for that counter, so this is the recovery path real
|
|
91
|
+
// WhatsApp peers walk after observing a retry receipt for
|
|
92
|
+
// a frame the recipient failed to decrypt.
|
|
93
|
+
const enc = { type: stored.type, ciphertext: stored.ciphertext };
|
|
94
|
+
const id = options.resendId ?? originalMsgId;
|
|
95
|
+
const stanza = buildMessage({
|
|
96
|
+
id,
|
|
97
|
+
from: this.jid,
|
|
98
|
+
type: 'text',
|
|
99
|
+
notify: this.displayName,
|
|
100
|
+
enc: [enc]
|
|
101
|
+
});
|
|
102
|
+
await this.deps.pushStanza(stanza);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const message = this.sentPlaintextById.get(originalMsgId);
|
|
106
|
+
if (!message) {
|
|
107
|
+
throw new Error(`FakePeer.replaySentMessage: no sent plaintext for id ${originalMsgId}`);
|
|
108
|
+
}
|
|
109
|
+
await this.sendMessage(message, { id: options.resendId ?? originalMsgId });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Rotates the peer's prekey material and resets the Signal session
|
|
113
|
+
* state so the lib has to re-run X3DH on the next outbound. Keeps
|
|
114
|
+
* the identity key pair (same user) but issues a new signed prekey
|
|
115
|
+
* + new one-time prekey set. After this call:
|
|
116
|
+
* - `keyBundle` reflects the new material;
|
|
117
|
+
* - the underlying `FakePeerDoubleRatchet` is fresh – the next
|
|
118
|
+
* inbound from the lib must be a `pkmsg` (initial signal
|
|
119
|
+
* message), which the new `keyBundle` is wired to decrypt;
|
|
120
|
+
* - any cached one-time prekey reservation in the dispenser is
|
|
121
|
+
* dropped so subsequent retry-receipts include this peer's new
|
|
122
|
+
* keys directly via {@link sendRetryReceipt}.
|
|
123
|
+
*
|
|
124
|
+
* Use this to simulate the "session lost" scenario that real
|
|
125
|
+
* WhatsApp clients trigger when they need the sender to fully
|
|
126
|
+
* re-bootstrap the Signal session via the retry-receipt path.
|
|
127
|
+
*/
|
|
128
|
+
async rotateForRetry() {
|
|
129
|
+
const refreshed = await generateFakePeerKeyBundle({
|
|
130
|
+
identityKeyPair: this.keyBundle.identityKeyPair,
|
|
131
|
+
registrationId: this.keyBundle.registrationId,
|
|
132
|
+
signedPreKeyId: this.keyBundle.signedPreKey.id + 1,
|
|
133
|
+
firstOneTimePreKeyId: (this.keyBundle.oneTimePreKeys[this.keyBundle.oneTimePreKeys.length - 1]?.id ?? 0) +
|
|
134
|
+
1
|
|
135
|
+
});
|
|
136
|
+
this.keyBundle = refreshed;
|
|
137
|
+
this.ratchet = new FakePeerDoubleRatchet(refreshed);
|
|
138
|
+
this.ratchetInitiated = false;
|
|
139
|
+
this.reservedOneTimePreKey = null;
|
|
140
|
+
}
|
|
141
|
+
sendConversation(text, options = {}) {
|
|
142
|
+
return this.sendMessage({ conversation: text }, options);
|
|
143
|
+
}
|
|
144
|
+
async sendMessageEdit(input, options = {}) {
|
|
145
|
+
await this.sendMessage({
|
|
146
|
+
protocolMessage: {
|
|
147
|
+
type: proto.Message.ProtocolMessage.Type.MESSAGE_EDIT,
|
|
148
|
+
key: {
|
|
149
|
+
remoteJid: this.jid,
|
|
150
|
+
fromMe: false,
|
|
151
|
+
id: input.targetMessageId
|
|
152
|
+
},
|
|
153
|
+
timestampMs: Date.now(),
|
|
154
|
+
editedMessage: {
|
|
155
|
+
conversation: input.newText
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}, options);
|
|
159
|
+
}
|
|
160
|
+
async sendMessageRevoke(input, options = {}) {
|
|
161
|
+
await this.sendMessage({
|
|
162
|
+
protocolMessage: {
|
|
163
|
+
type: proto.Message.ProtocolMessage.Type.REVOKE,
|
|
164
|
+
key: {
|
|
165
|
+
remoteJid: this.jid,
|
|
166
|
+
fromMe: false,
|
|
167
|
+
id: input.targetMessageId
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, options);
|
|
171
|
+
}
|
|
172
|
+
async sendReaction(input, options = {}) {
|
|
173
|
+
await this.sendMessage({
|
|
174
|
+
reactionMessage: {
|
|
175
|
+
key: {
|
|
176
|
+
remoteJid: this.jid,
|
|
177
|
+
fromMe: false,
|
|
178
|
+
id: input.targetMessageId
|
|
179
|
+
},
|
|
180
|
+
text: input.emoji,
|
|
181
|
+
senderTimestampMs: Date.now()
|
|
182
|
+
}
|
|
183
|
+
}, options);
|
|
184
|
+
}
|
|
185
|
+
async sendHistorySync(input = {}, options = {}) {
|
|
186
|
+
const message = await buildHistorySyncMessage(input);
|
|
187
|
+
await this.sendMessage(message, options);
|
|
188
|
+
}
|
|
189
|
+
async sendHistorySyncExternal(input, options = {}) {
|
|
190
|
+
const message = buildHistorySyncExternalMessage(input);
|
|
191
|
+
await this.sendMessage(message, options);
|
|
192
|
+
}
|
|
193
|
+
async sendImageMessage(input, options = {}) {
|
|
194
|
+
await this.sendMessage({
|
|
195
|
+
imageMessage: {
|
|
196
|
+
url: input.directPath,
|
|
197
|
+
directPath: input.directPath,
|
|
198
|
+
mediaKey: input.mediaKey,
|
|
199
|
+
fileSha256: input.fileSha256,
|
|
200
|
+
fileEncSha256: input.fileEncSha256,
|
|
201
|
+
fileLength: input.fileLength,
|
|
202
|
+
mimetype: input.mimetype ?? 'image/jpeg',
|
|
203
|
+
caption: input.caption,
|
|
204
|
+
width: input.width,
|
|
205
|
+
height: input.height,
|
|
206
|
+
mediaKeyTimestamp: Math.floor(Date.now() / 1000)
|
|
207
|
+
}
|
|
208
|
+
}, options);
|
|
209
|
+
}
|
|
210
|
+
async sendVideoMessage(input, options = {}) {
|
|
211
|
+
await this.sendMessage({
|
|
212
|
+
videoMessage: {
|
|
213
|
+
url: input.directPath,
|
|
214
|
+
directPath: input.directPath,
|
|
215
|
+
mediaKey: input.mediaKey,
|
|
216
|
+
fileSha256: input.fileSha256,
|
|
217
|
+
fileEncSha256: input.fileEncSha256,
|
|
218
|
+
fileLength: input.fileLength,
|
|
219
|
+
mimetype: input.mimetype ?? 'video/mp4',
|
|
220
|
+
caption: input.caption,
|
|
221
|
+
seconds: input.seconds,
|
|
222
|
+
width: input.width,
|
|
223
|
+
height: input.height,
|
|
224
|
+
mediaKeyTimestamp: Math.floor(Date.now() / 1000)
|
|
225
|
+
}
|
|
226
|
+
}, options);
|
|
227
|
+
}
|
|
228
|
+
async sendAudioMessage(input, options = {}) {
|
|
229
|
+
await this.sendMessage({
|
|
230
|
+
audioMessage: {
|
|
231
|
+
url: input.directPath,
|
|
232
|
+
directPath: input.directPath,
|
|
233
|
+
mediaKey: input.mediaKey,
|
|
234
|
+
fileSha256: input.fileSha256,
|
|
235
|
+
fileEncSha256: input.fileEncSha256,
|
|
236
|
+
fileLength: input.fileLength,
|
|
237
|
+
mimetype: input.mimetype ?? (input.ptt ? 'audio/ogg' : 'audio/mp4'),
|
|
238
|
+
seconds: input.seconds,
|
|
239
|
+
ptt: input.ptt,
|
|
240
|
+
mediaKeyTimestamp: Math.floor(Date.now() / 1000)
|
|
241
|
+
}
|
|
242
|
+
}, options);
|
|
243
|
+
}
|
|
244
|
+
async sendDocumentMessage(input, options = {}) {
|
|
245
|
+
await this.sendMessage({
|
|
246
|
+
documentMessage: {
|
|
247
|
+
url: input.directPath,
|
|
248
|
+
directPath: input.directPath,
|
|
249
|
+
mediaKey: input.mediaKey,
|
|
250
|
+
fileSha256: input.fileSha256,
|
|
251
|
+
fileEncSha256: input.fileEncSha256,
|
|
252
|
+
fileLength: input.fileLength,
|
|
253
|
+
mimetype: input.mimetype ?? 'application/pdf',
|
|
254
|
+
title: input.title,
|
|
255
|
+
fileName: input.fileName,
|
|
256
|
+
pageCount: input.pageCount,
|
|
257
|
+
mediaKeyTimestamp: Math.floor(Date.now() / 1000)
|
|
258
|
+
}
|
|
259
|
+
}, options);
|
|
260
|
+
}
|
|
261
|
+
async sendStickerMessage(input, options = {}) {
|
|
262
|
+
await this.sendMessage({
|
|
263
|
+
stickerMessage: {
|
|
264
|
+
url: input.directPath,
|
|
265
|
+
directPath: input.directPath,
|
|
266
|
+
mediaKey: input.mediaKey,
|
|
267
|
+
fileSha256: input.fileSha256,
|
|
268
|
+
fileEncSha256: input.fileEncSha256,
|
|
269
|
+
fileLength: input.fileLength,
|
|
270
|
+
mimetype: input.mimetype ?? 'image/webp',
|
|
271
|
+
width: input.width,
|
|
272
|
+
height: input.height,
|
|
273
|
+
isAnimated: input.isAnimated,
|
|
274
|
+
mediaKeyTimestamp: Math.floor(Date.now() / 1000)
|
|
275
|
+
}
|
|
276
|
+
}, options);
|
|
277
|
+
}
|
|
278
|
+
async sendAppStateSyncKeyShare(input, options = {}) {
|
|
279
|
+
const message = buildAppStateSyncKeyShareMessage(input);
|
|
280
|
+
await this.sendMessage(message, options);
|
|
281
|
+
}
|
|
282
|
+
async sendGroupConversation(groupJid, text, options = {}) {
|
|
283
|
+
return this.sendGroupMessage(groupJid, { conversation: text }, options);
|
|
284
|
+
}
|
|
285
|
+
async sendGroupMessage(groupJid, message, options = {}) {
|
|
286
|
+
let senderKey = this.senderKeysByGroup.get(groupJid);
|
|
287
|
+
if (!senderKey) {
|
|
288
|
+
senderKey = await FakeSenderKey.generate();
|
|
289
|
+
this.senderKeysByGroup.set(groupJid, senderKey);
|
|
290
|
+
}
|
|
291
|
+
const plaintext = encodePlaintextWithPadding(message);
|
|
292
|
+
const { ciphertext, distributionMessage } = await senderKey.encrypt(plaintext);
|
|
293
|
+
// Seed the sender key before the first skmsg for this group.
|
|
294
|
+
const encChildren = [];
|
|
295
|
+
if (!this.groupsBootstrapped.has(groupJid)) {
|
|
296
|
+
await this.ensureRatchetInitiated();
|
|
297
|
+
const bootstrapPlaintext = encodePlaintextWithPadding({
|
|
298
|
+
senderKeyDistributionMessage: {
|
|
299
|
+
groupId: groupJid,
|
|
300
|
+
axolotlSenderKeyDistributionMessage: distributionMessage
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
const { type: pkType, ciphertext: pkCt } = await this.ratchet.encrypt(bootstrapPlaintext);
|
|
304
|
+
encChildren.push({ type: pkType, ciphertext: pkCt });
|
|
305
|
+
this.groupsBootstrapped.add(groupJid);
|
|
306
|
+
}
|
|
307
|
+
encChildren.push({ type: 'skmsg', ciphertext });
|
|
308
|
+
const groupStanza = buildMessage({
|
|
309
|
+
id: options.id ?? this.nextId(),
|
|
310
|
+
from: groupJid,
|
|
311
|
+
participant: this.jid,
|
|
312
|
+
t: options.t,
|
|
313
|
+
type: options.type ?? 'text',
|
|
314
|
+
notify: this.displayName,
|
|
315
|
+
enc: encChildren
|
|
316
|
+
});
|
|
317
|
+
await this.deps.pushStanza(groupStanza);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Sends a `<receipt type="retry">` stanza to the lib asking it to
|
|
321
|
+
* re-encrypt and re-send the message identified by `originalMsgId`.
|
|
322
|
+
* Used by benches / tests that exercise the lib's outbound retry
|
|
323
|
+
* replay path.
|
|
324
|
+
*
|
|
325
|
+
* Shape matches WhatsApp Web's `WAWebRetryRequestParser`:
|
|
326
|
+
* `<receipt id type=retry from t><retry id count?/><registration>4B</registration>[<keys>...</keys>]</receipt>`.
|
|
327
|
+
*
|
|
328
|
+
* When `includeKeys` is true (default), the `<keys>` block carries
|
|
329
|
+
* the peer's current identity + signed prekey + one-time prekey.
|
|
330
|
+
* This forces the lib's `WaRetryCoordinator` to tear down the
|
|
331
|
+
* existing session and re-run X3DH against these fresh keys before
|
|
332
|
+
* re-encrypting the original payload, so the resend lands as a
|
|
333
|
+
* `pkmsg` against the peer's *new* prekey state. Pair this with
|
|
334
|
+
* {@link rotateForRetry} to simulate full session loss + recovery.
|
|
335
|
+
*
|
|
336
|
+
* `<device-identity>` is omitted: it's `maybeChild` in wa-web and
|
|
337
|
+
* also optional in the lib's parser. The fake-peer fixture doesn't
|
|
338
|
+
* have an ADV primary-device to sign one, and the lib's outbound
|
|
339
|
+
* retry replay path does not require it.
|
|
340
|
+
*/
|
|
341
|
+
async sendRetryReceipt(originalMsgId, options = {}) {
|
|
342
|
+
const receiptId = options.receiptId ?? originalMsgId;
|
|
343
|
+
const retryCount = options.count ?? 1;
|
|
344
|
+
const t = options.t ?? Math.floor(Date.now() / 1000);
|
|
345
|
+
const error = options.error;
|
|
346
|
+
const includeKeys = options.includeKeys !== false;
|
|
347
|
+
const registration = new Uint8Array(4);
|
|
348
|
+
const regId = this.keyBundle.registrationId & 0xffffffff;
|
|
349
|
+
registration[0] = (regId >>> 24) & 0xff;
|
|
350
|
+
registration[1] = (regId >>> 16) & 0xff;
|
|
351
|
+
registration[2] = (regId >>> 8) & 0xff;
|
|
352
|
+
registration[3] = regId & 0xff;
|
|
353
|
+
const retryAttrs = {
|
|
354
|
+
v: '1',
|
|
355
|
+
count: String(retryCount),
|
|
356
|
+
id: originalMsgId,
|
|
357
|
+
t: String(t)
|
|
358
|
+
};
|
|
359
|
+
if (error !== undefined && error !== 0) {
|
|
360
|
+
retryAttrs.error = String(error);
|
|
361
|
+
}
|
|
362
|
+
const content = [
|
|
363
|
+
{
|
|
364
|
+
tag: 'retry',
|
|
365
|
+
attrs: retryAttrs
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
tag: 'registration',
|
|
369
|
+
attrs: {},
|
|
370
|
+
content: registration
|
|
371
|
+
}
|
|
372
|
+
];
|
|
373
|
+
if (includeKeys) {
|
|
374
|
+
content.push(this.buildRetryKeysNode());
|
|
375
|
+
}
|
|
376
|
+
const receipt = {
|
|
377
|
+
tag: 'receipt',
|
|
378
|
+
attrs: {
|
|
379
|
+
id: receiptId,
|
|
380
|
+
from: this.jid,
|
|
381
|
+
type: 'retry',
|
|
382
|
+
t: String(t)
|
|
383
|
+
},
|
|
384
|
+
content
|
|
385
|
+
};
|
|
386
|
+
await this.deps.pushStanza(receipt);
|
|
387
|
+
}
|
|
388
|
+
buildRetryKeysNode() {
|
|
389
|
+
const skey = this.keyBundle.signedPreKey;
|
|
390
|
+
const oneTime = this.keyBundle.oneTimePreKeys[0];
|
|
391
|
+
if (!oneTime) {
|
|
392
|
+
throw new Error('FakePeer.sendRetryReceipt: no one-time prekey available');
|
|
393
|
+
}
|
|
394
|
+
const skeyIdBytes = encodeUint3(skey.id);
|
|
395
|
+
const keyIdBytes = encodeUint3(oneTime.id);
|
|
396
|
+
return {
|
|
397
|
+
tag: 'keys',
|
|
398
|
+
attrs: {},
|
|
399
|
+
content: [
|
|
400
|
+
{
|
|
401
|
+
tag: 'identity',
|
|
402
|
+
attrs: {},
|
|
403
|
+
content: this.keyBundle.identityKeyPair.pubKey
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
tag: 'skey',
|
|
407
|
+
attrs: {},
|
|
408
|
+
content: [
|
|
409
|
+
{ tag: 'id', attrs: {}, content: skeyIdBytes },
|
|
410
|
+
{ tag: 'value', attrs: {}, content: skey.keyPair.pubKey },
|
|
411
|
+
{ tag: 'signature', attrs: {}, content: skey.signature }
|
|
412
|
+
]
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
tag: 'key',
|
|
416
|
+
attrs: {},
|
|
417
|
+
content: [
|
|
418
|
+
{ tag: 'id', attrs: {}, content: keyIdBytes },
|
|
419
|
+
{ tag: 'value', attrs: {}, content: oneTime.keyPair.pubKey }
|
|
420
|
+
]
|
|
421
|
+
}
|
|
422
|
+
]
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
expectMessage(options = {}) {
|
|
426
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
427
|
+
return new Promise((resolve, reject) => {
|
|
428
|
+
let unsubscribe = null;
|
|
429
|
+
const timer = setTimeout(() => {
|
|
430
|
+
if (unsubscribe)
|
|
431
|
+
unsubscribe();
|
|
432
|
+
reject(new Error(`FakePeer.expectMessage timed out after ${timeoutMs}ms`));
|
|
433
|
+
}, timeoutMs);
|
|
434
|
+
unsubscribe = this.deps.subscribeInboundMessages((stanza) => {
|
|
435
|
+
const to = stanza.attrs.to ?? '';
|
|
436
|
+
if (to.endsWith('@g.us'))
|
|
437
|
+
return;
|
|
438
|
+
const enc = findEncForPeer(stanza, this.jid);
|
|
439
|
+
if (!enc)
|
|
440
|
+
return;
|
|
441
|
+
const encType = enc.attrs.type;
|
|
442
|
+
if (encType !== 'pkmsg' && encType !== 'msg')
|
|
443
|
+
return;
|
|
444
|
+
const ciphertext = enc.content;
|
|
445
|
+
if (!(ciphertext instanceof Uint8Array))
|
|
446
|
+
return;
|
|
447
|
+
this.decryptPairwise(encType, ciphertext)
|
|
448
|
+
.then((message) => {
|
|
449
|
+
clearTimeout(timer);
|
|
450
|
+
if (unsubscribe)
|
|
451
|
+
unsubscribe();
|
|
452
|
+
resolve({ message, stanza, encType });
|
|
453
|
+
})
|
|
454
|
+
.catch((error) => {
|
|
455
|
+
clearTimeout(timer);
|
|
456
|
+
if (unsubscribe)
|
|
457
|
+
unsubscribe();
|
|
458
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
expectGroupMessage(groupJid, options = {}) {
|
|
464
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
465
|
+
return new Promise((resolve, reject) => {
|
|
466
|
+
let unsubscribe = null;
|
|
467
|
+
const timer = setTimeout(() => {
|
|
468
|
+
if (unsubscribe)
|
|
469
|
+
unsubscribe();
|
|
470
|
+
reject(new Error(`FakePeer.expectGroupMessage timed out after ${timeoutMs}ms`));
|
|
471
|
+
}, timeoutMs);
|
|
472
|
+
unsubscribe = this.deps.subscribeInboundMessages((stanza) => {
|
|
473
|
+
if (stanza.attrs.to !== groupJid)
|
|
474
|
+
return;
|
|
475
|
+
const skmsg = findTopLevelEnc(stanza, 'skmsg');
|
|
476
|
+
if (!skmsg || !(skmsg.content instanceof Uint8Array))
|
|
477
|
+
return;
|
|
478
|
+
const senderJid = options.senderJid ?? stanza.attrs.participant;
|
|
479
|
+
if (!senderJid) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
this.bootstrapAndDecryptGroup(senderJid, groupJid, stanza, skmsg.content)
|
|
483
|
+
.then((message) => {
|
|
484
|
+
clearTimeout(timer);
|
|
485
|
+
if (unsubscribe)
|
|
486
|
+
unsubscribe();
|
|
487
|
+
resolve({ message, stanza, encType: 'skmsg' });
|
|
488
|
+
})
|
|
489
|
+
.catch((error) => {
|
|
490
|
+
clearTimeout(timer);
|
|
491
|
+
if (unsubscribe)
|
|
492
|
+
unsubscribe();
|
|
493
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async decryptPairwise(encType, ciphertext) {
|
|
499
|
+
const padded = encType === 'pkmsg'
|
|
500
|
+
? await this.ratchet.decryptPreKeyMessage(ciphertext)
|
|
501
|
+
: await this.ratchet.decryptMessage(ciphertext);
|
|
502
|
+
return proto.Message.decode(padded);
|
|
503
|
+
}
|
|
504
|
+
async decryptStanza(stanza) {
|
|
505
|
+
const enc = findEncForPeer(stanza, this.jid);
|
|
506
|
+
if (!enc || !(enc.content instanceof Uint8Array))
|
|
507
|
+
return null;
|
|
508
|
+
const encType = enc.attrs.type;
|
|
509
|
+
if (encType !== 'pkmsg' && encType !== 'msg')
|
|
510
|
+
return null;
|
|
511
|
+
const message = await this.decryptPairwise(encType, enc.content);
|
|
512
|
+
return { message, stanza, encType };
|
|
513
|
+
}
|
|
514
|
+
async bootstrapAndDecryptGroup(senderJid, groupJid, stanza, skmsgCiphertext) {
|
|
515
|
+
const bootstrap = findEncForPeer(stanza, this.jid);
|
|
516
|
+
if (bootstrap && bootstrap.content instanceof Uint8Array) {
|
|
517
|
+
const bootstrapType = bootstrap.attrs.type;
|
|
518
|
+
if (bootstrapType === 'pkmsg' || bootstrapType === 'msg') {
|
|
519
|
+
const padded = bootstrapType === 'pkmsg'
|
|
520
|
+
? await this.ratchet.decryptPreKeyMessage(bootstrap.content)
|
|
521
|
+
: await this.ratchet.decryptMessage(bootstrap.content);
|
|
522
|
+
const innerMessage = proto.Message.decode(padded);
|
|
523
|
+
const skdm = innerMessage.senderKeyDistributionMessage;
|
|
524
|
+
const axolotl = skdm?.axolotlSenderKeyDistributionMessage;
|
|
525
|
+
if (axolotl) {
|
|
526
|
+
this.groupRecvSession.addDistribution(groupJid, senderJid, axolotl);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const padded = await this.groupRecvSession.decryptGroupMessage(groupJid, senderJid, skmsgCiphertext);
|
|
531
|
+
return proto.Message.decode(padded);
|
|
532
|
+
}
|
|
533
|
+
async ensureRatchetInitiated() {
|
|
534
|
+
if (this.ratchetInitiated)
|
|
535
|
+
return;
|
|
536
|
+
if (this.ratchet.hasSendChain()) {
|
|
537
|
+
this.ratchetInitiated = true;
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const bundle = await this.deps.bundleResolver();
|
|
541
|
+
const oneTimePreKey = this.reservedOneTimePreKey ?? undefined;
|
|
542
|
+
await this.ratchet.initiateOutbound(bundle, {
|
|
543
|
+
oneTimePreKey,
|
|
544
|
+
skipOneTimePreKey: this.skipOneTimePreKey || !oneTimePreKey
|
|
545
|
+
});
|
|
546
|
+
this.ratchetInitiated = true;
|
|
547
|
+
}
|
|
548
|
+
nextId() {
|
|
549
|
+
this.nextMessageCounter += 1;
|
|
550
|
+
return `${this.jid}-${Date.now()}-${this.nextMessageCounter}`;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function findEncForPeer(stanza, peerJid) {
|
|
554
|
+
if (!Array.isArray(stanza.content))
|
|
555
|
+
return null;
|
|
556
|
+
for (const child of stanza.content) {
|
|
557
|
+
if (child.tag === 'enc' && stanza.attrs.to === peerJid) {
|
|
558
|
+
return child;
|
|
559
|
+
}
|
|
560
|
+
if (child.tag === 'participants' && Array.isArray(child.content)) {
|
|
561
|
+
for (const toNode of child.content) {
|
|
562
|
+
if (toNode.tag !== 'to' || toNode.attrs.jid !== peerJid)
|
|
563
|
+
continue;
|
|
564
|
+
if (!Array.isArray(toNode.content))
|
|
565
|
+
continue;
|
|
566
|
+
for (const inner of toNode.content) {
|
|
567
|
+
if (inner.tag === 'enc')
|
|
568
|
+
return inner;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
function findTopLevelEnc(stanza, type) {
|
|
576
|
+
if (!Array.isArray(stanza.content))
|
|
577
|
+
return null;
|
|
578
|
+
for (const child of stanza.content) {
|
|
579
|
+
if (child.tag === 'enc' && child.attrs.type === type) {
|
|
580
|
+
return child;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
function encodeUint3(value) {
|
|
586
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffffff) {
|
|
587
|
+
throw new Error(`encodeUint3: value out of range: ${value}`);
|
|
588
|
+
}
|
|
589
|
+
const out = new Uint8Array(3);
|
|
590
|
+
out[0] = (value >>> 16) & 0xff;
|
|
591
|
+
out[1] = (value >>> 8) & 0xff;
|
|
592
|
+
out[2] = value & 0xff;
|
|
593
|
+
return out;
|
|
594
|
+
}
|
|
595
|
+
function encodePlaintextWithPadding(message) {
|
|
596
|
+
const encoded = proto.Message.encode(message).finish();
|
|
597
|
+
const padLen = 16 - (encoded.byteLength % 16);
|
|
598
|
+
const out = new Uint8Array(encoded.byteLength + padLen);
|
|
599
|
+
out.set(encoded, 0);
|
|
600
|
+
for (let i = encoded.byteLength; i < out.byteLength; i += 1) {
|
|
601
|
+
out[i] = padLen;
|
|
602
|
+
}
|
|
603
|
+
return out;
|
|
604
|
+
}
|