zapo-js 1.6.2 → 1.7.1
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 +26 -12
- package/dist/abprops-spec.d.ts +10 -0
- package/dist/abprops-spec.js +25 -0
- package/dist/appstate/crypto/WaAppStateCrypto.js +4 -6
- package/dist/auth/WaAuthClient.d.ts +14 -1
- package/dist/auth/WaAuthClient.js +22 -1
- package/dist/auth/credentials-flow.d.ts +3 -1
- package/dist/auth/credentials-flow.js +3 -0
- package/dist/auth/pairing/WaPairingFlow.d.ts +5 -0
- package/dist/auth/pairing/WaPairingFlow.js +66 -14
- package/dist/auth/types.d.ts +13 -0
- package/dist/client/WaClient.d.ts +11 -0
- package/dist/client/WaClient.js +60 -0
- package/dist/client/WaClientFactory.d.ts +2 -2
- package/dist/client/WaClientFactory.js +53 -13
- package/dist/client/coordinators/WaAbPropsCoordinator.d.ts +2 -2
- package/dist/client/coordinators/WaAbPropsCoordinator.js +16 -4
- package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +2 -1
- package/dist/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
- package/dist/client/coordinators/WaMessageCoordinator.d.ts +96 -0
- package/dist/client/coordinators/WaMessageCoordinator.js +163 -8
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +64 -0
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +252 -57
- package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +97 -19
- package/dist/client/coordinators/WaPrivacyCoordinator.js +152 -101
- package/dist/client/coordinators/WaRetryCoordinator.d.ts +22 -0
- package/dist/client/coordinators/WaRetryCoordinator.js +74 -11
- package/dist/client/events/dirty.d.ts +3 -0
- package/dist/client/events/dirty.js +12 -7
- package/dist/client/events/offline.d.ts +9 -0
- package/dist/client/events/offline.js +95 -0
- package/dist/client/events/privacy.d.ts +45 -0
- package/dist/client/events/privacy.js +130 -0
- package/dist/client/messaging/group-metadata.d.ts +12 -0
- package/dist/client/messaging/group-metadata.js +24 -8
- package/dist/client/messaging/link-preview.d.ts +7 -0
- package/dist/client/messaging/link-preview.js +27 -0
- package/dist/client/messaging/messages.js +16 -12
- package/dist/client/newsletter/content.d.ts +4 -1
- package/dist/client/newsletter/content.js +43 -18
- package/dist/client/newsletter/discovery.js +4 -2
- package/dist/client/newsletter/messaging.d.ts +2 -0
- package/dist/client/newsletter/messaging.js +2 -1
- package/dist/client/newsletter/mex.d.ts +2 -2
- package/dist/client/persistence/WriteBehindPersistence.js +2 -1
- package/dist/client/persistence/ephemeral-setting.d.ts +42 -0
- package/dist/client/persistence/ephemeral-setting.js +120 -0
- package/dist/client/persistence/group-history.d.ts +29 -0
- package/dist/client/persistence/group-history.js +149 -0
- package/dist/client/persistence/history-blob.d.ts +25 -0
- package/dist/client/persistence/history-blob.js +38 -0
- package/dist/client/persistence/history-sync.d.ts +20 -0
- package/dist/client/persistence/history-sync.js +165 -71
- package/dist/client/plugins/types.d.ts +10 -2
- package/dist/client/types.d.ts +150 -16
- package/dist/crypto/core/index.d.ts +1 -1
- package/dist/crypto/core/index.js +2 -1
- package/dist/crypto/core/primitives.d.ts +19 -0
- package/dist/crypto/core/primitives.js +48 -0
- package/dist/crypto/core/xeddsa.js +35 -4
- package/dist/crypto/curves/X25519.js +29 -2
- package/dist/crypto/nativeBackend.d.ts +20 -0
- package/dist/crypto/nativeBackend.js +95 -0
- package/dist/esm/abprops-spec.js +15 -0
- package/dist/esm/appstate/crypto/WaAppStateCrypto.js +4 -6
- package/dist/esm/auth/WaAuthClient.js +22 -1
- package/dist/esm/auth/credentials-flow.js +3 -0
- package/dist/esm/auth/pairing/WaPairingFlow.js +67 -15
- package/dist/esm/client/WaClient.js +60 -0
- package/dist/esm/client/WaClientFactory.js +54 -14
- package/dist/esm/client/coordinators/WaAbPropsCoordinator.js +17 -5
- package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
- package/dist/esm/client/coordinators/WaMessageCoordinator.js +166 -11
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +254 -60
- package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +145 -94
- package/dist/esm/client/coordinators/WaRetryCoordinator.js +74 -11
- package/dist/esm/client/events/dirty.js +12 -7
- package/dist/esm/client/events/offline.js +92 -0
- package/dist/esm/client/events/privacy.js +123 -0
- package/dist/esm/client/messaging/group-metadata.js +25 -9
- package/dist/esm/client/messaging/link-preview.js +29 -2
- package/dist/esm/client/messaging/messages.js +16 -12
- package/dist/esm/client/newsletter/content.js +44 -19
- package/dist/esm/client/newsletter/discovery.js +4 -2
- package/dist/esm/client/newsletter/messaging.js +2 -1
- package/dist/esm/client/persistence/WriteBehindPersistence.js +2 -1
- package/dist/esm/client/persistence/ephemeral-setting.js +116 -0
- package/dist/esm/client/persistence/group-history.js +145 -0
- package/dist/esm/client/persistence/history-blob.js +34 -0
- package/dist/esm/client/persistence/history-sync.js +157 -65
- package/dist/esm/crypto/core/index.js +1 -1
- package/dist/esm/crypto/core/primitives.js +47 -0
- package/dist/esm/crypto/core/xeddsa.js +36 -5
- package/dist/esm/crypto/curves/X25519.js +29 -2
- package/dist/esm/crypto/nativeBackend.js +93 -0
- package/dist/esm/index.js +3 -1
- package/dist/esm/media/crypto/WaMediaCrypto.js +38 -19
- package/dist/esm/message/context-info.js +39 -0
- package/dist/esm/message/crypto/reporting-token.js +38 -216
- package/dist/esm/message/encode/content.js +73 -12
- package/dist/esm/message/encode/media-payload.js +3 -1
- package/dist/esm/message/kinds/group-history.js +36 -0
- package/dist/esm/message/primitives/incoming.js +32 -13
- package/dist/esm/protocol/abprops.js +18 -162
- package/dist/esm/protocol/constants.js +4 -3
- package/dist/esm/protocol/defaults.js +7 -0
- package/dist/esm/protocol/device-identity.js +10 -5
- package/dist/esm/protocol/jid.js +83 -36
- package/dist/esm/protocol/media.js +1 -0
- package/dist/esm/protocol/message.js +8 -0
- package/dist/esm/protocol/notification.js +1 -0
- package/dist/esm/protocol/privacy.js +70 -4
- package/dist/esm/signal/group/SenderKeyChain.js +1 -2
- package/dist/esm/signal/group/SenderKeyManager.js +1 -7
- package/dist/esm/signal/session/SignalProtocol.js +22 -21
- package/dist/esm/signal/session/SignalRatchet.js +20 -17
- package/dist/esm/signal/session/encoding.js +25 -3
- package/dist/esm/signal/session/resolver.js +11 -5
- package/dist/esm/store/contracts/chat-metadata.store.js +1 -0
- package/dist/esm/store/createStore.js +19 -2
- package/dist/esm/store/index.js +1 -0
- package/dist/esm/store/memory/chat-metadata.store.js +66 -0
- package/dist/esm/store/noop.store.js +8 -0
- package/dist/esm/transport/binary/constants.js +19 -0
- package/dist/esm/transport/binary/decoder.js +17 -19
- package/dist/esm/transport/binary/encoder.js +61 -18
- package/dist/esm/transport/node/builders/global.js +20 -0
- package/dist/esm/transport/node/builders/message.js +32 -15
- package/dist/esm/transport/node/builders/privacy.js +63 -4
- package/dist/esm/transport/noise/WaClientPayload.js +43 -22
- package/dist/esm/transport/noise/WaFrameCodec.js +14 -4
- package/dist/esm/util/coercion.js +12 -4
- package/dist/esm/util/index.js +1 -1
- package/dist/esm/util/protoscan.js +181 -0
- package/dist/esm/util/runtime.js +34 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +15 -2
- package/dist/media/crypto/WaMediaCrypto.js +36 -17
- package/dist/media/types.d.ts +1 -1
- package/dist/message/context-info.d.ts +10 -0
- package/dist/message/context-info.js +40 -0
- package/dist/message/crypto/reporting-token.d.ts +3 -1
- package/dist/message/crypto/reporting-token.js +38 -216
- package/dist/message/encode/content.d.ts +24 -4
- package/dist/message/encode/content.js +74 -12
- package/dist/message/encode/media-payload.d.ts +5 -2
- package/dist/message/encode/media-payload.js +3 -1
- package/dist/message/kinds/group-history.d.ts +30 -0
- package/dist/message/kinds/group-history.js +40 -0
- package/dist/message/primitives/incoming.d.ts +5 -0
- package/dist/message/primitives/incoming.js +32 -13
- package/dist/message/types.d.ts +10 -0
- package/dist/protocol/abprops.d.ts +27 -140
- package/dist/protocol/abprops.js +18 -163
- package/dist/protocol/constants.d.ts +4 -3
- package/dist/protocol/constants.js +10 -3
- package/dist/protocol/defaults.d.ts +7 -0
- package/dist/protocol/defaults.js +7 -0
- package/dist/protocol/device-identity.d.ts +14 -4
- package/dist/protocol/device-identity.js +9 -4
- package/dist/protocol/jid.d.ts +4 -1
- package/dist/protocol/jid.js +83 -36
- package/dist/protocol/media.d.ts +1 -0
- package/dist/protocol/media.js +1 -0
- package/dist/protocol/message.d.ts +6 -0
- package/dist/protocol/message.js +9 -0
- package/dist/protocol/notification.d.ts +1 -0
- package/dist/protocol/notification.js +1 -0
- package/dist/protocol/privacy.d.ts +52 -0
- package/dist/protocol/privacy.js +71 -5
- package/dist/signal/group/SenderKeyChain.d.ts +1 -1
- package/dist/signal/group/SenderKeyChain.js +1 -2
- package/dist/signal/group/SenderKeyManager.js +1 -7
- package/dist/signal/session/SignalProtocol.d.ts +13 -0
- package/dist/signal/session/SignalProtocol.js +22 -21
- package/dist/signal/session/SignalRatchet.d.ts +4 -4
- package/dist/signal/session/SignalRatchet.js +19 -16
- package/dist/signal/session/SignalSession.d.ts +8 -1
- package/dist/signal/session/encoding.d.ts +5 -0
- package/dist/signal/session/encoding.js +25 -3
- package/dist/signal/session/resolver.js +11 -5
- package/dist/store/contracts/chat-metadata.store.d.ts +34 -0
- package/dist/store/contracts/chat-metadata.store.js +2 -0
- package/dist/store/contracts/group-metadata.store.d.ts +7 -0
- package/dist/store/contracts/thread.store.d.ts +6 -0
- package/dist/store/createStore.d.ts +10 -2
- package/dist/store/createStore.js +18 -1
- package/dist/store/index.d.ts +3 -1
- package/dist/store/index.js +3 -1
- package/dist/store/memory/chat-metadata.store.d.ts +26 -0
- package/dist/store/memory/chat-metadata.store.js +70 -0
- package/dist/store/noop.store.d.ts +2 -0
- package/dist/store/noop.store.js +9 -1
- package/dist/store/types.d.ts +190 -39
- package/dist/transport/binary/constants.d.ts +8 -0
- package/dist/transport/binary/constants.js +20 -1
- package/dist/transport/binary/decoder.js +17 -19
- package/dist/transport/binary/encoder.js +60 -17
- package/dist/transport/node/builders/global.d.ts +8 -0
- package/dist/transport/node/builders/global.js +21 -0
- package/dist/transport/node/builders/message.js +32 -15
- package/dist/transport/node/builders/privacy.d.ts +26 -2
- package/dist/transport/node/builders/privacy.js +63 -3
- package/dist/transport/noise/WaClientPayload.js +43 -22
- package/dist/transport/noise/WaFrameCodec.d.ts +1 -0
- package/dist/transport/noise/WaFrameCodec.js +14 -4
- package/dist/transport/noise/types.d.ts +17 -0
- package/dist/util/coercion.d.ts +6 -0
- package/dist/util/coercion.js +13 -4
- package/dist/util/index.d.ts +1 -1
- package/dist/util/index.js +2 -1
- package/dist/util/protoscan.d.ts +33 -0
- package/dist/util/protoscan.js +186 -0
- package/dist/util/runtime.d.ts +12 -0
- package/dist/util/runtime.js +35 -0
- package/package.json +3 -1
- package/spec/abprops/index.d.ts +4402 -0
- package/spec/abprops/index.js +4372 -0
|
@@ -21,6 +21,25 @@ export declare function aesGcmEncrypt(key: AesKey, nonce: Uint8Array, plaintext:
|
|
|
21
21
|
export declare function aesGcmDecrypt(key: AesKey, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
22
22
|
/** AES-256-CBC encrypt with PKCS#7 padding (default Node behavior). */
|
|
23
23
|
export declare function aesCbcEncrypt(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array;
|
|
24
|
+
/**
|
|
25
|
+
* AES-256-CBC encrypt into a buffer that reserves `trailerBytes` of spare room
|
|
26
|
+
* after the ciphertext, for a caller that has to append a MAC to it.
|
|
27
|
+
*
|
|
28
|
+
* Appending to the result of {@link aesCbcEncrypt} means copying the whole
|
|
29
|
+
* ciphertext a second time, which on a media payload is a copy of the entire
|
|
30
|
+
* file to make room for ten bytes. Sizing the destination from the PKCS#7
|
|
31
|
+
* output length up front removes that copy, and feeding the cipher in bounded
|
|
32
|
+
* chunks keeps the transient buffer Node hands back to a chunk rather than a
|
|
33
|
+
* second full-size ciphertext, so peak memory stays near one copy of the
|
|
34
|
+
* payload instead of two.
|
|
35
|
+
*
|
|
36
|
+
* The ciphertext occupies `result.length - trailerBytes`; the reserved tail is
|
|
37
|
+
* left zeroed for the caller to fill.
|
|
38
|
+
*
|
|
39
|
+
* @throws when `trailerBytes` is not a non-negative safe integer, or when the
|
|
40
|
+
* cipher yields a length other than the PKCS#7 size the destination was cut to.
|
|
41
|
+
*/
|
|
42
|
+
export declare function aesCbcEncryptWithTrailer(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array, trailerBytes: number): Uint8Array;
|
|
24
43
|
/** AES-256-CBC decrypt with PKCS#7 padding. */
|
|
25
44
|
export declare function aesCbcDecrypt(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array;
|
|
26
45
|
/** AES-256-CTR encrypt with the given 16-byte counter. */
|
|
@@ -7,6 +7,7 @@ exports.md5Bytes = md5Bytes;
|
|
|
7
7
|
exports.aesGcmEncrypt = aesGcmEncrypt;
|
|
8
8
|
exports.aesGcmDecrypt = aesGcmDecrypt;
|
|
9
9
|
exports.aesCbcEncrypt = aesCbcEncrypt;
|
|
10
|
+
exports.aesCbcEncryptWithTrailer = aesCbcEncryptWithTrailer;
|
|
10
11
|
exports.aesCbcDecrypt = aesCbcDecrypt;
|
|
11
12
|
exports.aesCtrEncrypt = aesCtrEncrypt;
|
|
12
13
|
exports.aesCtrDecrypt = aesCtrDecrypt;
|
|
@@ -17,6 +18,9 @@ const node_crypto_1 = require("node:crypto");
|
|
|
17
18
|
const node_util_1 = require("node:util");
|
|
18
19
|
const bytes_1 = require("../../util/bytes");
|
|
19
20
|
const AES_GCM_TAG_LENGTH = 16;
|
|
21
|
+
const AES_CBC_BLOCK_SIZE = 16;
|
|
22
|
+
/** Bounds the transient buffer `cipher.update` returns per call. */
|
|
23
|
+
const AES_CBC_CHUNK_SIZE = 262144;
|
|
20
24
|
const pbkdf2Async = (0, node_util_1.promisify)(node_crypto_1.pbkdf2);
|
|
21
25
|
function feed(target, input) {
|
|
22
26
|
if (Array.isArray(input)) {
|
|
@@ -96,6 +100,50 @@ function aesCbcEncrypt(key, iv, plaintext) {
|
|
|
96
100
|
}
|
|
97
101
|
return (0, bytes_1.concatBytes)([head, tail]);
|
|
98
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* AES-256-CBC encrypt into a buffer that reserves `trailerBytes` of spare room
|
|
105
|
+
* after the ciphertext, for a caller that has to append a MAC to it.
|
|
106
|
+
*
|
|
107
|
+
* Appending to the result of {@link aesCbcEncrypt} means copying the whole
|
|
108
|
+
* ciphertext a second time, which on a media payload is a copy of the entire
|
|
109
|
+
* file to make room for ten bytes. Sizing the destination from the PKCS#7
|
|
110
|
+
* output length up front removes that copy, and feeding the cipher in bounded
|
|
111
|
+
* chunks keeps the transient buffer Node hands back to a chunk rather than a
|
|
112
|
+
* second full-size ciphertext, so peak memory stays near one copy of the
|
|
113
|
+
* payload instead of two.
|
|
114
|
+
*
|
|
115
|
+
* The ciphertext occupies `result.length - trailerBytes`; the reserved tail is
|
|
116
|
+
* left zeroed for the caller to fill.
|
|
117
|
+
*
|
|
118
|
+
* @throws when `trailerBytes` is not a non-negative safe integer, or when the
|
|
119
|
+
* cipher yields a length other than the PKCS#7 size the destination was cut to.
|
|
120
|
+
*/
|
|
121
|
+
function aesCbcEncryptWithTrailer(key, iv, plaintext, trailerBytes) {
|
|
122
|
+
if (!Number.isSafeInteger(trailerBytes) || trailerBytes < 0) {
|
|
123
|
+
throw new Error(`invalid trailer length ${trailerBytes}`);
|
|
124
|
+
}
|
|
125
|
+
const cipherLength = (Math.floor(plaintext.length / AES_CBC_BLOCK_SIZE) + 1) * AES_CBC_BLOCK_SIZE;
|
|
126
|
+
const out = new Uint8Array(cipherLength + trailerBytes);
|
|
127
|
+
const cipher = (0, node_crypto_1.createCipheriv)('aes-256-cbc', key, iv);
|
|
128
|
+
let offset = 0;
|
|
129
|
+
for (let start = 0; start < plaintext.length; start += AES_CBC_CHUNK_SIZE) {
|
|
130
|
+
const end = Math.min(start + AES_CBC_CHUNK_SIZE, plaintext.length);
|
|
131
|
+
const block = cipher.update(plaintext.subarray(start, end));
|
|
132
|
+
if (block.length > 0) {
|
|
133
|
+
out.set(block, offset);
|
|
134
|
+
offset += block.length;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const tail = cipher.final();
|
|
138
|
+
if (tail.length > 0) {
|
|
139
|
+
out.set(tail, offset);
|
|
140
|
+
offset += tail.length;
|
|
141
|
+
}
|
|
142
|
+
if (offset !== cipherLength) {
|
|
143
|
+
throw new Error(`aes-cbc produced ${offset} bytes, expected ${cipherLength}`);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
99
147
|
/** AES-256-CBC decrypt with PKCS#7 padding. */
|
|
100
148
|
function aesCbcDecrypt(key, iv, ciphertext) {
|
|
101
149
|
const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-cbc', key, iv);
|
|
@@ -9,17 +9,30 @@ const X25519_1 = require("../curves/X25519");
|
|
|
9
9
|
const edwards_1 = require("../math/edwards");
|
|
10
10
|
const le_1 = require("../math/le");
|
|
11
11
|
const mod_1 = require("../math/mod");
|
|
12
|
+
const nativeBackend_1 = require("../nativeBackend");
|
|
12
13
|
const bytes_1 = require("../../util/bytes");
|
|
13
14
|
const PREFIX_SIGNATURE_RANDOM = new Uint8Array([
|
|
14
15
|
0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
15
16
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
|
|
16
17
|
]);
|
|
18
|
+
const nativeBinding = (() => {
|
|
19
|
+
if (process.env.ZAPO_XEDDSA_FORCE_JS)
|
|
20
|
+
return null;
|
|
21
|
+
const mod = (0, nativeBackend_1.resolveNativeCryptoBackend)();
|
|
22
|
+
if (mod && typeof mod.xeddsaSign === 'function' && typeof mod.xeddsaVerify === 'function') {
|
|
23
|
+
return { xeddsaSign: mod.xeddsaSign, xeddsaVerify: mod.xeddsaVerify };
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
})();
|
|
17
27
|
/**
|
|
18
28
|
* Verifies an XEdDSA signature over `message` against an X25519 (Montgomery)
|
|
19
29
|
* public key, converting to the Edwards form internally. Returns `false`
|
|
20
30
|
* on shape mismatch instead of throwing.
|
|
21
31
|
*/
|
|
22
32
|
async function xeddsaVerify(curvePublicKey, message, signature) {
|
|
33
|
+
if (nativeBinding) {
|
|
34
|
+
return nativeBinding.xeddsaVerify(curvePublicKey, message, signature);
|
|
35
|
+
}
|
|
23
36
|
if (signature.length !== 64) {
|
|
24
37
|
return false;
|
|
25
38
|
}
|
|
@@ -38,16 +51,34 @@ async function xeddsaVerify(curvePublicKey, message, signature) {
|
|
|
38
51
|
signature[lastByteIndex] = originalLastByte;
|
|
39
52
|
}
|
|
40
53
|
}
|
|
54
|
+
const publicDerivationCache = new WeakMap();
|
|
55
|
+
function derivePublicForSigning(privateKey) {
|
|
56
|
+
const cached = publicDerivationCache.get(privateKey);
|
|
57
|
+
if (cached) {
|
|
58
|
+
return cached;
|
|
59
|
+
}
|
|
60
|
+
const clampedPrivateKey = (0, X25519_1.clampCurvePrivateKeyInPlace)(privateKey);
|
|
61
|
+
const privateScalar = (0, le_1.bytesToBigIntLE)(clampedPrivateKey);
|
|
62
|
+
const encodedPublic = (0, edwards_1.encodeExtendedPoint)((0, edwards_1.scalarMultBase)(privateScalar));
|
|
63
|
+
const derivation = {
|
|
64
|
+
privateScalar,
|
|
65
|
+
encodedPublic,
|
|
66
|
+
pubKeySignBit: encodedPublic[31] & 0x80
|
|
67
|
+
};
|
|
68
|
+
publicDerivationCache.set(privateKey, derivation);
|
|
69
|
+
return derivation;
|
|
70
|
+
}
|
|
41
71
|
/**
|
|
42
72
|
* Signs `message` with an X25519 (Montgomery) private key using the XEdDSA
|
|
43
73
|
* construction. Returns a 64-byte signature.
|
|
44
74
|
*/
|
|
45
75
|
async function xeddsaSign(privateKey, message) {
|
|
46
76
|
(0, bytes_1.assertByteLength)(privateKey, 32, `invalid curve25519 private key length ${privateKey.length}`);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
77
|
+
if (nativeBinding) {
|
|
78
|
+
return (0, bytes_1.toBytesView)(nativeBinding.xeddsaSign(privateKey, message));
|
|
79
|
+
}
|
|
80
|
+
const { privateScalar, encodedPublic, pubKeySignBit } = derivePublicForSigning(privateKey);
|
|
81
|
+
const clampedPrivateKey = privateKey;
|
|
51
82
|
const randomSuffix = await (0, random_1.randomBytesAsync)(64);
|
|
52
83
|
const r = (0, mod_1.modGroup)((0, le_1.bytesToBigIntLE)((0, primitives_1.sha512)([PREFIX_SIGNATURE_RANDOM, clampedPrivateKey, message, randomSuffix])));
|
|
53
84
|
const encodedR = (0, edwards_1.encodeExtendedPoint)((0, edwards_1.scalarMultBase)(r));
|
|
@@ -10,7 +10,17 @@ const constants_1 = require("../curves/constants");
|
|
|
10
10
|
const types_1 = require("../curves/types");
|
|
11
11
|
const constants_2 = require("../math/constants");
|
|
12
12
|
const fe_1 = require("../math/fe");
|
|
13
|
+
const nativeBackend_1 = require("../nativeBackend");
|
|
13
14
|
const bytes_1 = require("../../util/bytes");
|
|
15
|
+
const nativeX25519ScalarMult = (() => {
|
|
16
|
+
if (process.env.ZAPO_X25519_FORCE_JS)
|
|
17
|
+
return null;
|
|
18
|
+
const mod = (0, nativeBackend_1.resolveNativeCryptoBackend)();
|
|
19
|
+
if (mod && typeof mod.x25519ScalarMult === 'function') {
|
|
20
|
+
return mod.x25519ScalarMult;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
})();
|
|
14
24
|
const diffieHellmanWithCallback = node_crypto_1.diffieHellman;
|
|
15
25
|
let diffieHellmanAsync = null;
|
|
16
26
|
let diffieHellmanAsyncProbed = false;
|
|
@@ -76,19 +86,33 @@ function montgomeryToEdwardsPublic(curvePublicKey, signBit) {
|
|
|
76
86
|
encoded[31] = (encoded[31] & 0x7f) | (signBit & 0x80);
|
|
77
87
|
return encoded;
|
|
78
88
|
}
|
|
89
|
+
const privateKeyObjectCache = new WeakMap();
|
|
90
|
+
const publicKeyObjectCache = new WeakMap();
|
|
79
91
|
function x25519PrivateKeyObject(privKey) {
|
|
80
|
-
|
|
92
|
+
const cached = privateKeyObjectCache.get(privKey);
|
|
93
|
+
if (cached) {
|
|
94
|
+
return cached;
|
|
95
|
+
}
|
|
96
|
+
const keyObject = (0, node_crypto_1.createPrivateKey)({
|
|
81
97
|
key: (0, types_1.pkcs8FromRawPrivate)(constants_1.X25519_PKCS8_PREFIX, privKey),
|
|
82
98
|
format: 'der',
|
|
83
99
|
type: 'pkcs8'
|
|
84
100
|
});
|
|
101
|
+
privateKeyObjectCache.set(privKey, keyObject);
|
|
102
|
+
return keyObject;
|
|
85
103
|
}
|
|
86
104
|
function x25519PublicKeyObject(pubKey) {
|
|
87
|
-
|
|
105
|
+
const cached = publicKeyObjectCache.get(pubKey);
|
|
106
|
+
if (cached) {
|
|
107
|
+
return cached;
|
|
108
|
+
}
|
|
109
|
+
const keyObject = (0, node_crypto_1.createPublicKey)({
|
|
88
110
|
key: (0, bytes_1.concatBytes)([constants_1.X25519_SPKI_PREFIX, pubKey]),
|
|
89
111
|
format: 'der',
|
|
90
112
|
type: 'spki'
|
|
91
113
|
});
|
|
114
|
+
publicKeyObjectCache.set(pubKey, keyObject);
|
|
115
|
+
return keyObject;
|
|
92
116
|
}
|
|
93
117
|
/**
|
|
94
118
|
* X25519 key-pair generation and Diffie-Hellman scalar multiplication
|
|
@@ -120,6 +144,9 @@ class X25519 {
|
|
|
120
144
|
static async scalarMult(privKey, pubKey) {
|
|
121
145
|
(0, bytes_1.assertByteLength)(privKey, 32, 'x25519 private key must be 32 bytes');
|
|
122
146
|
(0, bytes_1.assertByteLength)(pubKey, 32, 'x25519 public key must be 32 bytes');
|
|
147
|
+
if (nativeX25519ScalarMult) {
|
|
148
|
+
return (0, bytes_1.toBytesView)(nativeX25519ScalarMult(privKey, pubKey));
|
|
149
|
+
}
|
|
123
150
|
const opts = {
|
|
124
151
|
privateKey: x25519PrivateKeyObject(privKey),
|
|
125
152
|
publicKey: x25519PublicKeyObject(pubKey)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface NativeCryptoModule {
|
|
2
|
+
readonly x25519ScalarMult?: (privateKey: Uint8Array, publicKey: Uint8Array) => Uint8Array;
|
|
3
|
+
readonly xeddsaSign?: (privateKey: Uint8Array, message: Uint8Array) => Uint8Array;
|
|
4
|
+
readonly xeddsaVerify?: (publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => boolean;
|
|
5
|
+
}
|
|
6
|
+
declare const ABSENCE_TOKENS: {
|
|
7
|
+
readonly napi: readonly ["'@zapo-js/native'", "binding.js", "Cannot find native binding"];
|
|
8
|
+
readonly wasm: readonly ["'@zapo-js/native'", "zapo_native_wasm.js"];
|
|
9
|
+
};
|
|
10
|
+
export type NativeBackendKind = keyof typeof ABSENCE_TOKENS;
|
|
11
|
+
/**
|
|
12
|
+
* Classifies a backend load failure: `true` means the backend is simply not
|
|
13
|
+
* available on this install (missing optional package, entry file absent,
|
|
14
|
+
* no prebuilt binary, ESM build without `require`) and the fallback should
|
|
15
|
+
* stay silent; `false` means the backend is present but broken and the
|
|
16
|
+
* degradation deserves a process warning.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isBackendAbsence(backend: NativeBackendKind, error: unknown): boolean;
|
|
19
|
+
export declare function resolveNativeCryptoBackend(): NativeCryptoModule | null;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isBackendAbsence = isBackendAbsence;
|
|
4
|
+
exports.resolveNativeCryptoBackend = resolveNativeCryptoBackend;
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the optional native crypto accelerator backend shared by the
|
|
7
|
+
* X25519 and XEdDSA paths. Selected via `ZAPO_NATIVE_BACKEND`:
|
|
8
|
+
*
|
|
9
|
+
* - `auto` (default) – try the NAPI addon first, then the WASM build,
|
|
10
|
+
* then no accelerator.
|
|
11
|
+
* - `napi` – only the compiled Rust NAPI addon (`@zapo-js/native`).
|
|
12
|
+
* - `wasm` – only the wasm-bindgen build of the same Rust core.
|
|
13
|
+
* The shipped artifact targets the web (ESM glue +
|
|
14
|
+
* explicit init) so one build serves browsers and
|
|
15
|
+
* Node; loading it here relies on `require(esm)`,
|
|
16
|
+
* available unflagged on Node 20.19+ / 22.12+.
|
|
17
|
+
* - `js` / `none` – no accelerator; callers fall back to the pure-JS
|
|
18
|
+
* (and `node:crypto`) implementations.
|
|
19
|
+
*
|
|
20
|
+
* The module is resolved once and memoised. A failed backend resolves to
|
|
21
|
+
* `null` (or, under `auto`, falls through to the next one) so callers
|
|
22
|
+
* transparently fall back to JS. An absent backend stays silent - that is
|
|
23
|
+
* the expected shape of an optional accelerator - but a present-yet-broken
|
|
24
|
+
* one (corrupt binary, ABI mismatch, unsupported runtime) emits a process
|
|
25
|
+
* warning with code `ZAPO_NATIVE_LOAD_FAILED` so the degradation is
|
|
26
|
+
* visible. The per-primitive `ZAPO_X25519_FORCE_JS` / `ZAPO_XEDDSA_FORCE_JS`
|
|
27
|
+
* escape hatches are honoured by the callers, not here.
|
|
28
|
+
*/
|
|
29
|
+
const node_fs_1 = require("node:fs");
|
|
30
|
+
const primitives_1 = require("../util/primitives");
|
|
31
|
+
const WASM_MODULE_SPECIFIER = '@zapo-js/native/wasm/pkg/zapo_native_wasm.js';
|
|
32
|
+
const WASM_BINARY_SPECIFIER = '@zapo-js/native/wasm/pkg/zapo_native_wasm_bg.wasm';
|
|
33
|
+
function loadWasmBackend() {
|
|
34
|
+
const mod = require(WASM_MODULE_SPECIFIER);
|
|
35
|
+
const wasmBytes = (0, node_fs_1.readFileSync)(require.resolve(WASM_BINARY_SPECIFIER));
|
|
36
|
+
mod.initSync({ module: wasmBytes });
|
|
37
|
+
return mod;
|
|
38
|
+
}
|
|
39
|
+
let cached;
|
|
40
|
+
const ABSENCE_TOKENS = {
|
|
41
|
+
napi: ["'@zapo-js/native'", 'binding.js', 'Cannot find native binding'],
|
|
42
|
+
wasm: ["'@zapo-js/native'", 'zapo_native_wasm.js']
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Classifies a backend load failure: `true` means the backend is simply not
|
|
46
|
+
* available on this install (missing optional package, entry file absent,
|
|
47
|
+
* no prebuilt binary, ESM build without `require`) and the fallback should
|
|
48
|
+
* stay silent; `false` means the backend is present but broken and the
|
|
49
|
+
* degradation deserves a process warning.
|
|
50
|
+
*/
|
|
51
|
+
function isBackendAbsence(backend, error) {
|
|
52
|
+
if (error instanceof ReferenceError) {
|
|
53
|
+
return error.message.includes('require is not defined');
|
|
54
|
+
}
|
|
55
|
+
const { code, message } = (error ?? {});
|
|
56
|
+
if (typeof message !== 'string')
|
|
57
|
+
return false;
|
|
58
|
+
const notFound = code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND';
|
|
59
|
+
if (!notFound && !message.includes('Cannot find native binding'))
|
|
60
|
+
return false;
|
|
61
|
+
return ABSENCE_TOKENS[backend].some((token) => message.includes(token));
|
|
62
|
+
}
|
|
63
|
+
function warnLoadFailure(backend, error) {
|
|
64
|
+
process.emitWarning('failed to load native crypto accelerator, falling back', {
|
|
65
|
+
code: 'ZAPO_NATIVE_LOAD_FAILED',
|
|
66
|
+
detail: `${backend}: ${(0, primitives_1.toError)(error).message}`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function resolveNativeCryptoBackend() {
|
|
70
|
+
if (cached !== undefined)
|
|
71
|
+
return cached;
|
|
72
|
+
const backend = (process.env.ZAPO_NATIVE_BACKEND ?? 'auto').toLowerCase();
|
|
73
|
+
cached = null;
|
|
74
|
+
if (backend === 'js' || backend === 'none')
|
|
75
|
+
return cached;
|
|
76
|
+
if (backend === 'napi' || backend === 'auto') {
|
|
77
|
+
try {
|
|
78
|
+
cached = require('@zapo-js/native');
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!isBackendAbsence('napi', error))
|
|
82
|
+
warnLoadFailure('napi', error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (cached === null && (backend === 'wasm' || backend === 'auto')) {
|
|
86
|
+
try {
|
|
87
|
+
cached = loadWasmBackend();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!isBackendAbsence('wasm', error))
|
|
91
|
+
warnLoadFailure('wasm', error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return cached;
|
|
95
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { WA_ABPROPS_BY_CODE } from '../../spec/abprops/index.js';
|
|
2
|
+
export { WA_ABPROPS, WA_ABPROPS_BY_CODE, WA_ABPROPS_SPECIAL_EARLY, WA_ABPROPS_USED_BEFORE_INIT, WA_GROUP_ABPROPS, WA_GROUP_ABPROPS_BY_CODE } from '../../spec/abprops/index.js';
|
|
3
|
+
// The reverse map is emitted with numeric literal keys, so it cannot be indexed
|
|
4
|
+
// with a plain `number`. Widening it keeps the lookup a single property read and
|
|
5
|
+
// avoids allocating a parallel Map with 2000+ entries at module init.
|
|
6
|
+
const AB_PROP_NAME_BY_CODE = WA_ABPROPS_BY_CODE;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the client-side prop name for a wire config code.
|
|
9
|
+
*
|
|
10
|
+
* Returns `undefined` for codes this build does not know about - the server
|
|
11
|
+
* advertises props from newer bundles, and WA Web drops those rows too.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveAbPropNameByCode(configCode) {
|
|
14
|
+
return AB_PROP_NAME_BY_CODE[configCode];
|
|
15
|
+
}
|
|
@@ -174,12 +174,10 @@ export class WaAppStateCrypto {
|
|
|
174
174
|
if (out.byteLength !== left.byteLength) {
|
|
175
175
|
throw new Error('lt hash output length mismatch');
|
|
176
176
|
}
|
|
177
|
-
const leftView = new DataView(left.buffer, left.byteOffset, left.byteLength);
|
|
178
|
-
const rightView = new DataView(right.buffer, right.byteOffset, right.byteLength);
|
|
179
|
-
const outView = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
180
177
|
for (let offset = 0; offset < left.byteLength; offset += APP_STATE_POINT_SIZE) {
|
|
181
|
-
const value = combine(
|
|
182
|
-
|
|
178
|
+
const value = combine(left[offset] | (left[offset + 1] << 8), right[offset] | (right[offset + 1] << 8));
|
|
179
|
+
out[offset] = value & 0xff;
|
|
180
|
+
out[offset + 1] = (value >>> 8) & 0xff;
|
|
183
181
|
}
|
|
184
182
|
return out;
|
|
185
183
|
}
|
|
@@ -196,7 +194,7 @@ export class WaAppStateCrypto {
|
|
|
196
194
|
generateValueMac(valueMacHmacKey, associatedData, iv, cipherText) {
|
|
197
195
|
const octetLength = new Uint8Array(APP_STATE_MAC_OCTET_LENGTH);
|
|
198
196
|
octetLength[octetLength.length - 1] = associatedData.byteLength & 0xff;
|
|
199
|
-
const full = hmacSha512Sign(valueMacHmacKey,
|
|
197
|
+
const full = hmacSha512Sign(valueMacHmacKey, [associatedData, iv, cipherText, octetLength]);
|
|
200
198
|
return full.subarray(0, APP_STATE_VALUE_MAC_LENGTH);
|
|
201
199
|
}
|
|
202
200
|
touchDerivedKeysCacheEntry(cacheKey, keys) {
|
|
@@ -20,11 +20,18 @@ export class WaAuthClient {
|
|
|
20
20
|
constructor(options, deps) {
|
|
21
21
|
this.versionOverride = null;
|
|
22
22
|
this.mobileAppVersionOverride = null;
|
|
23
|
+
/**
|
|
24
|
+
* Successful logins made by this instance, advertised as
|
|
25
|
+
* `connectAttemptCount`. Process-local by design – the persisted
|
|
26
|
+
* counterpart is `credentials.loginCounter`.
|
|
27
|
+
*/
|
|
28
|
+
this.connectAttemptCount = 0;
|
|
23
29
|
const device = resolveWaDeviceIdentity(options);
|
|
24
30
|
this.options = Object.freeze({
|
|
25
31
|
...options,
|
|
26
32
|
deviceBrowser: device.browser,
|
|
27
33
|
deviceOsDisplayName: device.osDisplayName,
|
|
34
|
+
deviceOsVersion: device.osVersion ?? undefined,
|
|
28
35
|
devicePlatform: device.platform,
|
|
29
36
|
requireFullSync: options.requireFullSync
|
|
30
37
|
});
|
|
@@ -122,8 +129,10 @@ export class WaAuthClient {
|
|
|
122
129
|
const mobileAppVersionOverride = this.mobileAppVersionOverride;
|
|
123
130
|
this.mobileAppVersionOverride = null;
|
|
124
131
|
return buildCommsConfig(this.logger, this.requireCredentials(), socketOptions, {
|
|
132
|
+
connectAttemptCount: this.connectAttemptCount,
|
|
125
133
|
deviceBrowser: this.options.deviceBrowser,
|
|
126
134
|
deviceOsDisplayName: this.options.deviceOsDisplayName,
|
|
135
|
+
deviceOsVersion: this.options.deviceOsVersion,
|
|
127
136
|
requireFullSync: this.options.requireFullSync,
|
|
128
137
|
version: override ?? this.options.version,
|
|
129
138
|
mobileTransport: this.options.mobileTransport,
|
|
@@ -236,10 +245,19 @@ export class WaAuthClient {
|
|
|
236
245
|
* Persists the per-connection success attributes from the server (LID,
|
|
237
246
|
* display name, companion key, last-success ts, props versions, ...). Only
|
|
238
247
|
* persists when at least one attribute actually changed.
|
|
248
|
+
*
|
|
249
|
+
* Pass `countsAsLogin` only for the server `success` node: it advances the
|
|
250
|
+
* login counters advertised as `lc` / `connectAttemptCount`. Callers that
|
|
251
|
+
* merely reuse this to persist a single attribute (a push-name change, for
|
|
252
|
+
* instance) must leave it unset.
|
|
239
253
|
*/
|
|
240
|
-
async persistSuccessAttributes(attributes) {
|
|
254
|
+
async persistSuccessAttributes(attributes, { countsAsLogin = false } = {}) {
|
|
255
|
+
if (countsAsLogin) {
|
|
256
|
+
this.connectAttemptCount += 1;
|
|
257
|
+
}
|
|
241
258
|
let persistDiff;
|
|
242
259
|
const computeDiff = (current, next) => ({
|
|
260
|
+
loginCounterChanged: next.loginCounter !== current.loginCounter,
|
|
243
261
|
lidChanged: next.meLid !== current.meLid,
|
|
244
262
|
displayNameChanged: next.meDisplayName !== current.meDisplayName,
|
|
245
263
|
companionChanged: (current.companionEncStatic === undefined) !==
|
|
@@ -259,6 +277,9 @@ export class WaAuthClient {
|
|
|
259
277
|
meDisplayName: attributes.meDisplayName ?? credentials.meDisplayName,
|
|
260
278
|
companionEncStatic: attributes.companionEncStatic ?? credentials.companionEncStatic,
|
|
261
279
|
lastSuccessTs: attributes.lastSuccessTs ?? credentials.lastSuccessTs,
|
|
280
|
+
loginCounter: countsAsLogin
|
|
281
|
+
? (credentials.loginCounter ?? 0) + 1
|
|
282
|
+
: credentials.loginCounter,
|
|
262
283
|
propsVersion: attributes.propsVersion ?? credentials.propsVersion,
|
|
263
284
|
abPropsVersion: attributes.abPropsVersion ?? credentials.abPropsVersion,
|
|
264
285
|
connectionLocation: attributes.connectionLocation ?? credentials.connectionLocation,
|
|
@@ -174,6 +174,8 @@ export async function buildCommsConfig(logger, credentials, socketOptions, clien
|
|
|
174
174
|
? {
|
|
175
175
|
username: loginIdentity.username,
|
|
176
176
|
device: loginIdentity.device,
|
|
177
|
+
loginCounter: credentials.loginCounter ?? 0,
|
|
178
|
+
connectAttemptCount: clientOptions.connectAttemptCount ?? 0,
|
|
177
179
|
deviceBrowser: clientOptions.deviceBrowser,
|
|
178
180
|
deviceOsDisplayName: clientOptions.deviceOsDisplayName,
|
|
179
181
|
versionBase
|
|
@@ -185,6 +187,7 @@ export async function buildCommsConfig(logger, credentials, socketOptions, clien
|
|
|
185
187
|
signedPreKey: credentials.signedPreKey,
|
|
186
188
|
deviceBrowser: clientOptions.deviceBrowser,
|
|
187
189
|
deviceOsDisplayName: clientOptions.deviceOsDisplayName,
|
|
190
|
+
deviceOsVersion: clientOptions.deviceOsVersion,
|
|
188
191
|
requireFullSync: clientOptions.requireFullSync,
|
|
189
192
|
versionBase
|
|
190
193
|
}
|
|
@@ -5,13 +5,18 @@ import { getWaBrowserDisplayName } from '../../protocol/browser.js';
|
|
|
5
5
|
import { WA_DEFAULTS, WA_IQ_TYPES, WA_NODE_TAGS, WA_SIGNALING } from '../../protocol/constants.js';
|
|
6
6
|
import { parsePhoneJid } from '../../protocol/jid.js';
|
|
7
7
|
import { ADV_PREFIX_HOSTED_ACCOUNT_SIGNATURE, computeAdvIdentityHmac, generateDeviceSignature, verifyDeviceIdentityAccountSignature } from '../../signal/attestation/WaAdvSignature.js';
|
|
8
|
-
import { buildAckNode, buildIqResultNode } from '../../transport/node/builders/global.js';
|
|
8
|
+
import { buildAckNode, buildIqErrorNode, buildIqResultNode } from '../../transport/node/builders/global.js';
|
|
9
9
|
import { buildCompanionFinishRequestNode, buildCompanionHelloRequestNode, buildGetCountryCodeRequestNode } from '../../transport/node/builders/pairing.js';
|
|
10
10
|
import { decodeNodeContentUtf8OrBytes, findNodeChild, findNodeChildrenByTags, getFirstNodeChild, getNodeChildrenNonEmptyUtf8ByTag, hasNodeChild } from '../../transport/node/helpers.js';
|
|
11
11
|
import { assertIqResult } from '../../transport/node/query.js';
|
|
12
12
|
import { concatBytes, decodeProtoBytes, uint8Equal, uint8TimingSafeEqual } from '../../util/bytes.js';
|
|
13
|
+
import { toError } from '../../util/primitives.js';
|
|
14
|
+
/** Server-enforced bounds on the `pair-success` `device-identity` payload. */
|
|
15
|
+
const PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES = 1;
|
|
16
|
+
const PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES = 500;
|
|
13
17
|
export class WaPairingFlow {
|
|
14
18
|
constructor(options) {
|
|
19
|
+
this.pairSuccessInFlight = false;
|
|
15
20
|
this.opts = options;
|
|
16
21
|
this.pairingSession = null;
|
|
17
22
|
}
|
|
@@ -30,6 +35,7 @@ export class WaPairingFlow {
|
|
|
30
35
|
const normalizedCustomCode = customCode !== undefined ? normalizeCustomPairingCode(customCode) : undefined;
|
|
31
36
|
const credentials = this.requireCredentials();
|
|
32
37
|
const phoneJid = parsePhoneJid(phoneNumber);
|
|
38
|
+
const createdAtSeconds = Math.floor(Date.now() / 1000);
|
|
33
39
|
const [companionHello, refreshedCredentials] = await Promise.all([
|
|
34
40
|
createCompanionHello({ customCode: normalizedCustomCode }),
|
|
35
41
|
this.rotateAdvSecret(credentials)
|
|
@@ -61,7 +67,7 @@ export class WaPairingFlow {
|
|
|
61
67
|
pairingCode: companionHello.pairingCode,
|
|
62
68
|
phoneJid,
|
|
63
69
|
ref,
|
|
64
|
-
createdAtSeconds
|
|
70
|
+
createdAtSeconds,
|
|
65
71
|
companionEphemeralKeyPair: companionHello.companionEphemeralKeyPair,
|
|
66
72
|
attempts: 0,
|
|
67
73
|
finished: false
|
|
@@ -163,6 +169,23 @@ export class WaPairingFlow {
|
|
|
163
169
|
this.opts.logger.debug('pair-device refs updated', { refsCount: refs.length });
|
|
164
170
|
}
|
|
165
171
|
async handlePairSuccess(iqNode, pairSuccessNode) {
|
|
172
|
+
if (this.pairSuccessInFlight) {
|
|
173
|
+
this.opts.logger.debug('pair-success ignored: already processing');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (this.opts.auth.getCredentials()?.meJid) {
|
|
177
|
+
this.opts.logger.debug('pair-success ignored: session already registered');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.pairSuccessInFlight = true;
|
|
181
|
+
try {
|
|
182
|
+
await this.completePairSuccess(iqNode, pairSuccessNode);
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
this.pairSuccessInFlight = false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async completePairSuccess(iqNode, pairSuccessNode) {
|
|
166
189
|
this.opts.logger.debug('processing pair-success node');
|
|
167
190
|
const credentials = this.requireCredentials();
|
|
168
191
|
const [deviceIdentityNode, deviceNode, platformNode] = findNodeChildrenByTags(pairSuccessNode, [WA_NODE_TAGS.DEVICE_IDENTITY, 'device', WA_NODE_TAGS.PLATFORM]);
|
|
@@ -174,7 +197,12 @@ export class WaPairingFlow {
|
|
|
174
197
|
});
|
|
175
198
|
throw new Error('pair-success stanza is missing required nodes');
|
|
176
199
|
}
|
|
177
|
-
const
|
|
200
|
+
const deviceIdentityBytes = decodeNodeContentUtf8OrBytes(deviceIdentityNode.content, 'pair-success.device-identity');
|
|
201
|
+
if (deviceIdentityBytes.length < PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES ||
|
|
202
|
+
deviceIdentityBytes.length > PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES) {
|
|
203
|
+
throw new Error(`pair-success device-identity must be ${PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES}-${PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES} bytes, got ${deviceIdentityBytes.length}`);
|
|
204
|
+
}
|
|
205
|
+
const wrappedIdentity = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityBytes);
|
|
178
206
|
const wrappedDetails = decodeProtoBytes(wrappedIdentity.details, 'ADVSignedDeviceIdentityHMAC.details');
|
|
179
207
|
const wrappedHmac = decodeProtoBytes(wrappedIdentity.hmac, 'ADVSignedDeviceIdentityHMAC.hmac');
|
|
180
208
|
const accountType = wrappedIdentity.accountType ?? proto.ADVEncryptionType.E2EE;
|
|
@@ -191,16 +219,23 @@ export class WaPairingFlow {
|
|
|
191
219
|
platform: platformNode.attrs.name,
|
|
192
220
|
isHosted
|
|
193
221
|
});
|
|
222
|
+
await this.sendPairSuccessValidationError(iqNode);
|
|
194
223
|
throw new Error('pair-success HMAC validation failed');
|
|
195
224
|
}
|
|
196
225
|
}
|
|
197
|
-
const
|
|
226
|
+
const built = await this.buildPairSuccessResponseIdentity(credentials, wrappedDetails);
|
|
227
|
+
if (!built) {
|
|
228
|
+
await this.sendPairSuccessValidationError(iqNode);
|
|
229
|
+
throw new Error('pair-success account signature validation failed');
|
|
230
|
+
}
|
|
231
|
+
const { signedIdentity, keyIndex, responseIdentityBytes } = built;
|
|
198
232
|
const nextCredentials = {
|
|
199
233
|
...credentials,
|
|
200
234
|
signedIdentity,
|
|
201
235
|
meJid: deviceNode.attrs.jid,
|
|
202
236
|
meLid: deviceNode.attrs.lid,
|
|
203
|
-
platform: platformNode.attrs.name
|
|
237
|
+
platform: platformNode.attrs.name,
|
|
238
|
+
loginCounter: 0
|
|
204
239
|
};
|
|
205
240
|
await this.opts.auth.updateCredentials(nextCredentials);
|
|
206
241
|
this.opts.logger.info('pair-success credentials updated', {
|
|
@@ -235,6 +270,7 @@ export class WaPairingFlow {
|
|
|
235
270
|
this.opts.callbacks.emitPaired(nextCredentials);
|
|
236
271
|
this.opts.logger.debug('pair-success completed and paired event emitted');
|
|
237
272
|
}
|
|
273
|
+
/** Returns `null` when the primary's account signature does not verify. */
|
|
238
274
|
async buildPairSuccessResponseIdentity(credentials, wrappedDetails) {
|
|
239
275
|
const signedIdentity = proto.ADVSignedDeviceIdentity.decode(wrappedDetails);
|
|
240
276
|
const details = decodeProtoBytes(signedIdentity.details, 'ADVSignedDeviceIdentity.details');
|
|
@@ -250,7 +286,7 @@ export class WaPairingFlow {
|
|
|
250
286
|
keyIndex: advDeviceIdentity.keyIndex ?? 0,
|
|
251
287
|
isDeviceHosted
|
|
252
288
|
});
|
|
253
|
-
|
|
289
|
+
return null;
|
|
254
290
|
}
|
|
255
291
|
}
|
|
256
292
|
signedIdentity.deviceSignature = await generateDeviceSignature(details, localIdentity, accountSignatureKey, false);
|
|
@@ -265,19 +301,35 @@ export class WaPairingFlow {
|
|
|
265
301
|
responseIdentityBytes
|
|
266
302
|
};
|
|
267
303
|
}
|
|
304
|
+
/** Mirrors the `not-authorized` IQ error WhatsApp Web returns on a failed pair-success. */
|
|
305
|
+
async sendPairSuccessValidationError(iqNode) {
|
|
306
|
+
try {
|
|
307
|
+
await this.opts.socket.sendNode(buildIqErrorNode(iqNode, { code: 401, text: 'not-authorized' }));
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
this.opts.logger.warn('failed to send pair-success error response', {
|
|
311
|
+
message: toError(error).message
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
268
315
|
async handlePrimaryHello(linkCodeNode) {
|
|
269
|
-
|
|
316
|
+
let credentials = this.requireCredentials();
|
|
270
317
|
const pairingSession = this.pairingSession;
|
|
271
|
-
if (!pairingSession
|
|
318
|
+
if (!pairingSession) {
|
|
272
319
|
this.opts.logger.trace('primary_hello ignored: no active session');
|
|
273
320
|
return;
|
|
274
321
|
}
|
|
275
322
|
pairingSession.attempts += 1;
|
|
276
323
|
this.opts.logger.debug('processing primary_hello', {
|
|
277
|
-
attempts: pairingSession.attempts
|
|
324
|
+
attempts: pairingSession.attempts,
|
|
325
|
+
finished: pairingSession.finished
|
|
278
326
|
});
|
|
279
|
-
if (pairingSession.
|
|
280
|
-
|
|
327
|
+
if (pairingSession.finished) {
|
|
328
|
+
if (pairingSession.attempts > WA_DEFAULTS.PAIRING_CODE_MAX_PRIMARY_HELLOS) {
|
|
329
|
+
throw new Error('pairing code exceeded maximum primary hello attempts');
|
|
330
|
+
}
|
|
331
|
+
credentials = await this.rotateAdvSecret(credentials);
|
|
332
|
+
pairingSession.finished = false;
|
|
281
333
|
}
|
|
282
334
|
const [refNode, wrappedPrimaryNode, primaryIdentityNode] = findNodeChildrenByTags(linkCodeNode, [
|
|
283
335
|
WA_NODE_TAGS.LINK_CODE_PAIRING_REF,
|
|
@@ -306,6 +358,10 @@ export class WaPairingFlow {
|
|
|
306
358
|
companionEphemeralPrivKey: pairingSession.companionEphemeralKeyPair.privKey,
|
|
307
359
|
registrationIdentityKeyPair: credentials.registrationInfo.identityKeyPair
|
|
308
360
|
});
|
|
361
|
+
await this.opts.auth.updateCredentials({
|
|
362
|
+
...credentials,
|
|
363
|
+
advSecretKey: finish.advSecret
|
|
364
|
+
});
|
|
309
365
|
const result = await this.opts.socket.query(buildCompanionFinishRequestNode({
|
|
310
366
|
phoneJid: pairingSession.phoneJid,
|
|
311
367
|
wrappedKeyBundle: finish.wrappedKeyBundle,
|
|
@@ -315,10 +371,6 @@ export class WaPairingFlow {
|
|
|
315
371
|
if (result.attrs.type === WA_IQ_TYPES.ERROR) {
|
|
316
372
|
throw new Error('companion_finish returned error');
|
|
317
373
|
}
|
|
318
|
-
await this.opts.auth.updateCredentials({
|
|
319
|
-
...credentials,
|
|
320
|
-
advSecretKey: finish.advSecret
|
|
321
|
-
});
|
|
322
374
|
pairingSession.finished = true;
|
|
323
375
|
this.opts.logger.debug('primary_hello completed with companion_finish success');
|
|
324
376
|
}
|