@open-wa/decrypt 5.0.0-alpha.7 → 5.0.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ pnpm add @open-wa/decrypt
12
12
 
13
13
  ## Documentation
14
14
 
15
- See the [docs site](https://docs.openwa.dev).
15
+ See the [docs site](https://openwa.dev).
16
16
 
17
17
  ## License
18
18
 
package/dist/index.cjs CHANGED
@@ -83,7 +83,7 @@ async function downloadEncryptedMedia(url, options) {
83
83
  for (let attempt = 0; attempt < maxRetries; attempt++) {
84
84
  const res = await fetch(url, { headers });
85
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");
86
+ if (res.status === 404) throw new Error("This media does not exist or is no longer available on the server. See: https://openwa.dev/pages/How%20to/decrypt-media.html#40439d");
87
87
  if (attempt < maxRetries - 1) await new Promise((r) => setTimeout(r, retryDelay));
88
88
  }
89
89
  throw new Error(`Failed to download media after ${maxRetries} attempts`);
@@ -1 +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":"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"}
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://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/dist/index.mjs CHANGED
@@ -82,7 +82,7 @@ async function downloadEncryptedMedia(url, options) {
82
82
  for (let attempt = 0; attempt < maxRetries; attempt++) {
83
83
  const res = await fetch(url, { headers });
84
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");
85
+ if (res.status === 404) throw new Error("This media does not exist or is no longer available on the server. See: https://openwa.dev/pages/How%20to/decrypt-media.html#40439d");
86
86
  if (attempt < maxRetries - 1) await new Promise((r) => setTimeout(r, retryDelay));
87
87
  }
88
88
  throw new Error(`Failed to download media after ${maxRetries} attempts`);
@@ -1 +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":";;;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,KACtB,SAAS,UAAU,eAAe,OAAO,MAAM,GAAG,EAAE,MAAM,IAAI,CAC/D;CAED,MAAM,KAAK,SAAS,SAAS,GAAG,GAAG;CAGnC,MAAM,WAAW,iBAAiB,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"}
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://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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-wa/decrypt",
3
- "version": "5.0.0-alpha.7",
3
+ "version": "5.0.0-alpha.8",
4
4
  "description": "Lightweight WhatsApp media decryption — zero external dependencies",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -21,10 +21,10 @@
21
21
  "@types/node": "^25.9.1",
22
22
  "typescript": "^6.0.3",
23
23
  "vitest": "4.1.2",
24
- "@open-wa/schema": "5.0.0-alpha.7"
24
+ "@open-wa/schema": "5.0.0-alpha.8"
25
25
  },
26
26
  "peerDependencies": {
27
- "@open-wa/schema": "5.0.0-alpha.7"
27
+ "@open-wa/schema": "5.0.0-alpha.8"
28
28
  },
29
29
  "peerDependenciesMeta": {
30
30
  "@open-wa/schema": {