@japofc/baileys 2.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/NOTICE.md +111 -0
- package/README.md +1254 -0
- package/WAProto/index.d.ts +9 -0
- package/WAProto/index.js +142276 -0
- package/engine-requirements.js +13 -0
- package/lib/Builders/A2UI.d.ts +242 -0
- package/lib/Builders/A2UI.js +308 -0
- package/lib/Builders/AIRich.d.ts +105 -0
- package/lib/Builders/AIRich.js +2286 -0
- package/lib/Builders/Button.d.ts +103 -0
- package/lib/Builders/Button.js +883 -0
- package/lib/Builders/ButtonV2.d.ts +28 -0
- package/lib/Builders/ButtonV2.js +144 -0
- package/lib/Builders/ButtonV3.d.ts +25 -0
- package/lib/Builders/ButtonV3.js +184 -0
- package/lib/Builders/Carousel.d.ts +22 -0
- package/lib/Builders/Carousel.js +111 -0
- package/lib/Builders/JapBaileys.d.ts +34 -0
- package/lib/Builders/JapBaileys.js +108 -0
- package/lib/Builders/Poll.d.ts +25 -0
- package/lib/Builders/Poll.js +128 -0
- package/lib/Builders/index.d.ts +17 -0
- package/lib/Builders/index.js +11 -0
- package/lib/Builders/shared.d.ts +67 -0
- package/lib/Builders/shared.js +652 -0
- package/lib/Defaults/index.d.ts +154 -0
- package/lib/Defaults/index.js +178 -0
- package/lib/Framework/Bot.d.ts +61 -0
- package/lib/Framework/Bot.js +248 -0
- package/lib/Framework/Context.d.ts +21 -0
- package/lib/Framework/Context.js +89 -0
- package/lib/Framework/MediaManager.d.ts +17 -0
- package/lib/Framework/MediaManager.js +151 -0
- package/lib/Framework/SessionManager.d.ts +13 -0
- package/lib/Framework/SessionManager.js +33 -0
- package/lib/Framework/StatsManager.d.ts +26 -0
- package/lib/Framework/StatsManager.js +119 -0
- package/lib/Framework/Store/SQLiteStore.d.ts +11 -0
- package/lib/Framework/Store/SQLiteStore.js +73 -0
- package/lib/Framework/index.d.ts +9 -0
- package/lib/Framework/index.js +10 -0
- package/lib/Signal/Group/ciphertext-message.d.ts +9 -0
- package/lib/Signal/Group/ciphertext-message.js +11 -0
- package/lib/Signal/Group/group-session-builder.d.ts +7 -0
- package/lib/Signal/Group/group-session-builder.js +29 -0
- package/lib/Signal/Group/group_cipher.d.ts +10 -0
- package/lib/Signal/Group/group_cipher.js +81 -0
- package/lib/Signal/Group/index.d.ts +11 -0
- package/lib/Signal/Group/index.js +11 -0
- package/lib/Signal/Group/keyhelper.d.ts +6 -0
- package/lib/Signal/Group/keyhelper.js +17 -0
- package/lib/Signal/Group/sender-chain-key.d.ts +13 -0
- package/lib/Signal/Group/sender-chain-key.js +25 -0
- package/lib/Signal/Group/sender-key-distribution-message.d.ts +16 -0
- package/lib/Signal/Group/sender-key-distribution-message.js +62 -0
- package/lib/Signal/Group/sender-key-message.d.ts +18 -0
- package/lib/Signal/Group/sender-key-message.js +65 -0
- package/lib/Signal/Group/sender-key-name.d.ts +11 -0
- package/lib/Signal/Group/sender-key-name.js +47 -0
- package/lib/Signal/Group/sender-key-record.d.ts +12 -0
- package/lib/Signal/Group/sender-key-record.js +40 -0
- package/lib/Signal/Group/sender-key-state.d.ts +16 -0
- package/lib/Signal/Group/sender-key-state.js +83 -0
- package/lib/Signal/Group/sender-message-key.d.ts +11 -0
- package/lib/Signal/Group/sender-message-key.js +25 -0
- package/lib/Signal/libsignal.d.ts +54 -0
- package/lib/Signal/libsignal.js +430 -0
- package/lib/Signal/lid-mapping.d.ts +20 -0
- package/lib/Signal/lid-mapping.js +276 -0
- package/lib/Socket/Client/index.d.ts +2 -0
- package/lib/Socket/Client/index.js +2 -0
- package/lib/Socket/Client/types.d.ts +5 -0
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.d.ts +12 -0
- package/lib/Socket/Client/websocket.js +53 -0
- package/lib/Socket/business.d.ts +395 -0
- package/lib/Socket/business.js +379 -0
- package/lib/Socket/chats.d.ts +136 -0
- package/lib/Socket/chats.js +1327 -0
- package/lib/Socket/communities.d.ts +562 -0
- package/lib/Socket/communities.js +439 -0
- package/lib/Socket/dugong.d.ts +117 -0
- package/lib/Socket/dugong.js +798 -0
- package/lib/Socket/groups.d.ts +285 -0
- package/lib/Socket/groups.js +354 -0
- package/lib/Socket/index.d.ts +585 -0
- package/lib/Socket/index.js +52 -0
- package/lib/Socket/messages-recv.d.ts +333 -0
- package/lib/Socket/messages-recv.js +1781 -0
- package/lib/Socket/messages-send.d.ts +353 -0
- package/lib/Socket/messages-send.js +1524 -0
- package/lib/Socket/mex.d.ts +1 -0
- package/lib/Socket/mex.js +41 -0
- package/lib/Socket/newsletter.d.ts +286 -0
- package/lib/Socket/newsletter.js +364 -0
- package/lib/Socket/socket.d.ts +60 -0
- package/lib/Socket/socket.js +1012 -0
- package/lib/Socket/username.d.ts +82 -0
- package/lib/Socket/username.js +159 -0
- package/lib/Store/index.d.ts +3 -0
- package/lib/Store/index.js +3 -0
- package/lib/Store/make-in-memory-store.d.ts +62 -0
- package/lib/Store/make-in-memory-store.js +428 -0
- package/lib/Store/make-ordered-dictionary.d.ts +12 -0
- package/lib/Store/make-ordered-dictionary.js +78 -0
- package/lib/Store/object-repository.d.ts +10 -0
- package/lib/Store/object-repository.js +23 -0
- package/lib/Types/Auth.d.ts +117 -0
- package/lib/Types/Auth.js +1 -0
- package/lib/Types/Bussines.d.ts +25 -0
- package/lib/Types/Call.d.ts +15 -0
- package/lib/Types/Call.js +1 -0
- package/lib/Types/Chat.d.ts +124 -0
- package/lib/Types/Chat.js +7 -0
- package/lib/Types/Contact.d.ts +26 -0
- package/lib/Types/Contact.js +1 -0
- package/lib/Types/Events.d.ts +256 -0
- package/lib/Types/Events.js +1 -0
- package/lib/Types/GroupMetadata.d.ts +71 -0
- package/lib/Types/GroupMetadata.js +1 -0
- package/lib/Types/Label.d.ts +5 -0
- package/lib/Types/Label.js +24 -0
- package/lib/Types/LabelAssociation.d.ts +6 -0
- package/lib/Types/LabelAssociation.js +6 -0
- package/lib/Types/Message.d.ts +62 -0
- package/lib/Types/Message.js +17 -0
- package/lib/Types/Mex.d.ts +2 -0
- package/lib/Types/Mex.js +38 -0
- package/lib/Types/Product.d.ts +79 -0
- package/lib/Types/Product.js +1 -0
- package/lib/Types/RichType.d.ts +2 -0
- package/lib/Types/RichType.js +22 -0
- package/lib/Types/Signal.d.ts +87 -0
- package/lib/Types/Signal.js +1 -0
- package/lib/Types/Socket.d.ts +138 -0
- package/lib/Types/Socket.js +1 -0
- package/lib/Types/State.d.ts +8 -0
- package/lib/Types/State.js +55 -0
- package/lib/Types/USync.d.ts +26 -0
- package/lib/Types/index.d.ts +68 -0
- package/lib/Types/index.js +27 -0
- package/lib/Utils/MessageBuilder.d.ts +2 -0
- package/lib/Utils/MessageBuilder.js +2 -0
- package/lib/Utils/PersistentStore.d.ts +60 -0
- package/lib/Utils/PersistentStore.js +592 -0
- package/lib/Utils/anti-delete.d.ts +74 -0
- package/lib/Utils/anti-delete.js +221 -0
- package/lib/Utils/auth-secure.d.ts +86 -0
- package/lib/Utils/auth-secure.js +764 -0
- package/lib/Utils/auth-utils.d.ts +57 -0
- package/lib/Utils/auth-utils.js +306 -0
- package/lib/Utils/auto-reply.d.ts +47 -0
- package/lib/Utils/auto-reply.js +157 -0
- package/lib/Utils/baileys-event-stream.d.ts +8 -0
- package/lib/Utils/baileys-event-stream.js +62 -0
- package/lib/Utils/browser-utils.d.ts +8 -0
- package/lib/Utils/browser-utils.js +27 -0
- package/lib/Utils/business.d.ts +49 -0
- package/lib/Utils/business.js +239 -0
- package/lib/Utils/button-helper-utils.d.ts +53 -0
- package/lib/Utils/button-helper-utils.js +312 -0
- package/lib/Utils/button-sender.d.ts +54 -0
- package/lib/Utils/button-sender.js +822 -0
- package/lib/Utils/chat-control.d.ts +159 -0
- package/lib/Utils/chat-control.js +230 -0
- package/lib/Utils/chat-history-helpers.d.ts +21 -0
- package/lib/Utils/chat-history-helpers.js +70 -0
- package/lib/Utils/chat-utils.d.ts +407 -0
- package/lib/Utils/chat-utils.js +885 -0
- package/lib/Utils/companion-reg-client-utils.d.ts +4 -0
- package/lib/Utils/companion-reg-client-utils.js +39 -0
- package/lib/Utils/crypto.d.ts +41 -0
- package/lib/Utils/crypto.js +147 -0
- package/lib/Utils/decode-wa-message.d.ts +83 -0
- package/lib/Utils/decode-wa-message.js +316 -0
- package/lib/Utils/event-buffer.d.ts +12 -0
- package/lib/Utils/event-buffer.js +635 -0
- package/lib/Utils/generics.d.ts +69 -0
- package/lib/Utils/generics.js +438 -0
- package/lib/Utils/history-share.d.ts +11 -0
- package/lib/Utils/history-share.js +70 -0
- package/lib/Utils/history.d.ts +90 -0
- package/lib/Utils/history.js +146 -0
- package/lib/Utils/humanizer.d.ts +12 -0
- package/lib/Utils/humanizer.js +73 -0
- package/lib/Utils/identity-change-handler.d.ts +13 -0
- package/lib/Utils/identity-change-handler.js +49 -0
- package/lib/Utils/index.d.ts +57 -0
- package/lib/Utils/index.js +56 -0
- package/lib/Utils/jid-tools.d.ts +6 -0
- package/lib/Utils/jid-tools.js +77 -0
- package/lib/Utils/link-preview.d.ts +12 -0
- package/lib/Utils/link-preview.js +84 -0
- package/lib/Utils/logger.d.ts +4 -0
- package/lib/Utils/logger.js +2 -0
- package/lib/Utils/lt-hash.d.ts +12 -0
- package/lib/Utils/lt-hash.js +26 -0
- package/lib/Utils/make-mutex.d.ts +6 -0
- package/lib/Utils/make-mutex.js +32 -0
- package/lib/Utils/media-messages.d.ts +18 -0
- package/lib/Utils/media-messages.js +70 -0
- package/lib/Utils/media-set.d.ts +13 -0
- package/lib/Utils/media-set.js +171 -0
- package/lib/Utils/message-kind.d.ts +32 -0
- package/lib/Utils/message-kind.js +136 -0
- package/lib/Utils/message-retry-manager.d.ts +79 -0
- package/lib/Utils/message-retry-manager.js +264 -0
- package/lib/Utils/message-search.d.ts +44 -0
- package/lib/Utils/message-search.js +188 -0
- package/lib/Utils/messages-media.d.ts +135 -0
- package/lib/Utils/messages-media.js +868 -0
- package/lib/Utils/messages.d.ts +44 -0
- package/lib/Utils/messages.js +2326 -0
- package/lib/Utils/meta-ai.d.ts +13 -0
- package/lib/Utils/meta-ai.js +74 -0
- package/lib/Utils/mini-app.d.ts +35 -0
- package/lib/Utils/mini-app.js +213 -0
- package/lib/Utils/noise-handler.d.ts +20 -0
- package/lib/Utils/noise-handler.js +200 -0
- package/lib/Utils/offline-node-processor.d.ts +9 -0
- package/lib/Utils/offline-node-processor.js +39 -0
- package/lib/Utils/past-participants.d.ts +14 -0
- package/lib/Utils/past-participants.js +40 -0
- package/lib/Utils/pre-key-manager.d.ts +25 -0
- package/lib/Utils/pre-key-manager.js +105 -0
- package/lib/Utils/process-message.d.ts +82 -0
- package/lib/Utils/process-message.js +808 -0
- package/lib/Utils/qr-render.d.ts +59 -0
- package/lib/Utils/qr-render.js +179 -0
- package/lib/Utils/qrcodegen.d.ts +79 -0
- package/lib/Utils/qrcodegen.js +848 -0
- package/lib/Utils/reporting-utils.d.ts +12 -0
- package/lib/Utils/reporting-utils.js +257 -0
- package/lib/Utils/rich-message-utils.d.ts +48 -0
- package/lib/Utils/rich-message-utils.js +444 -0
- package/lib/Utils/router.d.ts +37 -0
- package/lib/Utils/router.js +165 -0
- package/lib/Utils/scheduled-posts.d.ts +30 -0
- package/lib/Utils/scheduled-posts.js +110 -0
- package/lib/Utils/scheduling.d.ts +42 -0
- package/lib/Utils/scheduling.js +139 -0
- package/lib/Utils/signal.d.ts +82 -0
- package/lib/Utils/signal.js +200 -0
- package/lib/Utils/stanza-ack.d.ts +16 -0
- package/lib/Utils/stanza-ack.js +37 -0
- package/lib/Utils/status.d.ts +50 -0
- package/lib/Utils/status.js +107 -0
- package/lib/Utils/stickerpack.d.ts +51 -0
- package/lib/Utils/stickerpack.js +274 -0
- package/lib/Utils/sync-action-utils.d.ts +2 -0
- package/lib/Utils/sync-action-utils.js +48 -0
- package/lib/Utils/tag.d.ts +12 -0
- package/lib/Utils/tag.js +44 -0
- package/lib/Utils/tc-token-utils.d.ts +29 -0
- package/lib/Utils/tc-token-utils.js +162 -0
- package/lib/Utils/templates.d.ts +76 -0
- package/lib/Utils/templates.js +150 -0
- package/lib/Utils/transcribe.d.ts +12 -0
- package/lib/Utils/transcribe.js +107 -0
- package/lib/Utils/use-cache-manager-auth-state.d.ts +13 -0
- package/lib/Utils/use-cache-manager-auth-state.js +81 -0
- package/lib/Utils/use-multi-file-auth-state.d.ts +10 -0
- package/lib/Utils/use-multi-file-auth-state.js +166 -0
- package/lib/Utils/use-single-file-auth-state.d.ts +10 -0
- package/lib/Utils/use-single-file-auth-state.js +113 -0
- package/lib/Utils/use-sqlite-auth-state.d.ts +11 -0
- package/lib/Utils/use-sqlite-auth-state.js +169 -0
- package/lib/Utils/validate-connection.d.ts +42 -0
- package/lib/Utils/validate-connection.js +198 -0
- package/lib/Utils/vcard.d.ts +58 -0
- package/lib/Utils/vcard.js +104 -0
- package/lib/Utils/voice-note.d.ts +15 -0
- package/lib/Utils/voice-note.js +63 -0
- package/lib/VoIP/audio-feeder.d.ts +18 -0
- package/lib/VoIP/audio-feeder.js +160 -0
- package/lib/VoIP/call-recorder.d.ts +13 -0
- package/lib/VoIP/call-recorder.js +73 -0
- package/lib/VoIP/index.d.ts +86 -0
- package/lib/VoIP/index.js +611 -0
- package/lib/VoIP/relay-transport.d.ts +43 -0
- package/lib/VoIP/relay-transport.js +563 -0
- package/lib/VoIP/signaling.d.ts +26 -0
- package/lib/VoIP/signaling.js +662 -0
- package/lib/VoIP/types.d.ts +69 -0
- package/lib/VoIP/types.js +17 -0
- package/lib/VoIP/wasm-engine.d.ts +120 -0
- package/lib/VoIP/wasm-engine.js +1358 -0
- package/lib/VoIP/worker-bootstrap.d.ts +4 -0
- package/lib/VoIP/worker-bootstrap.js +1046 -0
- package/lib/WABinary/constants.d.ts +61 -0
- package/lib/WABinary/constants.js +1466 -0
- package/lib/WABinary/decode.d.ts +9 -0
- package/lib/WABinary/decode.js +261 -0
- package/lib/WABinary/encode.d.ts +2 -0
- package/lib/WABinary/encode.js +219 -0
- package/lib/WABinary/generic-utils.d.ts +74 -0
- package/lib/WABinary/generic-utils.js +276 -0
- package/lib/WABinary/index.d.ts +5 -0
- package/lib/WABinary/index.js +5 -0
- package/lib/WABinary/jid-utils.d.ts +28 -0
- package/lib/WABinary/jid-utils.js +95 -0
- package/lib/WABinary/types.d.ts +19 -0
- package/lib/WABinary/types.js +1 -0
- package/lib/WAM/BinaryInfo.d.ts +7 -0
- package/lib/WAM/BinaryInfo.js +9 -0
- package/lib/WAM/constants.d.ts +34926 -0
- package/lib/WAM/constants.js +22852 -0
- package/lib/WAM/encode.d.ts +1 -0
- package/lib/WAM/encode.js +149 -0
- package/lib/WAM/index.d.ts +3 -0
- package/lib/WAM/index.js +3 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts +36 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +51 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts +29 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +62 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +12 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts +12 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +9 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +24 -0
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts +38 -0
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -0
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.d.ts +14 -0
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -0
- package/lib/WAUSync/Protocols/index.d.ts +7 -0
- package/lib/WAUSync/Protocols/index.js +7 -0
- package/lib/WAUSync/USyncQuery.d.ts +20 -0
- package/lib/WAUSync/USyncQuery.js +121 -0
- package/lib/WAUSync/USyncUser.d.ts +16 -0
- package/lib/WAUSync/USyncUser.js +30 -0
- package/lib/WAUSync/index.d.ts +3 -0
- package/lib/WAUSync/index.js +3 -0
- package/lib/assets/wasm/loader.d.ts +5 -0
- package/lib/assets/wasm/loader.js +5 -0
- package/lib/assets/wasm/whatsapp.wasm +0 -0
- package/lib/assets/wasm/worker-modules.d.ts +5 -0
- package/lib/assets/wasm/worker-modules.js +273 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +23 -0
- package/package.json +180 -0
- package/postinstall-banner.js +82 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JAP Baileys — Security Pack (auth at-rest encryption, backup, integrity, guards)
|
|
3
|
+
*
|
|
4
|
+
* Covers: encrypted auth-state variants, encrypted backup/restore, integrity
|
|
5
|
+
* checksums, corruption repair, pairing-code rate limiting, QR secret
|
|
6
|
+
* protection, secret-redacting logger, and secure logout wiping.
|
|
7
|
+
*
|
|
8
|
+
* Item map:
|
|
9
|
+
* (1) auth-state encryption → useEncryptedFileAuthState / useEncryptedSingleFileAuthState
|
|
10
|
+
* (2) pairing rate limit → createRateLimiter / withPairingGuard
|
|
11
|
+
* (3) QR protection → createQRGuard / redactSecrets
|
|
12
|
+
* (4) secure logout → secureLogout
|
|
13
|
+
* (5) auth/Signal regression → tests/security.test.js
|
|
14
|
+
* (6) encrypted backup → backupAuthState / restoreAuthState
|
|
15
|
+
* (7) secret redaction → redactSecrets / secureLogger
|
|
16
|
+
* (8) session integrity → writeAuthIntegrity / verifyAuthIntegrity
|
|
17
|
+
* (9) corruption recovery → repairAuthState
|
|
18
|
+
* (10) security + fuzz tests → tests/security.test.js
|
|
19
|
+
*/
|
|
20
|
+
import { randomBytes, scryptSync, createCipheriv, createDecipheriv, createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
21
|
+
import { promises as fs } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { Mutex } from 'async-mutex';
|
|
24
|
+
import { BufferJSON } from './generics.js';
|
|
25
|
+
import { initAuthCreds } from './auth-utils.js';
|
|
26
|
+
import { proto } from '../../WAProto/index.js';
|
|
27
|
+
|
|
28
|
+
/** Magic prefix for JAP encrypted JSON payloads. */
|
|
29
|
+
export const ENC_MAGIC = 'JAPENC1';
|
|
30
|
+
const ENC_ALGO = 'aes-256-gcm';
|
|
31
|
+
const SCRYPT_KEYLEN = 32;
|
|
32
|
+
const SALT_LEN = 16;
|
|
33
|
+
const IV_LEN = 12;
|
|
34
|
+
|
|
35
|
+
/** Derive a 32-byte key from a password (scrypt, per-file random salt). */
|
|
36
|
+
export function deriveKey(password, salt) {
|
|
37
|
+
if (!password || typeof password !== 'string') {
|
|
38
|
+
throw new Error('auth-secure: password must be a non-empty string');
|
|
39
|
+
}
|
|
40
|
+
return scryptSync(password, salt, SCRYPT_KEYLEN, { N: 16384, r: 8, p: 1 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Encrypt a JSON-serialisable value.
|
|
45
|
+
* @returns {string} `JAPENC1.<saltB64>.<ivB64u>.<tagB64u>.<ctB64u>` (URL-safe parts)
|
|
46
|
+
*/
|
|
47
|
+
export function encryptJSON(value, password) {
|
|
48
|
+
const salt = randomBytes(SALT_LEN);
|
|
49
|
+
const iv = randomBytes(IV_LEN);
|
|
50
|
+
const key = deriveKey(password, salt);
|
|
51
|
+
const cipher = createCipheriv(ENC_ALGO, key, iv);
|
|
52
|
+
const plain = Buffer.from(JSON.stringify(value, BufferJSON.replacer));
|
|
53
|
+
const ct = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
54
|
+
const tag = cipher.getAuthTag();
|
|
55
|
+
const b64u = (b) => b.toString('base64url');
|
|
56
|
+
const out = [ENC_MAGIC, b64u(salt), b64u(iv), b64u(tag), b64u(ct)].join('.');
|
|
57
|
+
key.fill(0);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Decrypt a payload produced by {@link encryptJSON}.
|
|
63
|
+
* @throws when the password is wrong or the payload was tampered with.
|
|
64
|
+
*/
|
|
65
|
+
export function decryptJSON(payload, password) {
|
|
66
|
+
if (typeof payload !== 'string' || !payload.startsWith(ENC_MAGIC + '.')) {
|
|
67
|
+
throw new Error('auth-secure: not a JAP encrypted payload');
|
|
68
|
+
}
|
|
69
|
+
const parts = payload.split('.');
|
|
70
|
+
if (parts.length !== 5) {
|
|
71
|
+
throw new Error('auth-secure: malformed encrypted payload');
|
|
72
|
+
}
|
|
73
|
+
const [, saltB64, ivB64, tagB64, ctB64] = parts;
|
|
74
|
+
let salt;
|
|
75
|
+
let iv;
|
|
76
|
+
let tag;
|
|
77
|
+
let ct;
|
|
78
|
+
try {
|
|
79
|
+
salt = Buffer.from(saltB64, 'base64url');
|
|
80
|
+
iv = Buffer.from(ivB64, 'base64url');
|
|
81
|
+
tag = Buffer.from(tagB64, 'base64url');
|
|
82
|
+
ct = Buffer.from(ctB64, 'base64url');
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error('auth-secure: malformed encrypted payload encoding');
|
|
85
|
+
}
|
|
86
|
+
if (salt.length !== SALT_LEN || iv.length !== IV_LEN || tag.length !== 16 || !ct.length) {
|
|
87
|
+
throw new Error('auth-secure: malformed encrypted payload fields');
|
|
88
|
+
}
|
|
89
|
+
const key = deriveKey(password, salt);
|
|
90
|
+
try {
|
|
91
|
+
const decipher = createDecipheriv(ENC_ALGO, key, iv);
|
|
92
|
+
decipher.setAuthTag(tag);
|
|
93
|
+
const plain = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
94
|
+
return JSON.parse(plain.toString('utf8'), BufferJSON.reviver);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
throw new Error('auth-secure: decryption failed (wrong password or tampered data)', { cause: err });
|
|
97
|
+
} finally {
|
|
98
|
+
key.fill(0);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isEncryptedPayload(value) {
|
|
103
|
+
return typeof value === 'string' && value.startsWith(ENC_MAGIC + '.');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const fileLocks = new Map();
|
|
107
|
+
function lockFor(path) {
|
|
108
|
+
let m = fileLocks.get(path);
|
|
109
|
+
if (!m) {
|
|
110
|
+
m = new Mutex();
|
|
111
|
+
fileLocks.set(path, m);
|
|
112
|
+
}
|
|
113
|
+
return m;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Multi-file auth state with AES-256-GCM encrypted file contents.
|
|
118
|
+
* Reads legacy plaintext files too (transparent migration on next save).
|
|
119
|
+
*
|
|
120
|
+
* @param {string} folder folder holding creds.json + `<type>-<id>.json`
|
|
121
|
+
* @param {{ password: string, logger?: any }} opts password is REQUIRED
|
|
122
|
+
*/
|
|
123
|
+
export async function useEncryptedFileAuthState(folder, opts) {
|
|
124
|
+
const { password, logger } = opts || {};
|
|
125
|
+
if (!password)
|
|
126
|
+
throw new Error('useEncryptedFileAuthState: opts.password is required');
|
|
127
|
+
await fs.mkdir(folder, { recursive: true });
|
|
128
|
+
|
|
129
|
+
const writeData = (data, file) => {
|
|
130
|
+
const filePath = join(folder, fixFileName(file));
|
|
131
|
+
return lockFor(filePath).runExclusive(() => fs.writeFile(filePath, encryptJSON(data, password), { encoding: 'utf-8' }));
|
|
132
|
+
};
|
|
133
|
+
const readData = async (file) => {
|
|
134
|
+
const filePath = join(folder, fixFileName(file));
|
|
135
|
+
return lockFor(filePath).runExclusive(async () => {
|
|
136
|
+
try {
|
|
137
|
+
const raw = await fs.readFile(filePath, { encoding: 'utf-8' });
|
|
138
|
+
const trimmed = raw.trim();
|
|
139
|
+
// transparent migration: legacy plaintext still readable
|
|
140
|
+
if (!isEncryptedPayload(trimmed)) {
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(trimmed, BufferJSON.reviver);
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
return decryptJSON(trimmed, password);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
logger?.warn?.({ file, err: String(err) }, 'auth-secure: failed to decrypt auth file (wrong password?)');
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
const removeData = (file) => {
|
|
159
|
+
const filePath = join(folder, fixFileName(file));
|
|
160
|
+
return lockFor(filePath)
|
|
161
|
+
.runExclusive(() => fs.unlink(filePath))
|
|
162
|
+
.catch(() => {});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const folderInfo = await fs.stat(folder).catch(() => {});
|
|
166
|
+
if (folderInfo) {
|
|
167
|
+
if (!folderInfo.isDirectory()) {
|
|
168
|
+
throw new Error(`found "${folder}" which is not a directory`);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
await fs.mkdir(folder, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const creds = (await readData('creds.json')) || initAuthCreds();
|
|
175
|
+
return {
|
|
176
|
+
state: {
|
|
177
|
+
creds,
|
|
178
|
+
keys: {
|
|
179
|
+
get: async (type, ids) => {
|
|
180
|
+
const data = {};
|
|
181
|
+
await Promise.all(ids.map(async (id) => {
|
|
182
|
+
let value = await readData(`${type}-${id}.json`);
|
|
183
|
+
if (type === 'app-state-sync-key' && value) {
|
|
184
|
+
value = proto.Message.AppStateSyncKeyData.fromObject(value);
|
|
185
|
+
}
|
|
186
|
+
data[id] = value;
|
|
187
|
+
}));
|
|
188
|
+
return data;
|
|
189
|
+
},
|
|
190
|
+
set: async (data) => {
|
|
191
|
+
const tasks = [];
|
|
192
|
+
for (const category of Object.keys(data)) {
|
|
193
|
+
for (const id of Object.keys(data[category])) {
|
|
194
|
+
const value = data[category][id];
|
|
195
|
+
const file = `${category}-${id}.json`;
|
|
196
|
+
tasks.push(value ? writeData(value, file) : removeData(file));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
await Promise.all(tasks);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
saveCreds: () => writeData(creds, 'creds.json'),
|
|
204
|
+
clearState: async () => {
|
|
205
|
+
try {
|
|
206
|
+
const files = await fs.readdir(folder);
|
|
207
|
+
await Promise.all(files
|
|
208
|
+
.filter((f) => f.endsWith('.json'))
|
|
209
|
+
.map((f) => fs.unlink(join(folder, f)).catch(() => {})));
|
|
210
|
+
} catch { /* ignore */ }
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function fixFileName(file) {
|
|
216
|
+
return file?.replace(/\//g, '__')?.replace(/:/g, '-');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Single-file auth state with AES-256-GCM encrypted contents.
|
|
221
|
+
* Reads legacy plaintext files too (transparent migration on next save).
|
|
222
|
+
*/
|
|
223
|
+
export async function useEncryptedSingleFileAuthState(file, opts) {
|
|
224
|
+
const { password, logger } = opts || {};
|
|
225
|
+
if (!password)
|
|
226
|
+
throw new Error('useEncryptedSingleFileAuthState: opts.password is required');
|
|
227
|
+
const keyMap = {};
|
|
228
|
+
const saveState = async (data) => {
|
|
229
|
+
await fs.writeFile(file, encryptJSON(data, password), { encoding: 'utf-8' });
|
|
230
|
+
};
|
|
231
|
+
let creds;
|
|
232
|
+
let saveCreds;
|
|
233
|
+
if (await existsAsync(file)) {
|
|
234
|
+
const raw = (await fs.readFile(file, { encoding: 'utf-8' })).trim();
|
|
235
|
+
const data = isEncryptedPayload(raw)
|
|
236
|
+
? decryptJSON(raw, password)
|
|
237
|
+
: JSON.parse(raw, BufferJSON.reviver);
|
|
238
|
+
creds = data.creds;
|
|
239
|
+
saveCreds = async () => saveState({ creds, keys: keyMap });
|
|
240
|
+
const { keys } = data;
|
|
241
|
+
for (const type of Object.keys(keys || {})) {
|
|
242
|
+
keyMap[type] = {};
|
|
243
|
+
for (const id of Object.keys(keys[type])) {
|
|
244
|
+
let value = keys[type][id];
|
|
245
|
+
if (type === 'app-state-sync-key' && value) {
|
|
246
|
+
value = proto.Message.AppStateSyncKeyData.fromObject(value);
|
|
247
|
+
}
|
|
248
|
+
keyMap[type][id] = value;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
creds = initAuthCreds();
|
|
253
|
+
saveCreds = async () => saveState({ creds, keys: keyMap });
|
|
254
|
+
await saveCreds();
|
|
255
|
+
}
|
|
256
|
+
// minimal debounce (single flush 300ms after last key write)
|
|
257
|
+
let flushTimer = null;
|
|
258
|
+
const scheduleFlush = () => {
|
|
259
|
+
if (flushTimer)
|
|
260
|
+
clearTimeout(flushTimer);
|
|
261
|
+
flushTimer = setTimeout(() => {
|
|
262
|
+
flushTimer = null;
|
|
263
|
+
saveCreds().catch((e) => logger?.warn?.({ err: String(e) }, 'auth-secure: scheduled flush failed'));
|
|
264
|
+
}, 300);
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
state: {
|
|
268
|
+
creds,
|
|
269
|
+
keys: {
|
|
270
|
+
get: async (type, ids) => {
|
|
271
|
+
const data = {};
|
|
272
|
+
for (const id of ids)
|
|
273
|
+
data[id] = keyMap?.[type]?.[id];
|
|
274
|
+
return data;
|
|
275
|
+
},
|
|
276
|
+
set: async (data) => {
|
|
277
|
+
for (const category of Object.keys(data)) {
|
|
278
|
+
keyMap[category] = keyMap[category] || {};
|
|
279
|
+
for (const id of Object.keys(data[category])) {
|
|
280
|
+
const value = data[category][id];
|
|
281
|
+
if (value)
|
|
282
|
+
keyMap[category][id] = value;
|
|
283
|
+
else
|
|
284
|
+
delete keyMap[category][id];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
scheduleFlush();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
saveCreds,
|
|
292
|
+
flush: () => {
|
|
293
|
+
if (flushTimer) {
|
|
294
|
+
clearTimeout(flushTimer);
|
|
295
|
+
flushTimer = null;
|
|
296
|
+
}
|
|
297
|
+
return saveCreds();
|
|
298
|
+
},
|
|
299
|
+
clearState: async () => {
|
|
300
|
+
for (const k of Object.keys(keyMap))
|
|
301
|
+
delete keyMap[k];
|
|
302
|
+
await saveState({ creds: initAuthCreds(), keys: {} });
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function existsAsync(path) {
|
|
308
|
+
try {
|
|
309
|
+
await fs.stat(path);
|
|
310
|
+
return true;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// (6) Encrypted backup / restore
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
export const BACKUP_MAGIC = 'JAPBACKUP1';
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Back up an auth folder (multi-file) or single auth file into one
|
|
323
|
+
* password-encrypted blob file. Password is REQUIRED.
|
|
324
|
+
*/
|
|
325
|
+
export async function backupAuthState(src, outFile, { password } = {}) {
|
|
326
|
+
if (!password)
|
|
327
|
+
throw new Error('backupAuthState: opts.password is required');
|
|
328
|
+
const st = await fs.stat(src);
|
|
329
|
+
const files = {};
|
|
330
|
+
if (st.isDirectory()) {
|
|
331
|
+
const names = (await fs.readdir(src)).filter((n) => n.endsWith('.json'));
|
|
332
|
+
for (const n of names) {
|
|
333
|
+
files[n] = (await fs.readFile(join(src, n), { encoding: 'utf-8' })).toString();
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
files['auth.json'] = (await fs.readFile(src, { encoding: 'utf-8' })).toString();
|
|
337
|
+
}
|
|
338
|
+
const blob = {
|
|
339
|
+
magic: BACKUP_MAGIC,
|
|
340
|
+
createdAt: new Date().toISOString(),
|
|
341
|
+
files: encryptJSON(files, password)
|
|
342
|
+
};
|
|
343
|
+
await fs.writeFile(outFile, JSON.stringify(blob), { encoding: 'utf-8' });
|
|
344
|
+
return { outFile, files: Object.keys(files).length, createdAt: blob.createdAt };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Restore a blob created by {@link backupAuthState} into a folder
|
|
349
|
+
* (multi-file; creates it) or a single file (when `dest` ends in .json and
|
|
350
|
+
* the backup holds exactly one file — or pass `single: true`).
|
|
351
|
+
*/
|
|
352
|
+
export async function restoreAuthState(backupFile, dest, { password, single } = {}) {
|
|
353
|
+
if (!password)
|
|
354
|
+
throw new Error('restoreAuthState: opts.password is required');
|
|
355
|
+
const blob = JSON.parse(await fs.readFile(backupFile, { encoding: 'utf-8' }));
|
|
356
|
+
if (blob?.magic !== BACKUP_MAGIC)
|
|
357
|
+
throw new Error('restoreAuthState: not a JAP auth backup');
|
|
358
|
+
const files = decryptJSON(blob.files, password); // throws on wrong password
|
|
359
|
+
const names = Object.keys(files);
|
|
360
|
+
const toSingle = single || (names.length === 1 && dest.endsWith('.json') && names[0] === 'auth.json');
|
|
361
|
+
if (toSingle) {
|
|
362
|
+
await fs.mkdir(join(dest, '..').replace(/\\/g, '/'), { recursive: true }).catch(() => {});
|
|
363
|
+
await fs.writeFile(dest, files[names[0]], { encoding: 'utf-8' });
|
|
364
|
+
} else {
|
|
365
|
+
await fs.mkdir(dest, { recursive: true });
|
|
366
|
+
await Promise.all(names.map((n) => fs.writeFile(join(dest, n), files[n], { encoding: 'utf-8' })));
|
|
367
|
+
}
|
|
368
|
+
return { dest, files: names.length, createdAt: blob.createdAt };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// (8) Session integrity (checksums)
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
const INTEGRITY_FILE = 'integrity.json';
|
|
375
|
+
|
|
376
|
+
function sha256Hex(buf) {
|
|
377
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Snapshot sha256 (or HMAC-sha256 with `secret`) of every *.json in folder. */
|
|
381
|
+
export async function writeAuthIntegrity(folder, { secret } = {}) {
|
|
382
|
+
const names = (await fs.readdir(folder)).filter((n) => n.endsWith('.json') && n !== INTEGRITY_FILE);
|
|
383
|
+
const sums = {};
|
|
384
|
+
for (const n of names) {
|
|
385
|
+
const raw = await fs.readFile(join(folder, n));
|
|
386
|
+
sums[n] = secret ? createHmac('sha256', secret).update(raw).digest('hex') : sha256Hex(raw);
|
|
387
|
+
}
|
|
388
|
+
const snap = { createdAt: new Date().toISOString(), hmac: Boolean(secret), sums };
|
|
389
|
+
await fs.writeFile(join(folder, INTEGRITY_FILE), JSON.stringify(snap, null, 2), { encoding: 'utf-8' });
|
|
390
|
+
return snap;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Verify folder against its integrity snapshot. */
|
|
394
|
+
export async function verifyAuthIntegrity(folder, { secret } = {}) {
|
|
395
|
+
const snapRaw = await fs.readFile(join(folder, INTEGRITY_FILE), { encoding: 'utf-8' }).catch(() => null);
|
|
396
|
+
if (!snapRaw)
|
|
397
|
+
return { ok: false, error: 'no integrity snapshot', mismatched: [], missing: [], extra: [] };
|
|
398
|
+
let snap;
|
|
399
|
+
try {
|
|
400
|
+
snap = JSON.parse(snapRaw);
|
|
401
|
+
} catch {
|
|
402
|
+
return { ok: false, error: 'integrity snapshot corrupt', mismatched: [], missing: [], extra: [] };
|
|
403
|
+
}
|
|
404
|
+
if (snap.hmac && !secret)
|
|
405
|
+
return { ok: false, error: 'snapshot requires secret', mismatched: [], missing: [], extra: [] };
|
|
406
|
+
if (!snap.hmac && secret)
|
|
407
|
+
return { ok: false, error: 'snapshot has no HMAC but secret given', mismatched: [], missing: [], extra: [] };
|
|
408
|
+
const mismatched = [];
|
|
409
|
+
const missing = [];
|
|
410
|
+
for (const [name, want] of Object.entries(snap.sums || {})) {
|
|
411
|
+
const raw = await fs.readFile(join(folder, name)).catch(() => null);
|
|
412
|
+
if (raw === null) {
|
|
413
|
+
missing.push(name);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const got = snap.hmac ? createHmac('sha256', secret).update(raw).digest('hex') : sha256Hex(raw);
|
|
417
|
+
let equal = false;
|
|
418
|
+
try {
|
|
419
|
+
equal = timingSafeEqual(Buffer.from(got, 'hex'), Buffer.from(want, 'hex'));
|
|
420
|
+
} catch { /* length mismatch → unequal */ }
|
|
421
|
+
if (!equal)
|
|
422
|
+
mismatched.push(name);
|
|
423
|
+
}
|
|
424
|
+
const current = new Set((await fs.readdir(folder).catch(() => [])).filter((n) => n.endsWith('.json') && n !== INTEGRITY_FILE));
|
|
425
|
+
const extra = [...current].filter((n) => !(n in (snap.sums || {})));
|
|
426
|
+
const ok = mismatched.length === 0 && missing.length === 0;
|
|
427
|
+
return { ok, mismatched, missing, extra };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// (9) Corruption recovery
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
/**
|
|
434
|
+
* Scan an auth folder for corrupt files (unparseable JSON / undecryptable),
|
|
435
|
+
* quarantine them to `.corrupt-<ts>-<name>`, and optionally restore creds
|
|
436
|
+
* from an encrypted backup. Never deletes without quarantining.
|
|
437
|
+
*/
|
|
438
|
+
export async function repairAuthState(folder, { password, backupFile, backupPassword, logger } = {}) {
|
|
439
|
+
const report = { ok: true, checked: 0, quarantined: [], credsRestored: false };
|
|
440
|
+
let names;
|
|
441
|
+
try {
|
|
442
|
+
names = (await fs.readdir(folder)).filter((n) => n.endsWith('.json') && n !== INTEGRITY_FILE);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
return { ok: false, checked: 0, quarantined: [], credsRestored: false, error: String(err) };
|
|
445
|
+
}
|
|
446
|
+
const ts = Date.now();
|
|
447
|
+
for (const n of names) {
|
|
448
|
+
report.checked++;
|
|
449
|
+
const p = join(folder, n);
|
|
450
|
+
let valid = false;
|
|
451
|
+
try {
|
|
452
|
+
const raw = (await fs.readFile(p, { encoding: 'utf-8' })).trim();
|
|
453
|
+
if (isEncryptedPayload(raw)) {
|
|
454
|
+
if (password) {
|
|
455
|
+
decryptJSON(raw, password);
|
|
456
|
+
valid = true;
|
|
457
|
+
} else {
|
|
458
|
+
// can't verify without password → treat as OK (not our call)
|
|
459
|
+
valid = true;
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
JSON.parse(raw, BufferJSON.reviver);
|
|
463
|
+
valid = true;
|
|
464
|
+
}
|
|
465
|
+
} catch (err) {
|
|
466
|
+
logger?.warn?.({ file: n, err: String(err) }, 'auth-secure: quarantining corrupt auth file');
|
|
467
|
+
}
|
|
468
|
+
if (!valid) {
|
|
469
|
+
const q = join(folder, `.corrupt-${ts}-${n}`);
|
|
470
|
+
try {
|
|
471
|
+
await fs.rename(p, q);
|
|
472
|
+
report.quarantined.push(n);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
report.ok = false;
|
|
475
|
+
report.error = `failed to quarantine ${n}: ${String(err)}`;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const credsGone = report.quarantined.includes('creds.json') || !(await existsAsync(join(folder, 'creds.json')));
|
|
480
|
+
if (credsGone && backupFile) {
|
|
481
|
+
try {
|
|
482
|
+
const blob = JSON.parse(await fs.readFile(backupFile, { encoding: 'utf-8' }));
|
|
483
|
+
const files = decryptJSON(blob.files, backupPassword || password);
|
|
484
|
+
const credsRaw = files['creds.json'] || files['auth.json'];
|
|
485
|
+
if (!credsRaw)
|
|
486
|
+
throw new Error('backup has no creds');
|
|
487
|
+
await fs.writeFile(join(folder, 'creds.json'), credsRaw, { encoding: 'utf-8' });
|
|
488
|
+
report.credsRestored = true;
|
|
489
|
+
} catch (err) {
|
|
490
|
+
report.ok = false;
|
|
491
|
+
report.error = `creds restore failed: ${String(err)}`;
|
|
492
|
+
}
|
|
493
|
+
} else if (credsGone) {
|
|
494
|
+
report.ok = false;
|
|
495
|
+
report.error = 'creds.json missing/corrupt and no backup provided (re-pair required)';
|
|
496
|
+
}
|
|
497
|
+
return report;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// (2) Pairing-code rate limiting + anti-abuse
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
/**
|
|
504
|
+
* Sliding-window rate limiter. `now` injectable for tests.
|
|
505
|
+
* Returns `{ allowed, retryAfterMs }` — never throws.
|
|
506
|
+
*/
|
|
507
|
+
export function createRateLimiter({ max = 5, windowMs = 60 * 60 * 1000, minIntervalMs = 0, now = () => Date.now() } = {}) {
|
|
508
|
+
const hits = new Map(); // key -> number[] timestamps
|
|
509
|
+
const stats = { allowed: 0, blocked: 0 };
|
|
510
|
+
function prune(arr, t) {
|
|
511
|
+
while (arr.length && t - arr[0] >= windowMs)
|
|
512
|
+
arr.shift();
|
|
513
|
+
return arr;
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
stats,
|
|
517
|
+
check(key = 'global') {
|
|
518
|
+
const t = now();
|
|
519
|
+
const arr = prune(hits.get(key) || [], t);
|
|
520
|
+
if (minIntervalMs && arr.length && t - arr[arr.length - 1] < minIntervalMs) {
|
|
521
|
+
stats.blocked++;
|
|
522
|
+
return { allowed: false, retryAfterMs: minIntervalMs - (t - arr[arr.length - 1]) };
|
|
523
|
+
}
|
|
524
|
+
if (arr.length >= max) {
|
|
525
|
+
stats.blocked++;
|
|
526
|
+
return { allowed: false, retryAfterMs: windowMs - (t - arr[0]) };
|
|
527
|
+
}
|
|
528
|
+
arr.push(t);
|
|
529
|
+
hits.set(key, arr);
|
|
530
|
+
stats.allowed++;
|
|
531
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
532
|
+
},
|
|
533
|
+
reset(key) {
|
|
534
|
+
if (key)
|
|
535
|
+
hits.delete(key);
|
|
536
|
+
else
|
|
537
|
+
hits.clear();
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Wrap `sock.requestPairingCode` with per-phone + global rate limits.
|
|
544
|
+
* @returns {{ restore: () => void, limiter, global }} — call `restore()` to unwrap.
|
|
545
|
+
*/
|
|
546
|
+
export function withPairingGuard(sock, { maxPerHour = 5, globalMaxPerHour = 20, minIntervalMs = 30_000, logger, now } = {}) {
|
|
547
|
+
if (!sock || typeof sock.requestPairingCode !== 'function') {
|
|
548
|
+
throw new Error('withPairingGuard: sock.requestPairingCode not found');
|
|
549
|
+
}
|
|
550
|
+
const limiter = createRateLimiter({ max: maxPerHour, windowMs: 3_600_000, minIntervalMs, now });
|
|
551
|
+
const global = createRateLimiter({ max: globalMaxPerHour, windowMs: 3_600_000, now });
|
|
552
|
+
const original = sock.requestPairingCode.bind(sock);
|
|
553
|
+
sock.requestPairingCode = async (phoneNumber) => {
|
|
554
|
+
const perPhone = limiter.check(String(phoneNumber));
|
|
555
|
+
const g = global.check('global');
|
|
556
|
+
if (!perPhone.allowed || !g.allowed) {
|
|
557
|
+
const retryAfterMs = Math.max(perPhone.retryAfterMs, g.retryAfterMs);
|
|
558
|
+
logger?.warn?.({ phoneNumber: String(phoneNumber).slice(0, 4) + '****', retryAfterMs }, 'auth-secure: pairing-code request rate-limited');
|
|
559
|
+
const err = new Error(`pairing-code rate limited, retry in ${Math.ceil(retryAfterMs / 1000)}s`);
|
|
560
|
+
err.retryAfterMs = retryAfterMs;
|
|
561
|
+
throw err;
|
|
562
|
+
}
|
|
563
|
+
return original(phoneNumber);
|
|
564
|
+
};
|
|
565
|
+
return { restore: () => { sock.requestPairingCode = original; }, limiter, global };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
// (3) QR secret protection
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
/**
|
|
572
|
+
* QR fist-guard: passes the FIRST qr per window, swallows repeats (they are
|
|
573
|
+
* equivalent secrets — re-logging/re-rendering only widens exposure), and
|
|
574
|
+
* expires the window after `ttlMs`. Feed it `ev.qr` values.
|
|
575
|
+
*/
|
|
576
|
+
export function createQRGuard({ ttlMs = 60_000, now = () => Date.now(), onFirst, onBlocked } = {}) {
|
|
577
|
+
let seenAt = 0;
|
|
578
|
+
let current = null;
|
|
579
|
+
const stats = { passed: 0, blocked: 0 };
|
|
580
|
+
return {
|
|
581
|
+
stats,
|
|
582
|
+
/** @returns the qr if it should be handled, `null` if it must be swallowed */
|
|
583
|
+
handle(qr) {
|
|
584
|
+
if (!qr)
|
|
585
|
+
return null;
|
|
586
|
+
const t = now();
|
|
587
|
+
if (current !== null && t - seenAt < ttlMs) {
|
|
588
|
+
stats.blocked++;
|
|
589
|
+
onBlocked?.(stats.blocked);
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
current = qr;
|
|
593
|
+
seenAt = t;
|
|
594
|
+
stats.passed++;
|
|
595
|
+
onFirst?.(qr);
|
|
596
|
+
return qr;
|
|
597
|
+
},
|
|
598
|
+
reset() {
|
|
599
|
+
current = null;
|
|
600
|
+
seenAt = 0;
|
|
601
|
+
},
|
|
602
|
+
get active() {
|
|
603
|
+
return current !== null && now() - seenAt < ttlMs;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
// (7) Secret-redacting logger
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
const SENSITIVE_EXACT = new Set([
|
|
612
|
+
'qr', 'qrcode', 'pairingcode', 'pin', 'otp', 'seed', 'mnemonic', 'passphrase',
|
|
613
|
+
'noisekey', 'privatekey', 'advsecretkey', 'mediakey', 'fileencsha256', 'mediakeytimestamp'
|
|
614
|
+
]);
|
|
615
|
+
const SENSITIVE_SUFFIX = ['key', 'token', 'secret', 'password', 'passwd', 'pwd', 'credential', 'cred', 'auth', 'noise', 'salt', 'iv', 'signature', 'ciphertext'];
|
|
616
|
+
const REDACTED = '[REDACTED]';
|
|
617
|
+
|
|
618
|
+
function isSensitiveKey(key) {
|
|
619
|
+
if (typeof key !== 'string' || !key)
|
|
620
|
+
return false;
|
|
621
|
+
const norm = key.replace(/[^a-zA-Z]/g, '').toLowerCase();
|
|
622
|
+
if (!norm)
|
|
623
|
+
return false;
|
|
624
|
+
if (SENSITIVE_EXACT.has(norm))
|
|
625
|
+
return true;
|
|
626
|
+
return SENSITIVE_SUFFIX.some((s) => norm === s || norm.endsWith(s));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Deep-clone `value`, replacing sensitive fields with `[REDACTED]`.
|
|
631
|
+
* Handles circular refs, Buffers/Uint8Arrays (→ `[BINARY:<n>B]` when under a
|
|
632
|
+
* sensitive key, else kept), Dates, and class instances (kept as-is).
|
|
633
|
+
*/
|
|
634
|
+
export function redactSecrets(value, { extraKeys = [], maxDepth = 12 } = {}) {
|
|
635
|
+
const extra = new Set(extraKeys);
|
|
636
|
+
const seen = new Map();
|
|
637
|
+
const walk = (val, depth) => {
|
|
638
|
+
if (val === null || typeof val !== 'object') {
|
|
639
|
+
// bare strings that ARE encrypted payloads / tokens passed directly
|
|
640
|
+
if (typeof val === 'string' && depth > 0 && (isEncryptedPayload(val) || /^[A-Za-z0-9+/=]{64,}$/.test(val) && val.length > 96)) {
|
|
641
|
+
return REDACTED;
|
|
642
|
+
}
|
|
643
|
+
return val;
|
|
644
|
+
}
|
|
645
|
+
if (seen.has(val))
|
|
646
|
+
return seen.get(val);
|
|
647
|
+
if (val instanceof Date || val instanceof RegExp)
|
|
648
|
+
return val;
|
|
649
|
+
if (Buffer.isBuffer(val) || val instanceof Uint8Array)
|
|
650
|
+
return val; // binary kept unless under sensitive key (handled by parent)
|
|
651
|
+
if (depth >= maxDepth)
|
|
652
|
+
return '[MAXDEPTH]';
|
|
653
|
+
if (Array.isArray(val)) {
|
|
654
|
+
const out = [];
|
|
655
|
+
seen.set(val, out);
|
|
656
|
+
for (const item of val)
|
|
657
|
+
out.push(walk(item, depth + 1));
|
|
658
|
+
return out;
|
|
659
|
+
}
|
|
660
|
+
const protoOf = Object.getPrototypeOf(val);
|
|
661
|
+
if (protoOf !== null && protoOf !== Object.prototype) {
|
|
662
|
+
return val; // class instances (KeyPair etc.) — never serialised anyway
|
|
663
|
+
}
|
|
664
|
+
const out = {};
|
|
665
|
+
seen.set(val, out);
|
|
666
|
+
for (const [k, v] of Object.entries(val)) {
|
|
667
|
+
if (extra.has(k) || isSensitiveKey(k)) {
|
|
668
|
+
out[k] = REDACTED;
|
|
669
|
+
} else if ((Buffer.isBuffer(v) || v instanceof Uint8Array) && v.length > 0 && isSensitiveKey(k)) {
|
|
670
|
+
out[k] = REDACTED;
|
|
671
|
+
} else {
|
|
672
|
+
out[k] = walk(v, depth + 1);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return out;
|
|
676
|
+
};
|
|
677
|
+
return walk(value, 0);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Wrap any pino-compatible logger so every object arg is redacted first.
|
|
682
|
+
* Usage: `const sock = makeWASocket({ logger: secureLogger(pino(...)) })`.
|
|
683
|
+
*/
|
|
684
|
+
export function secureLogger(base, { extraKeys } = {}) {
|
|
685
|
+
if (!base || typeof base !== 'object')
|
|
686
|
+
throw new Error('secureLogger: base logger required');
|
|
687
|
+
const wrap = (fn) => (...args) => fn(...args.map((a) => (a && typeof a === 'object' ? redactSecrets(a, { extraKeys }) : a)));
|
|
688
|
+
const out = Object.create(Object.getPrototypeOf(base) || null);
|
|
689
|
+
for (const k of ['trace', 'debug', 'info', 'warn', 'error', 'fatal']) {
|
|
690
|
+
if (typeof base[k] === 'function')
|
|
691
|
+
out[k] = wrap(base[k].bind(base));
|
|
692
|
+
}
|
|
693
|
+
if (typeof base.child === 'function') {
|
|
694
|
+
out.child = (bindings) => secureLogger(base.child(redactSecrets(bindings || {}, { extraKeys })), { extraKeys });
|
|
695
|
+
}
|
|
696
|
+
// passthrough level + misc props
|
|
697
|
+
for (const k of ['level', 'silent']) {
|
|
698
|
+
if (k in base) {
|
|
699
|
+
try {
|
|
700
|
+
out[k] = base[k];
|
|
701
|
+
} catch { /* readonly — ignore */ }
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ---------------------------------------------------------------------------
|
|
708
|
+
// (4) Secure logout / session revocation
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
const CREDS_SECRET_FIELDS = [
|
|
711
|
+
'noiseKey', 'pairingEphemeralKeyPair', 'signedIdentityKey', 'signedPreKey',
|
|
712
|
+
'advSecretKey', 'pairingCode', 'accountSyncCounter', 'lastPropHash',
|
|
713
|
+
'routingInfo', 'additionalData', 'me', 'account'
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Log out AND wipe local secrets: sends logout, unlinks every *.json in the
|
|
718
|
+
* auth folder (overwritten with random bytes first), scrubs the in-memory
|
|
719
|
+
* creds object. Returns a report. SSD/note: overwrite is best-effort.
|
|
720
|
+
*/
|
|
721
|
+
export async function secureLogout(sock, { authFolder, authFile, passes = 1, logger } = {}) {
|
|
722
|
+
const report = { loggedOut: false, wiped: [], credsScrubbed: false };
|
|
723
|
+
if (sock && typeof sock.logout === 'function') {
|
|
724
|
+
try {
|
|
725
|
+
await sock.logout('secure logout — revoking session');
|
|
726
|
+
report.loggedOut = true;
|
|
727
|
+
} catch (err) {
|
|
728
|
+
logger?.warn?.({ err: String(err) }, 'auth-secure: remote logout failed, continuing with local wipe');
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const targets = [];
|
|
732
|
+
if (authFolder) {
|
|
733
|
+
const names = await fs.readdir(authFolder).catch(() => []);
|
|
734
|
+
for (const n of names.filter((n) => n.endsWith('.json') || n.startsWith('.corrupt-')))
|
|
735
|
+
targets.push(join(authFolder, n));
|
|
736
|
+
}
|
|
737
|
+
if (authFile)
|
|
738
|
+
targets.push(authFile);
|
|
739
|
+
for (const p of targets) {
|
|
740
|
+
try {
|
|
741
|
+
const st = await fs.stat(p);
|
|
742
|
+
for (let i = 0; i < passes; i++) {
|
|
743
|
+
const fh = await fs.open(p, 'r+');
|
|
744
|
+
try {
|
|
745
|
+
await fh.write(randomBytes(Math.min(st.size, 1 << 20)), 0, Math.min(st.size, 1 << 20), 0);
|
|
746
|
+
} finally {
|
|
747
|
+
await fh.close();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
await fs.unlink(p);
|
|
751
|
+
report.wiped.push(p);
|
|
752
|
+
} catch { /* already gone — fine */ }
|
|
753
|
+
}
|
|
754
|
+
const creds = sock?.authState?.creds || sock?.creds;
|
|
755
|
+
if (creds && typeof creds === 'object') {
|
|
756
|
+
for (const f of CREDS_SECRET_FIELDS) {
|
|
757
|
+
try {
|
|
758
|
+
delete creds[f];
|
|
759
|
+
} catch { /* ignore */ }
|
|
760
|
+
}
|
|
761
|
+
report.credsScrubbed = true;
|
|
762
|
+
}
|
|
763
|
+
return report;
|
|
764
|
+
}
|