@open-wa/decrypt 5.0.0-alpha.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/LICENSE.md ADDED
@@ -0,0 +1,67 @@
1
+ ### Copyright (c) 2020 Mohammed Shah
2
+
3
+ # Hippocratic + Do Not Harm (H-DNH) Version 1.1
4
+
5
+ ## Preamble
6
+
7
+ Most software today is developed with little to no thought of how it will be used, or the consequences for our society and planet.
8
+
9
+ As software developers, we engineer the infrastructure of the 21st century. We recognise that our infrastructure has great power to shape the world and the lives of those we share it with, and we choose to consciously take responsibility for the social and environmental impacts of what we build.
10
+
11
+ We envisage a world free from injustice, inequality, and the reckless destruction of lives and our planet. We reject slavery in all its forms, whether by force, indebtedness, or by algorithms that hack human vulnerabilities. We seek a world where humankind is at peace with our neighbours, nature, and ourselves. We want our work to enrich the physical, mental and spiritual wellbeing of all society.
12
+
13
+ We build software to further this vision of a just world, or at the very least, to not put that vision further from reach.
14
+
15
+ ## Terms
16
+
17
+ Licensor hereby grants permission by this license ("License"), free of charge, to any person or entity (the "Licensee") obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
18
+
19
+ 1. All redistribution of source code or binary form, including any modifications must be under these terms. You must inform recipients that the code is governed by these conditions, and how they can obtain a copy of this license. You may not attempt to alter the conditions of who may/may not use this software.
20
+
21
+ 2. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
22
+
23
+ 3. This software must not be used by any organisation, website, product or service that:
24
+
25
+ a) lobbies for, promotes, or derives a majority of income from actions that support or contribute to:
26
+ * sex trafficking
27
+ * human trafficking
28
+ * slavery
29
+ * indentured servitude
30
+ * gambling
31
+ * tobacco
32
+ * adversely addictive behaviours
33
+ * nuclear energy
34
+ * warfare
35
+ * weapons manufacturing
36
+ * war crimes
37
+ * settler colonialism
38
+ * apartheid
39
+ * vaccine misinformation
40
+ * climate misinformation
41
+ * child endangerment
42
+ * violence (except when required to protect public safety)
43
+ * burning of forests
44
+ * deforestation
45
+ * hate speech or discrimination based on age, gender, gender identity, race, sexuality, religion, nationality
46
+
47
+ b) lobbies against, or derives a majority of income from actions that discourage or frustrate:
48
+ * peace
49
+ * access to the rights set out in the Universal Declaration of Human Rights and the Convention on the Rights of the Child
50
+ * peaceful assembly and association (including worker associations)
51
+ * a safe environment or action to curtail the use of fossil fuels or prevent climate change
52
+ * democratic processes
53
+
54
+
55
+ 4. Compliance with Human Rights Laws and Human Rights Principles:
56
+
57
+ a. **Human Rights Laws**. The Software shall not be used by any person or entity for any systems, activities, or other uses that violate any applicable laws, regulations, or rules that protect human, civil, labor, privacy, political, environmental, security, economic, due process, or similar rights (the "Human Rights Laws"). Where the Human Rights Laws of more than one jurisdiction are applicable to the use of the Software, the Human Rights Laws that are most protective of the individuals or groups harmed shall apply.
58
+
59
+ b. **Human Rights Principles**. Licensee is advised to consult the articles of the United Nations Universal Declaration of Human Rights (https://www.un.org/en/universal-declaration-human-rights/) and the United Nations Global Compact (https://www.unglobalcompact.org/ what-is-gc/mission/principles) that define recognized principles of international human rights (the "Human Rights Principles"). It is Licensor's express intent that all use of the Software be consistent with Human Rights Principles. If Licensor receives notification or otherwise learns of an alleged violation of any Human Rights Principles relating to Licensee's use of the Software, Licensor may in its discretion and without obligation (i) (a) notify Licensee of such allegation and (b) allow Licensee 90 days from notification under (i)(a) to investigate and respond to Licensor regarding the allegation and (ii) (a) after the earlier of 90 days from notification under (i)(a), or Licensee's response under (i)(b), notify Licensee of License termination and (b) allow Licensee an additional 90 days from notification under (ii)(a) to cease use of the Software.
60
+
61
+ c. **Indemnity**. Licensee shall hold harmless and indemnify Licensor against all losses, damages, liabilities, deficiencies, claims, actions, judgments, settlements, interest, awards, penalties, fines, costs, or expenses of whatever kind, including Licensor's reasonable attorneys' fees, arising out of or relating to Licensee's non-compliance with this License or use of the Software in violation of Human Rights Laws or Human Rights Principles. 
62
+
63
+ 5. Enforceability: If any portion or provision of this License is determined to be invalid, illegal, or unenforceable by a court of competent jurisdiction, then such invalidity, illegality, or unenforceability shall not affect any other term or provision of this License or invalidate or render unenforceable such term or provision in any other jurisdiction. Upon a determination that any term or provision is invalid, illegal, or unenforceable, to the extent permitted by applicable law, the court may modify this License to affect the original intent of the parties as closely as possible. The section headings are for convenience only and are not intended to affect the construction or interpretation of this License. Any rule of construction to the effect that ambiguities are to be resolved against the drafting party shall not apply in interpreting this License. The language in this License shall be interpreted as to its fair meaning and not strictly for or against any party.
64
+
65
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
66
+
67
+ This license is an extension of the Hippocratic License - an Ethical Source license (https://ethicalsource.dev) and the Do Not Harm License - (https://github.com/raisely/NoHarm)
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @open-wa/decrypt
2
+
3
+ Lightweight WhatsApp media decryption — zero external dependencies
4
+
5
+ Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @open-wa/decrypt
11
+ ```
12
+
13
+ ## Documentation
14
+
15
+ See the [docs site](https://docs.openwa.dev).
16
+
17
+ ## License
18
+
19
+ [H-DNH V1.0](https://github.com/open-wa/wa-automate-nodejs/blob/main/LICENSE.md) — Hippocratic + Do Not Harm
package/dist/index.cjs ADDED
@@ -0,0 +1,163 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_crypto = require("node:crypto");
3
+ //#region src/constants.ts
4
+ /** Mapping from message type to WhatsApp's internal media-type key name. */
5
+ const mediaTypes = {
6
+ IMAGE: "Image",
7
+ VIDEO: "Video",
8
+ AUDIO: "Audio",
9
+ PTT: "Audio",
10
+ DOCUMENT: "Document",
11
+ STICKER: "Image"
12
+ };
13
+ /** Message types where a missing `size` field is tolerable. */
14
+ const NON_SIZE_TYPES = ["sticker"];
15
+ /** Thrown when a message is missing fields required for decryption. */
16
+ var MissingCriticalDataError = class extends Error {
17
+ name = "MissingCriticalDataError";
18
+ constructor(message) {
19
+ super(message);
20
+ }
21
+ };
22
+ //#endregion
23
+ //#region src/crypto.ts
24
+ /**
25
+ * Core crypto routines for WhatsApp media decryption.
26
+ *
27
+ * Uses only `node:crypto` builtins — no external HKDF library needed.
28
+ * `crypto.hkdfSync` landed in Node 15 and is available in Bun.
29
+ */
30
+ /**
31
+ * Derive the AES-256-CBC key and IV from the media key using HKDF-SHA256,
32
+ * then decrypt the ciphertext.
33
+ */
34
+ function decryptBuffer(encryptedData, mediaKeyBase64, mediaType, expectedSize, mimetype) {
35
+ const mediaKeyBytes = Buffer.from(mediaKeyBase64, "base64");
36
+ const info = `WhatsApp ${mediaTypes[mediaType.toUpperCase()] ?? mediaTypes[Object.keys(mediaTypes).find((t) => mimetype?.includes(t.toLowerCase())) ?? ""]} Keys`;
37
+ const expanded = Buffer.from((0, node_crypto.hkdfSync)("sha256", mediaKeyBytes, Buffer.alloc(32), info, 112));
38
+ const iv = expanded.subarray(0, 16);
39
+ const decipher = (0, node_crypto.createDecipheriv)("aes-256-cbc", expanded.subarray(16, 48), iv);
40
+ decipher.setAutoPadding(false);
41
+ const decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
42
+ return expectedSize ? fixPadding(decrypted, expectedSize) : decrypted;
43
+ }
44
+ /**
45
+ * Correct PKCS#7-style padding that WhatsApp applies inconsistently.
46
+ */
47
+ function fixPadding(data, expectedSize) {
48
+ const padding = 16 - expectedSize % 16 & 15;
49
+ if (padding > 0) {
50
+ if (expectedSize + padding === data.length) return data.subarray(0, data.length - padding);
51
+ if (data.length + padding === expectedSize) {
52
+ const pad = Buffer.alloc(padding, padding);
53
+ return Buffer.concat([data, pad]);
54
+ }
55
+ }
56
+ return data;
57
+ }
58
+ //#endregion
59
+ //#region src/download.ts
60
+ /**
61
+ * Download encrypted media from WhatsApp CDN using native fetch.
62
+ *
63
+ * Replaces the legacy axios-based downloader.
64
+ */
65
+ function buildUserAgent(override) {
66
+ let ua = override ?? "WhatsApp/2.16.352 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36";
67
+ if (!ua.includes("WhatsApp")) ua = `WhatsApp/2.16.352 ${ua}`;
68
+ return ua;
69
+ }
70
+ /**
71
+ * Download an encrypted media blob from the given URL.
72
+ * Uses the global `fetch` API (Bun native / Node 18+).
73
+ */
74
+ async function downloadEncryptedMedia(url, options) {
75
+ const { useragent, maxRetries = 3, retryDelay = 2e3 } = options ?? {};
76
+ const headers = {
77
+ "User-Agent": buildUserAgent(useragent),
78
+ DNT: "1",
79
+ "Upgrade-Insecure-Requests": "1",
80
+ Origin: "https://web.whatsapp.com/",
81
+ Referer: "https://web.whatsapp.com/"
82
+ };
83
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
84
+ const res = await fetch(url, { headers });
85
+ if (res.ok) return new Uint8Array(await res.arrayBuffer());
86
+ if (res.status === 404) throw new Error("This media does not exist or is no longer available on the server. See: https://docs.openwa.dev/pages/How%20to/decrypt-media.html#40439d");
87
+ if (attempt < maxRetries - 1) await new Promise((r) => setTimeout(r, retryDelay));
88
+ }
89
+ throw new Error(`Failed to download media after ${maxRetries} attempts`);
90
+ }
91
+ //#endregion
92
+ //#region src/decrypt.ts
93
+ /**
94
+ * Main decryptMedia entry point.
95
+ *
96
+ * Downloads the encrypted blob → decrypts with AES-256-CBC via HKDF-derived keys.
97
+ */
98
+ /**
99
+ * Download and decrypt a WhatsApp media message.
100
+ *
101
+ * @param message — A message object with at least the required decryption fields.
102
+ * @param optionsOrUa — Either a {@link DecryptOptions} object or a legacy user-agent string.
103
+ * @returns The decrypted media as a Buffer.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * import { decryptMedia } from '@open-wa/decrypt';
108
+ *
109
+ * const buffer = await decryptMedia(message);
110
+ * ```
111
+ */
112
+ async function decryptMedia(message, optionsOrUa) {
113
+ const opts = typeof optionsOrUa === "string" ? { useragent: optionsOrUa } : optionsOrUa ?? {};
114
+ validateMessage(message);
115
+ return decryptBuffer(await downloadEncryptedMedia((message.deprecatedMms3Url ?? message.clientUrl).trim(), opts), message.mediaKey, message.type, message.size, message.mimetype);
116
+ }
117
+ function validateMessage(message) {
118
+ const missing = [];
119
+ if (!message.mediaKey) missing.push("mediaKey");
120
+ if (!message.filehash) missing.push("filehash");
121
+ if (!message.mimetype) missing.push("mimetype");
122
+ if (!message.type) missing.push("type");
123
+ if (!message.size) missing.push("size");
124
+ if (missing.length === 0) return;
125
+ if (missing.length === 1 && missing[0] === "size") {
126
+ if (!NON_SIZE_TYPES.includes(message.type)) console.warn("@open-wa/decrypt: size property is missing. File will fail an integrity check.");
127
+ return;
128
+ }
129
+ throw new MissingCriticalDataError(`Message is missing critical data: ${missing.join(", ")}`);
130
+ }
131
+ //#endregion
132
+ //#region src/bleach.ts
133
+ /**
134
+ * Strip a message down to the minimum fields needed for decryption.
135
+ *
136
+ * Useful for minimizing sensitive data in transit — removes sender info,
137
+ * chat context, etc.
138
+ */
139
+ const BLEACH_KEEP = new Set([
140
+ "type",
141
+ "clientUrl",
142
+ "mimetype",
143
+ "mediaKey",
144
+ "size",
145
+ "filehash",
146
+ "uploadhash",
147
+ "deprecatedMms3Url"
148
+ ]);
149
+ /**
150
+ * Return a copy of the message containing only the fields required for decryption.
151
+ */
152
+ function bleachMessage(message) {
153
+ const result = {};
154
+ for (const key of Object.keys(message)) if (BLEACH_KEEP.has(key)) result[key] = message[key];
155
+ return result;
156
+ }
157
+ //#endregion
158
+ exports.MissingCriticalDataError = MissingCriticalDataError;
159
+ exports.bleachMessage = bleachMessage;
160
+ exports.decryptMedia = decryptMedia;
161
+ exports.mediaTypes = mediaTypes;
162
+
163
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/constants.ts","../src/crypto.ts","../src/download.ts","../src/decrypt.ts","../src/bleach.ts"],"sourcesContent":["/** Mapping from message type to WhatsApp's internal media-type key name. */\nexport const mediaTypes: Record<string, string> = {\n IMAGE: 'Image',\n VIDEO: 'Video',\n AUDIO: 'Audio',\n PTT: 'Audio',\n DOCUMENT: 'Document',\n STICKER: 'Image',\n} as const;\n\n/** Message types where a missing `size` field is tolerable. */\nexport const NON_SIZE_TYPES = ['sticker'] as const;\n\n/** The default user-agent used for fetching encrypted media blobs. */\nexport const DEFAULT_USER_AGENT =\n 'WhatsApp/2.16.352 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36';\n\n/** Thrown when a message is missing fields required for decryption. */\nexport class MissingCriticalDataError extends Error {\n override readonly name = 'MissingCriticalDataError';\n\n constructor(message: string) {\n super(message);\n }\n}\n","/**\n * Core crypto routines for WhatsApp media decryption.\n *\n * Uses only `node:crypto` builtins — no external HKDF library needed.\n * `crypto.hkdfSync` landed in Node 15 and is available in Bun.\n */\nimport { createDecipheriv, hkdfSync } from 'node:crypto';\n\nimport { mediaTypes } from './constants.js';\n\n/**\n * Derive the AES-256-CBC key and IV from the media key using HKDF-SHA256,\n * then decrypt the ciphertext.\n */\nexport function decryptBuffer(\n encryptedData: Uint8Array,\n mediaKeyBase64: string,\n mediaType: string,\n expectedSize?: number,\n mimetype?: string,\n): Buffer {\n const mediaKeyBytes = Buffer.from(mediaKeyBase64, 'base64');\n\n // Resolve the info string: \"WhatsApp <Type> Keys\"\n const typeKey =\n mediaTypes[mediaType.toUpperCase()] ??\n mediaTypes[\n Object.keys(mediaTypes).find((t) =>\n mimetype?.includes(t.toLowerCase()),\n ) ?? ''\n ];\n\n const info = `WhatsApp ${typeKey} Keys`;\n\n // HKDF expand to 112 bytes (iv=16 + cipherKey=32 + macKey=32 + refKey=32)\n const expanded = Buffer.from(\n hkdfSync('sha256', mediaKeyBytes, Buffer.alloc(32), info, 112),\n );\n\n const iv = expanded.subarray(0, 16);\n const cipherKey = expanded.subarray(16, 48);\n\n const decipher = createDecipheriv('aes-256-cbc', cipherKey, iv);\n // WhatsApp handles padding manually — disable OpenSSL auto-padding\n decipher.setAutoPadding(false);\n const decrypted = Buffer.concat([\n decipher.update(encryptedData),\n decipher.final(),\n ]);\n\n return expectedSize ? fixPadding(decrypted, expectedSize) : decrypted;\n}\n\n/**\n * Correct PKCS#7-style padding that WhatsApp applies inconsistently.\n */\nfunction fixPadding(data: Buffer, expectedSize: number): Buffer {\n const padding = (16 - (expectedSize % 16)) & 0xf;\n if (padding > 0) {\n if (expectedSize + padding === data.length) {\n // Trim trailing padding bytes\n return data.subarray(0, data.length - padding);\n }\n if (data.length + padding === expectedSize) {\n // Append missing padding\n const pad = Buffer.alloc(padding, padding);\n return Buffer.concat([data, pad]);\n }\n }\n return data;\n}\n","/**\n * Download encrypted media from WhatsApp CDN using native fetch.\n *\n * Replaces the legacy axios-based downloader.\n */\nimport {\n DEFAULT_USER_AGENT,\n} from './constants.js';\n\nfunction buildUserAgent(override?: string): string {\n let ua = override ?? DEFAULT_USER_AGENT;\n if (!ua.includes('WhatsApp')) ua = `WhatsApp/2.16.352 ${ua}`;\n return ua;\n}\n\n/**\n * Download an encrypted media blob from the given URL.\n * Uses the global `fetch` API (Bun native / Node 18+).\n */\nexport async function downloadEncryptedMedia(\n url: string,\n options?: { useragent?: string; maxRetries?: number; retryDelay?: number },\n): Promise<Uint8Array> {\n const { useragent, maxRetries = 3, retryDelay = 2_000 } = options ?? {};\n\n const headers: Record<string, string> = {\n 'User-Agent': buildUserAgent(useragent),\n DNT: '1',\n 'Upgrade-Insecure-Requests': '1',\n Origin: 'https://web.whatsapp.com/',\n Referer: 'https://web.whatsapp.com/',\n };\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const res = await fetch(url, { headers });\n\n if (res.ok) {\n return new Uint8Array(await res.arrayBuffer());\n }\n\n if (res.status === 404) {\n throw new Error(\n 'This media does not exist or is no longer available on the server. ' +\n 'See: https://docs.openwa.dev/pages/How%20to/decrypt-media.html#40439d',\n );\n }\n\n // Retry on transient errors\n if (attempt < maxRetries - 1) {\n await new Promise((r) => setTimeout(r, retryDelay));\n }\n }\n\n throw new Error(`Failed to download media after ${maxRetries} attempts`);\n}\n","/**\n * Main decryptMedia entry point.\n *\n * Downloads the encrypted blob → decrypts with AES-256-CBC via HKDF-derived keys.\n */\nimport { MissingCriticalDataError, NON_SIZE_TYPES } from './constants.js';\nimport { decryptBuffer } from './crypto.js';\nimport { downloadEncryptedMedia } from './download.js';\nimport type { DecryptableMessage, DecryptOptions } from './types.js';\n\n/**\n * Download and decrypt a WhatsApp media message.\n *\n * @param message — A message object with at least the required decryption fields.\n * @param optionsOrUa — Either a {@link DecryptOptions} object or a legacy user-agent string.\n * @returns The decrypted media as a Buffer.\n *\n * @example\n * ```ts\n * import { decryptMedia } from '@open-wa/decrypt';\n *\n * const buffer = await decryptMedia(message);\n * ```\n */\nexport async function decryptMedia(\n message: DecryptableMessage,\n optionsOrUa?: DecryptOptions | string,\n): Promise<Buffer> {\n // Normalise options — accept legacy string signature for backward compat\n const opts: DecryptOptions =\n typeof optionsOrUa === 'string'\n ? { useragent: optionsOrUa }\n : optionsOrUa ?? {};\n\n validateMessage(message);\n\n const url = (message.deprecatedMms3Url ?? message.clientUrl)!.trim();\n const encrypted = await downloadEncryptedMedia(url, opts);\n\n return decryptBuffer(\n encrypted,\n message.mediaKey,\n message.type,\n message.size,\n message.mimetype,\n );\n}\n\nfunction validateMessage(message: DecryptableMessage): void {\n const missing: string[] = [];\n\n if (!message.mediaKey) missing.push('mediaKey');\n if (!message.filehash) missing.push('filehash');\n if (!message.mimetype) missing.push('mimetype');\n if (!message.type) missing.push('type');\n if (!message.size) missing.push('size');\n\n if (missing.length === 0) return;\n\n // Size-only omission is a warning for non-sticker types, not an error\n if (missing.length === 1 && missing[0] === 'size') {\n if (!NON_SIZE_TYPES.includes(message.type as (typeof NON_SIZE_TYPES)[number])) {\n console.warn(\n '@open-wa/decrypt: size property is missing. File will fail an integrity check.',\n );\n }\n return;\n }\n\n throw new MissingCriticalDataError(\n `Message is missing critical data: ${missing.join(', ')}`,\n );\n}\n","/**\n * Strip a message down to the minimum fields needed for decryption.\n *\n * Useful for minimizing sensitive data in transit — removes sender info,\n * chat context, etc.\n */\n\nconst BLEACH_KEEP = new Set([\n 'type',\n 'clientUrl',\n 'mimetype',\n 'mediaKey',\n 'size',\n 'filehash',\n 'uploadhash',\n 'deprecatedMms3Url',\n]);\n\n/**\n * Return a copy of the message containing only the fields required for decryption.\n */\nexport function bleachMessage<T extends Record<string, unknown>>(\n message: T,\n): Partial<T> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(message)) {\n if (BLEACH_KEEP.has(key)) {\n result[key] = message[key];\n }\n }\n return result as Partial<T>;\n}\n"],"mappings":";;;;AACA,MAAa,aAAqC;CAChD,OAAO;CACP,OAAO;CACP,OAAO;CACP,KAAK;CACL,UAAU;CACV,SAAS;CACV;;AAGD,MAAa,iBAAiB,CAAC,UAAU;;AAOzC,IAAa,2BAAb,cAA8C,MAAM;CAClD,OAAyB;CAEzB,YAAY,SAAiB;AAC3B,QAAM,QAAQ;;;;;;;;;;;;;;;ACRlB,SAAgB,cACd,eACA,gBACA,WACA,cACA,UACQ;CACR,MAAM,gBAAgB,OAAO,KAAK,gBAAgB,SAAS;CAW3D,MAAM,OAAO,YAPX,WAAW,UAAU,aAAa,KAClC,WACE,OAAO,KAAK,WAAW,CAAC,MAAM,MAC5B,UAAU,SAAS,EAAE,aAAa,CAAC,CACpC,IAAI,IAGwB;CAGjC,MAAM,WAAW,OAAO,MAAA,GAAA,YAAA,UACb,UAAU,eAAe,OAAO,MAAM,GAAG,EAAE,MAAM,IAAI,CAC/D;CAED,MAAM,KAAK,SAAS,SAAS,GAAG,GAAG;CAGnC,MAAM,YAAA,GAAA,YAAA,kBAA4B,eAFhB,SAAS,SAAS,IAAI,GAAG,EAEiB,GAAG;AAE/D,UAAS,eAAe,MAAM;CAC9B,MAAM,YAAY,OAAO,OAAO,CAC9B,SAAS,OAAO,cAAc,EAC9B,SAAS,OAAO,CACjB,CAAC;AAEF,QAAO,eAAe,WAAW,WAAW,aAAa,GAAG;;;;;AAM9D,SAAS,WAAW,MAAc,cAA8B;CAC9D,MAAM,UAAW,KAAM,eAAe,KAAO;AAC7C,KAAI,UAAU,GAAG;AACf,MAAI,eAAe,YAAY,KAAK,OAElC,QAAO,KAAK,SAAS,GAAG,KAAK,SAAS,QAAQ;AAEhD,MAAI,KAAK,SAAS,YAAY,cAAc;GAE1C,MAAM,MAAM,OAAO,MAAM,SAAS,QAAQ;AAC1C,UAAO,OAAO,OAAO,CAAC,MAAM,IAAI,CAAC;;;AAGrC,QAAO;;;;;;;;;AC5DT,SAAS,eAAe,UAA2B;CACjD,IAAI,KAAK,YAAA;AACT,KAAI,CAAC,GAAG,SAAS,WAAW,CAAE,MAAK,qBAAqB;AACxD,QAAO;;;;;;AAOT,eAAsB,uBACpB,KACA,SACqB;CACrB,MAAM,EAAE,WAAW,aAAa,GAAG,aAAa,QAAU,WAAW,EAAE;CAEvE,MAAM,UAAkC;EACtC,cAAc,eAAe,UAAU;EACvC,KAAK;EACL,6BAA6B;EAC7B,QAAQ;EACR,SAAS;EACV;AAED,MAAK,IAAI,UAAU,GAAG,UAAU,YAAY,WAAW;EACrD,MAAM,MAAM,MAAM,MAAM,KAAK,EAAE,SAAS,CAAC;AAEzC,MAAI,IAAI,GACN,QAAO,IAAI,WAAW,MAAM,IAAI,aAAa,CAAC;AAGhD,MAAI,IAAI,WAAW,IACjB,OAAM,IAAI,MACR,2IAED;AAIH,MAAI,UAAU,aAAa,EACzB,OAAM,IAAI,SAAS,MAAM,WAAW,GAAG,WAAW,CAAC;;AAIvD,OAAM,IAAI,MAAM,kCAAkC,WAAW,WAAW;;;;;;;;;;;;;;;;;;;;;;;AC7B1E,eAAsB,aACpB,SACA,aACiB;CAEjB,MAAM,OACJ,OAAO,gBAAgB,WACnB,EAAE,WAAW,aAAa,GAC1B,eAAe,EAAE;AAEvB,iBAAgB,QAAQ;AAKxB,QAAO,cAFW,MAAM,wBADX,QAAQ,qBAAqB,QAAQ,WAAY,MAAM,EAChB,KAAK,EAIvD,QAAQ,UACR,QAAQ,MACR,QAAQ,MACR,QAAQ,SACT;;AAGH,SAAS,gBAAgB,SAAmC;CAC1D,MAAM,UAAoB,EAAE;AAE5B,KAAI,CAAC,QAAQ,SAAU,SAAQ,KAAK,WAAW;AAC/C,KAAI,CAAC,QAAQ,SAAU,SAAQ,KAAK,WAAW;AAC/C,KAAI,CAAC,QAAQ,SAAU,SAAQ,KAAK,WAAW;AAC/C,KAAI,CAAC,QAAQ,KAAM,SAAQ,KAAK,OAAO;AACvC,KAAI,CAAC,QAAQ,KAAM,SAAQ,KAAK,OAAO;AAEvC,KAAI,QAAQ,WAAW,EAAG;AAG1B,KAAI,QAAQ,WAAW,KAAK,QAAQ,OAAO,QAAQ;AACjD,MAAI,CAAC,eAAe,SAAS,QAAQ,KAAwC,CAC3E,SAAQ,KACN,iFACD;AAEH;;AAGF,OAAM,IAAI,yBACR,qCAAqC,QAAQ,KAAK,KAAK,GACxD;;;;;;;;;;AChEH,MAAM,cAAc,IAAI,IAAI;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;AAKF,SAAgB,cACd,SACY;CACZ,MAAM,SAAkC,EAAE;AAC1C,MAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,CACpC,KAAI,YAAY,IAAI,IAAI,CACtB,QAAO,OAAO,QAAQ;AAG1B,QAAO"}
@@ -0,0 +1,77 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * Type definitions for decryptable WhatsApp media messages.
4
+ *
5
+ * Self-contained — no external type deps like type-fest or wa-automate-types-only.
6
+ */
7
+ /**
8
+ * Utility type: at least one of the specified keys must be present.
9
+ * Replaces the `type-fest` RequireAtLeastOne import.
10
+ */
11
+ type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> & { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys];
12
+ /** The absolute minimum fields needed for decryption. */
13
+ interface RequiredDecryptionMessage {
14
+ mediaKey: string;
15
+ filehash: string;
16
+ mimetype: string;
17
+ type: string;
18
+ size: number;
19
+ }
20
+ /**
21
+ * A message with enough data to download + decrypt the media attachment.
22
+ * At least one of `clientUrl` or `deprecatedMms3Url` must be present.
23
+ */
24
+ type DecryptableMessage = RequireAtLeastOne<{
25
+ clientUrl?: string;
26
+ deprecatedMms3Url?: string;
27
+ }, 'clientUrl' | 'deprecatedMms3Url'> & RequiredDecryptionMessage;
28
+ /** Options for the decryptMedia function. */
29
+ interface DecryptOptions {
30
+ /** Custom User-Agent header for the download request. */
31
+ useragent?: string;
32
+ /** Maximum number of retry attempts for the download (default: 3). */
33
+ maxRetries?: number;
34
+ /** Delay in ms between retries (default: 2000). */
35
+ retryDelay?: number;
36
+ }
37
+ //#endregion
38
+ //#region src/decrypt.d.ts
39
+ /**
40
+ * Download and decrypt a WhatsApp media message.
41
+ *
42
+ * @param message — A message object with at least the required decryption fields.
43
+ * @param optionsOrUa — Either a {@link DecryptOptions} object or a legacy user-agent string.
44
+ * @returns The decrypted media as a Buffer.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { decryptMedia } from '@open-wa/decrypt';
49
+ *
50
+ * const buffer = await decryptMedia(message);
51
+ * ```
52
+ */
53
+ declare function decryptMedia(message: DecryptableMessage, optionsOrUa?: DecryptOptions | string): Promise<Buffer>;
54
+ //#endregion
55
+ //#region src/bleach.d.ts
56
+ /**
57
+ * Strip a message down to the minimum fields needed for decryption.
58
+ *
59
+ * Useful for minimizing sensitive data in transit — removes sender info,
60
+ * chat context, etc.
61
+ */
62
+ /**
63
+ * Return a copy of the message containing only the fields required for decryption.
64
+ */
65
+ declare function bleachMessage<T extends Record<string, unknown>>(message: T): Partial<T>;
66
+ //#endregion
67
+ //#region src/constants.d.ts
68
+ /** Mapping from message type to WhatsApp's internal media-type key name. */
69
+ declare const mediaTypes: Record<string, string>;
70
+ /** Thrown when a message is missing fields required for decryption. */
71
+ declare class MissingCriticalDataError extends Error {
72
+ readonly name = "MissingCriticalDataError";
73
+ constructor(message: string);
74
+ }
75
+ //#endregion
76
+ export { type DecryptOptions, type DecryptableMessage, MissingCriticalDataError, type RequiredDecryptionMessage, bleachMessage, decryptMedia, mediaTypes };
77
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/decrypt.ts","../src/bleach.ts","../src/constants.ts"],"mappings":";;;;;;;;;;KAUK,iBAAA,uBAAwC,CAAA,SAAU,CAAA,IAAK,IAAA,CAC1D,CAAA,EACA,OAAA,OAAc,CAAA,EAAG,IAAA,aAGT,IAAA,KAAS,QAAA,CAAS,IAAA,CAAK,CAAA,EAAG,CAAA,KAAM,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,OAAA,CAAQ,IAAA,EAAM,CAAA,MACpE,IAAA;;UAGa,yBAAA;EACf,QAAA;EACA,QAAA;EACA,QAAA;EACA,IAAA;EACA,IAAA;AAAA;;;;;KAOU,kBAAA,GAAqB,iBAAA;EAE7B,SAAA;EACA,iBAAA;AAAA,wCAIF,yBAAA;;UAGe,cAAA;EA9Bf;EAgCA,SAAA;EA/Bc;EAiCd,UAAA;EA9BG;EAgCH,UAAA;AAAA;;;;;;;;;;;;;;;;;iBCvBoB,YAAA,CACpB,OAAA,EAAS,kBAAA,EACT,WAAA,GAAc,cAAA,YACb,OAAA,CAAQ,MAAA;;;;;;;;;;;;iBCNK,aAAA,WAAwB,MAAA,kBAAA,CACtC,OAAA,EAAS,CAAA,GACR,OAAA,CAAQ,CAAA;;;;cCtBE,UAAA,EAAY,MAAA;;cAiBZ,wBAAA,SAAiC,KAAA;EAAA,SAC1B,IAAA;cAEN,OAAA;AAAA"}
@@ -0,0 +1,77 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * Type definitions for decryptable WhatsApp media messages.
4
+ *
5
+ * Self-contained — no external type deps like type-fest or wa-automate-types-only.
6
+ */
7
+ /**
8
+ * Utility type: at least one of the specified keys must be present.
9
+ * Replaces the `type-fest` RequireAtLeastOne import.
10
+ */
11
+ type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> & { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys];
12
+ /** The absolute minimum fields needed for decryption. */
13
+ interface RequiredDecryptionMessage {
14
+ mediaKey: string;
15
+ filehash: string;
16
+ mimetype: string;
17
+ type: string;
18
+ size: number;
19
+ }
20
+ /**
21
+ * A message with enough data to download + decrypt the media attachment.
22
+ * At least one of `clientUrl` or `deprecatedMms3Url` must be present.
23
+ */
24
+ type DecryptableMessage = RequireAtLeastOne<{
25
+ clientUrl?: string;
26
+ deprecatedMms3Url?: string;
27
+ }, 'clientUrl' | 'deprecatedMms3Url'> & RequiredDecryptionMessage;
28
+ /** Options for the decryptMedia function. */
29
+ interface DecryptOptions {
30
+ /** Custom User-Agent header for the download request. */
31
+ useragent?: string;
32
+ /** Maximum number of retry attempts for the download (default: 3). */
33
+ maxRetries?: number;
34
+ /** Delay in ms between retries (default: 2000). */
35
+ retryDelay?: number;
36
+ }
37
+ //#endregion
38
+ //#region src/decrypt.d.ts
39
+ /**
40
+ * Download and decrypt a WhatsApp media message.
41
+ *
42
+ * @param message — A message object with at least the required decryption fields.
43
+ * @param optionsOrUa — Either a {@link DecryptOptions} object or a legacy user-agent string.
44
+ * @returns The decrypted media as a Buffer.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { decryptMedia } from '@open-wa/decrypt';
49
+ *
50
+ * const buffer = await decryptMedia(message);
51
+ * ```
52
+ */
53
+ declare function decryptMedia(message: DecryptableMessage, optionsOrUa?: DecryptOptions | string): Promise<Buffer>;
54
+ //#endregion
55
+ //#region src/bleach.d.ts
56
+ /**
57
+ * Strip a message down to the minimum fields needed for decryption.
58
+ *
59
+ * Useful for minimizing sensitive data in transit — removes sender info,
60
+ * chat context, etc.
61
+ */
62
+ /**
63
+ * Return a copy of the message containing only the fields required for decryption.
64
+ */
65
+ declare function bleachMessage<T extends Record<string, unknown>>(message: T): Partial<T>;
66
+ //#endregion
67
+ //#region src/constants.d.ts
68
+ /** Mapping from message type to WhatsApp's internal media-type key name. */
69
+ declare const mediaTypes: Record<string, string>;
70
+ /** Thrown when a message is missing fields required for decryption. */
71
+ declare class MissingCriticalDataError extends Error {
72
+ readonly name = "MissingCriticalDataError";
73
+ constructor(message: string);
74
+ }
75
+ //#endregion
76
+ export { type DecryptOptions, type DecryptableMessage, MissingCriticalDataError, type RequiredDecryptionMessage, bleachMessage, decryptMedia, mediaTypes };
77
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/decrypt.ts","../src/bleach.ts","../src/constants.ts"],"mappings":";;;;;;;;;;KAUK,iBAAA,uBAAwC,CAAA,SAAU,CAAA,IAAK,IAAA,CAC1D,CAAA,EACA,OAAA,OAAc,CAAA,EAAG,IAAA,aAGT,IAAA,KAAS,QAAA,CAAS,IAAA,CAAK,CAAA,EAAG,CAAA,KAAM,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,OAAA,CAAQ,IAAA,EAAM,CAAA,MACpE,IAAA;;UAGa,yBAAA;EACf,QAAA;EACA,QAAA;EACA,QAAA;EACA,IAAA;EACA,IAAA;AAAA;;;;;KAOU,kBAAA,GAAqB,iBAAA;EAE7B,SAAA;EACA,iBAAA;AAAA,wCAIF,yBAAA;;UAGe,cAAA;EA9Bf;EAgCA,SAAA;EA/Bc;EAiCd,UAAA;EA9BG;EAgCH,UAAA;AAAA;;;;;;;;;;;;;;;;;iBCvBoB,YAAA,CACpB,OAAA,EAAS,kBAAA,EACT,WAAA,GAAc,cAAA,YACb,OAAA,CAAQ,MAAA;;;;;;;;;;;;iBCNK,aAAA,WAAwB,MAAA,kBAAA,CACtC,OAAA,EAAS,CAAA,GACR,OAAA,CAAQ,CAAA;;;;cCtBE,UAAA,EAAY,MAAA;;cAiBZ,wBAAA,SAAiC,KAAA;EAAA,SAC1B,IAAA;cAEN,OAAA;AAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,159 @@
1
+ import { createDecipheriv, hkdfSync } from "node:crypto";
2
+ //#region src/constants.ts
3
+ /** Mapping from message type to WhatsApp's internal media-type key name. */
4
+ const mediaTypes = {
5
+ IMAGE: "Image",
6
+ VIDEO: "Video",
7
+ AUDIO: "Audio",
8
+ PTT: "Audio",
9
+ DOCUMENT: "Document",
10
+ STICKER: "Image"
11
+ };
12
+ /** Message types where a missing `size` field is tolerable. */
13
+ const NON_SIZE_TYPES = ["sticker"];
14
+ /** Thrown when a message is missing fields required for decryption. */
15
+ var MissingCriticalDataError = class extends Error {
16
+ name = "MissingCriticalDataError";
17
+ constructor(message) {
18
+ super(message);
19
+ }
20
+ };
21
+ //#endregion
22
+ //#region src/crypto.ts
23
+ /**
24
+ * Core crypto routines for WhatsApp media decryption.
25
+ *
26
+ * Uses only `node:crypto` builtins — no external HKDF library needed.
27
+ * `crypto.hkdfSync` landed in Node 15 and is available in Bun.
28
+ */
29
+ /**
30
+ * Derive the AES-256-CBC key and IV from the media key using HKDF-SHA256,
31
+ * then decrypt the ciphertext.
32
+ */
33
+ function decryptBuffer(encryptedData, mediaKeyBase64, mediaType, expectedSize, mimetype) {
34
+ const mediaKeyBytes = Buffer.from(mediaKeyBase64, "base64");
35
+ const info = `WhatsApp ${mediaTypes[mediaType.toUpperCase()] ?? mediaTypes[Object.keys(mediaTypes).find((t) => mimetype?.includes(t.toLowerCase())) ?? ""]} Keys`;
36
+ const expanded = Buffer.from(hkdfSync("sha256", mediaKeyBytes, Buffer.alloc(32), info, 112));
37
+ const iv = expanded.subarray(0, 16);
38
+ const decipher = createDecipheriv("aes-256-cbc", expanded.subarray(16, 48), iv);
39
+ decipher.setAutoPadding(false);
40
+ const decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
41
+ return expectedSize ? fixPadding(decrypted, expectedSize) : decrypted;
42
+ }
43
+ /**
44
+ * Correct PKCS#7-style padding that WhatsApp applies inconsistently.
45
+ */
46
+ function fixPadding(data, expectedSize) {
47
+ const padding = 16 - expectedSize % 16 & 15;
48
+ if (padding > 0) {
49
+ if (expectedSize + padding === data.length) return data.subarray(0, data.length - padding);
50
+ if (data.length + padding === expectedSize) {
51
+ const pad = Buffer.alloc(padding, padding);
52
+ return Buffer.concat([data, pad]);
53
+ }
54
+ }
55
+ return data;
56
+ }
57
+ //#endregion
58
+ //#region src/download.ts
59
+ /**
60
+ * Download encrypted media from WhatsApp CDN using native fetch.
61
+ *
62
+ * Replaces the legacy axios-based downloader.
63
+ */
64
+ function buildUserAgent(override) {
65
+ let ua = override ?? "WhatsApp/2.16.352 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36";
66
+ if (!ua.includes("WhatsApp")) ua = `WhatsApp/2.16.352 ${ua}`;
67
+ return ua;
68
+ }
69
+ /**
70
+ * Download an encrypted media blob from the given URL.
71
+ * Uses the global `fetch` API (Bun native / Node 18+).
72
+ */
73
+ async function downloadEncryptedMedia(url, options) {
74
+ const { useragent, maxRetries = 3, retryDelay = 2e3 } = options ?? {};
75
+ const headers = {
76
+ "User-Agent": buildUserAgent(useragent),
77
+ DNT: "1",
78
+ "Upgrade-Insecure-Requests": "1",
79
+ Origin: "https://web.whatsapp.com/",
80
+ Referer: "https://web.whatsapp.com/"
81
+ };
82
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
83
+ const res = await fetch(url, { headers });
84
+ if (res.ok) return new Uint8Array(await res.arrayBuffer());
85
+ if (res.status === 404) throw new Error("This media does not exist or is no longer available on the server. See: https://docs.openwa.dev/pages/How%20to/decrypt-media.html#40439d");
86
+ if (attempt < maxRetries - 1) await new Promise((r) => setTimeout(r, retryDelay));
87
+ }
88
+ throw new Error(`Failed to download media after ${maxRetries} attempts`);
89
+ }
90
+ //#endregion
91
+ //#region src/decrypt.ts
92
+ /**
93
+ * Main decryptMedia entry point.
94
+ *
95
+ * Downloads the encrypted blob → decrypts with AES-256-CBC via HKDF-derived keys.
96
+ */
97
+ /**
98
+ * Download and decrypt a WhatsApp media message.
99
+ *
100
+ * @param message — A message object with at least the required decryption fields.
101
+ * @param optionsOrUa — Either a {@link DecryptOptions} object or a legacy user-agent string.
102
+ * @returns The decrypted media as a Buffer.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * import { decryptMedia } from '@open-wa/decrypt';
107
+ *
108
+ * const buffer = await decryptMedia(message);
109
+ * ```
110
+ */
111
+ async function decryptMedia(message, optionsOrUa) {
112
+ const opts = typeof optionsOrUa === "string" ? { useragent: optionsOrUa } : optionsOrUa ?? {};
113
+ validateMessage(message);
114
+ return decryptBuffer(await downloadEncryptedMedia((message.deprecatedMms3Url ?? message.clientUrl).trim(), opts), message.mediaKey, message.type, message.size, message.mimetype);
115
+ }
116
+ function validateMessage(message) {
117
+ const missing = [];
118
+ if (!message.mediaKey) missing.push("mediaKey");
119
+ if (!message.filehash) missing.push("filehash");
120
+ if (!message.mimetype) missing.push("mimetype");
121
+ if (!message.type) missing.push("type");
122
+ if (!message.size) missing.push("size");
123
+ if (missing.length === 0) return;
124
+ if (missing.length === 1 && missing[0] === "size") {
125
+ if (!NON_SIZE_TYPES.includes(message.type)) console.warn("@open-wa/decrypt: size property is missing. File will fail an integrity check.");
126
+ return;
127
+ }
128
+ throw new MissingCriticalDataError(`Message is missing critical data: ${missing.join(", ")}`);
129
+ }
130
+ //#endregion
131
+ //#region src/bleach.ts
132
+ /**
133
+ * Strip a message down to the minimum fields needed for decryption.
134
+ *
135
+ * Useful for minimizing sensitive data in transit — removes sender info,
136
+ * chat context, etc.
137
+ */
138
+ const BLEACH_KEEP = new Set([
139
+ "type",
140
+ "clientUrl",
141
+ "mimetype",
142
+ "mediaKey",
143
+ "size",
144
+ "filehash",
145
+ "uploadhash",
146
+ "deprecatedMms3Url"
147
+ ]);
148
+ /**
149
+ * Return a copy of the message containing only the fields required for decryption.
150
+ */
151
+ function bleachMessage(message) {
152
+ const result = {};
153
+ for (const key of Object.keys(message)) if (BLEACH_KEEP.has(key)) result[key] = message[key];
154
+ return result;
155
+ }
156
+ //#endregion
157
+ export { MissingCriticalDataError, bleachMessage, decryptMedia, mediaTypes };
158
+
159
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/crypto.ts","../src/download.ts","../src/decrypt.ts","../src/bleach.ts"],"sourcesContent":["/** Mapping from message type to WhatsApp's internal media-type key name. */\nexport const mediaTypes: Record<string, string> = {\n IMAGE: 'Image',\n VIDEO: 'Video',\n AUDIO: 'Audio',\n PTT: 'Audio',\n DOCUMENT: 'Document',\n STICKER: 'Image',\n} as const;\n\n/** Message types where a missing `size` field is tolerable. */\nexport const NON_SIZE_TYPES = ['sticker'] as const;\n\n/** The default user-agent used for fetching encrypted media blobs. */\nexport const DEFAULT_USER_AGENT =\n 'WhatsApp/2.16.352 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36';\n\n/** Thrown when a message is missing fields required for decryption. */\nexport class MissingCriticalDataError extends Error {\n override readonly name = 'MissingCriticalDataError';\n\n constructor(message: string) {\n super(message);\n }\n}\n","/**\n * Core crypto routines for WhatsApp media decryption.\n *\n * Uses only `node:crypto` builtins — no external HKDF library needed.\n * `crypto.hkdfSync` landed in Node 15 and is available in Bun.\n */\nimport { createDecipheriv, hkdfSync } from 'node:crypto';\n\nimport { mediaTypes } from './constants.js';\n\n/**\n * Derive the AES-256-CBC key and IV from the media key using HKDF-SHA256,\n * then decrypt the ciphertext.\n */\nexport function decryptBuffer(\n encryptedData: Uint8Array,\n mediaKeyBase64: string,\n mediaType: string,\n expectedSize?: number,\n mimetype?: string,\n): Buffer {\n const mediaKeyBytes = Buffer.from(mediaKeyBase64, 'base64');\n\n // Resolve the info string: \"WhatsApp <Type> Keys\"\n const typeKey =\n mediaTypes[mediaType.toUpperCase()] ??\n mediaTypes[\n Object.keys(mediaTypes).find((t) =>\n mimetype?.includes(t.toLowerCase()),\n ) ?? ''\n ];\n\n const info = `WhatsApp ${typeKey} Keys`;\n\n // HKDF expand to 112 bytes (iv=16 + cipherKey=32 + macKey=32 + refKey=32)\n const expanded = Buffer.from(\n hkdfSync('sha256', mediaKeyBytes, Buffer.alloc(32), info, 112),\n );\n\n const iv = expanded.subarray(0, 16);\n const cipherKey = expanded.subarray(16, 48);\n\n const decipher = createDecipheriv('aes-256-cbc', cipherKey, iv);\n // WhatsApp handles padding manually — disable OpenSSL auto-padding\n decipher.setAutoPadding(false);\n const decrypted = Buffer.concat([\n decipher.update(encryptedData),\n decipher.final(),\n ]);\n\n return expectedSize ? fixPadding(decrypted, expectedSize) : decrypted;\n}\n\n/**\n * Correct PKCS#7-style padding that WhatsApp applies inconsistently.\n */\nfunction fixPadding(data: Buffer, expectedSize: number): Buffer {\n const padding = (16 - (expectedSize % 16)) & 0xf;\n if (padding > 0) {\n if (expectedSize + padding === data.length) {\n // Trim trailing padding bytes\n return data.subarray(0, data.length - padding);\n }\n if (data.length + padding === expectedSize) {\n // Append missing padding\n const pad = Buffer.alloc(padding, padding);\n return Buffer.concat([data, pad]);\n }\n }\n return data;\n}\n","/**\n * Download encrypted media from WhatsApp CDN using native fetch.\n *\n * Replaces the legacy axios-based downloader.\n */\nimport {\n DEFAULT_USER_AGENT,\n} from './constants.js';\n\nfunction buildUserAgent(override?: string): string {\n let ua = override ?? DEFAULT_USER_AGENT;\n if (!ua.includes('WhatsApp')) ua = `WhatsApp/2.16.352 ${ua}`;\n return ua;\n}\n\n/**\n * Download an encrypted media blob from the given URL.\n * Uses the global `fetch` API (Bun native / Node 18+).\n */\nexport async function downloadEncryptedMedia(\n url: string,\n options?: { useragent?: string; maxRetries?: number; retryDelay?: number },\n): Promise<Uint8Array> {\n const { useragent, maxRetries = 3, retryDelay = 2_000 } = options ?? {};\n\n const headers: Record<string, string> = {\n 'User-Agent': buildUserAgent(useragent),\n DNT: '1',\n 'Upgrade-Insecure-Requests': '1',\n Origin: 'https://web.whatsapp.com/',\n Referer: 'https://web.whatsapp.com/',\n };\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const res = await fetch(url, { headers });\n\n if (res.ok) {\n return new Uint8Array(await res.arrayBuffer());\n }\n\n if (res.status === 404) {\n throw new Error(\n 'This media does not exist or is no longer available on the server. ' +\n 'See: https://docs.openwa.dev/pages/How%20to/decrypt-media.html#40439d',\n );\n }\n\n // Retry on transient errors\n if (attempt < maxRetries - 1) {\n await new Promise((r) => setTimeout(r, retryDelay));\n }\n }\n\n throw new Error(`Failed to download media after ${maxRetries} attempts`);\n}\n","/**\n * Main decryptMedia entry point.\n *\n * Downloads the encrypted blob → decrypts with AES-256-CBC via HKDF-derived keys.\n */\nimport { MissingCriticalDataError, NON_SIZE_TYPES } from './constants.js';\nimport { decryptBuffer } from './crypto.js';\nimport { downloadEncryptedMedia } from './download.js';\nimport type { DecryptableMessage, DecryptOptions } from './types.js';\n\n/**\n * Download and decrypt a WhatsApp media message.\n *\n * @param message — A message object with at least the required decryption fields.\n * @param optionsOrUa — Either a {@link DecryptOptions} object or a legacy user-agent string.\n * @returns The decrypted media as a Buffer.\n *\n * @example\n * ```ts\n * import { decryptMedia } from '@open-wa/decrypt';\n *\n * const buffer = await decryptMedia(message);\n * ```\n */\nexport async function decryptMedia(\n message: DecryptableMessage,\n optionsOrUa?: DecryptOptions | string,\n): Promise<Buffer> {\n // Normalise options — accept legacy string signature for backward compat\n const opts: DecryptOptions =\n typeof optionsOrUa === 'string'\n ? { useragent: optionsOrUa }\n : optionsOrUa ?? {};\n\n validateMessage(message);\n\n const url = (message.deprecatedMms3Url ?? message.clientUrl)!.trim();\n const encrypted = await downloadEncryptedMedia(url, opts);\n\n return decryptBuffer(\n encrypted,\n message.mediaKey,\n message.type,\n message.size,\n message.mimetype,\n );\n}\n\nfunction validateMessage(message: DecryptableMessage): void {\n const missing: string[] = [];\n\n if (!message.mediaKey) missing.push('mediaKey');\n if (!message.filehash) missing.push('filehash');\n if (!message.mimetype) missing.push('mimetype');\n if (!message.type) missing.push('type');\n if (!message.size) missing.push('size');\n\n if (missing.length === 0) return;\n\n // Size-only omission is a warning for non-sticker types, not an error\n if (missing.length === 1 && missing[0] === 'size') {\n if (!NON_SIZE_TYPES.includes(message.type as (typeof NON_SIZE_TYPES)[number])) {\n console.warn(\n '@open-wa/decrypt: size property is missing. File will fail an integrity check.',\n );\n }\n return;\n }\n\n throw new MissingCriticalDataError(\n `Message is missing critical data: ${missing.join(', ')}`,\n );\n}\n","/**\n * Strip a message down to the minimum fields needed for decryption.\n *\n * Useful for minimizing sensitive data in transit — removes sender info,\n * chat context, etc.\n */\n\nconst BLEACH_KEEP = new Set([\n 'type',\n 'clientUrl',\n 'mimetype',\n 'mediaKey',\n 'size',\n 'filehash',\n 'uploadhash',\n 'deprecatedMms3Url',\n]);\n\n/**\n * Return a copy of the message containing only the fields required for decryption.\n */\nexport function bleachMessage<T extends Record<string, unknown>>(\n message: T,\n): Partial<T> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(message)) {\n if (BLEACH_KEEP.has(key)) {\n result[key] = message[key];\n }\n }\n return result as Partial<T>;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@open-wa/decrypt",
3
+ "version": "5.0.0-alpha.0",
4
+ "description": "Lightweight WhatsApp media decryption — zero external dependencies",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.mts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "dependencies": {},
20
+ "devDependencies": {
21
+ "@types/node": "^25.9.1",
22
+ "typescript": "^6.0.3",
23
+ "vitest": "4.1.2",
24
+ "@open-wa/schema": "5.0.0-alpha.0"
25
+ },
26
+ "peerDependencies": {
27
+ "@open-wa/schema": "5.0.0-alpha.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "@open-wa/schema": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "keywords": [
38
+ "open-wa",
39
+ "whatsapp",
40
+ "decrypt",
41
+ "media"
42
+ ],
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/open-wa/wa-automate-nodejs.git"
46
+ },
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "dev": "tsdown --watch",
50
+ "test": "vitest run",
51
+ "lint": "oxlint src",
52
+ "typecheck": "tsc --noEmit"
53
+ }
54
+ }