feat: replace async crypto with sync Rust WASM (port of Baileys b5c1741)
Surgically applies the changes from WhiskeySockets/Baileys commit b5c1741
("feat: replace async crypto with sync Rust WASM" by jlucaso1) while
preserving all custom modifications (interactive messages, carousels,
albums, sticker packs, native flow buttons, Prometheus metrics, etc).
Changes:
- Replace async hkdf/md5 (Web Crypto API) with sync re-exports from whatsapp-rust-bridge@0.5.2
- Replace LTHash class with LTHashAntiTampering from WASM
- Replace mutationKeys() with expandAppStateKeys() from WASM
- Remove ~25 unnecessary await keywords across crypto call chain
- Update Buffer→Uint8Array types for MediaDecryptionKeyInfo and internal crypto functions
- Make noise handshake, media retry encrypt/decrypt, and reporting token generation synchronous
Performance impact:
- Eliminates Promise overhead on every HKDF/LTHash operation
- Significant improvement during app state sync (hundreds of mutations per reconnection)
- Sync crypto reduces event loop pressure under high session load
Custom code preserved (zero conflicts):
- messages-send.ts: All interactive message, carousel, album, sticker pack logic intact
- Types/Message.ts: All custom types (NativeFlowButton, Carousel, Album, etc.) intact
- All Prometheus metrics, circuit breakers, session TTL logic intact
https://claude.ai/code/session_01Ffc5YrPuqv8N9SwEuSM8mr
This commit is contained in:
Generated
+7
@@ -21,6 +21,7 @@
|
||||
"pino": "^9.6",
|
||||
"prom-client": "^15.1.3",
|
||||
"protobufjs": "^7.2.4",
|
||||
"whatsapp-rust-bridge": "0.5.2",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -14568,6 +14569,12 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatsapp-rust-bridge": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/whatsapp-rust-bridge/-/whatsapp-rust-bridge-0.5.2.tgz",
|
||||
"integrity": "sha512-6KBRNvxg6WMIwZ/euA8qVzj16qxMBzLllfmaJIP1JGAAfSvwn6nr8JDOMXeqpXPEOl71UfOG+79JwKEoT2b1Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"pino": "^9.6",
|
||||
"prom-client": "^15.1.3",
|
||||
"protobufjs": "^7.2.4",
|
||||
"whatsapp-rust-bridge": "0.5.2",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -842,7 +842,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
)
|
||||
const random = randomBytes(32)
|
||||
const linkCodeSalt = randomBytes(32)
|
||||
const linkCodePairingExpanded = await hkdf(companionSharedKey, 32, {
|
||||
const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
|
||||
salt: linkCodeSalt,
|
||||
info: 'link_code_pairing_key_bundle_encryption_key'
|
||||
})
|
||||
@@ -856,7 +856,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
|
||||
const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey)
|
||||
const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random])
|
||||
authState.creds.advSecretKey = (await hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64')
|
||||
authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64')
|
||||
await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
|
||||
@@ -1619,7 +1619,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const content = assertMediaContent(message.message)
|
||||
const mediaKey = content.mediaKey!
|
||||
const meId = authState.creds.me!.id
|
||||
const node = await encryptMediaRetryRequest(message.key, mediaKey, meId)
|
||||
const node = encryptMediaRetryRequest(message.key, mediaKey, meId)
|
||||
|
||||
let error: Error | undefined = undefined
|
||||
await Promise.all([
|
||||
@@ -1631,7 +1631,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
error = result.error
|
||||
} else {
|
||||
try {
|
||||
const media = await decryptMediaRetryData(result.media!, mediaKey, result.key.id!)
|
||||
const media = decryptMediaRetryData(result.media!, mediaKey, result.key.id!)
|
||||
if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) {
|
||||
const resultStr = proto.MediaRetryNotification.ResultType[media.result!]
|
||||
throw new Boom(`Media re-upload failed by device (${resultStr})`, {
|
||||
|
||||
@@ -574,7 +574,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
|
||||
logger.trace({ handshake }, 'handshake recv from WA')
|
||||
|
||||
const keyEnc = await noise.processHandshake(handshake, creds.noiseKey)
|
||||
const keyEnc = noise.processHandshake(handshake, creds.noiseKey)
|
||||
|
||||
let node: proto.IClientPayload
|
||||
if (!creds.me) {
|
||||
|
||||
@@ -1018,9 +1018,9 @@ export type WAMessageCursor = { before: WAMessageKey | undefined } | { after: WA
|
||||
export type MessageUserReceiptUpdate = { key: WAMessageKey; receipt: MessageUserReceipt }
|
||||
|
||||
export type MediaDecryptionKeyInfo = {
|
||||
iv: Buffer
|
||||
cipherKey: Buffer
|
||||
macKey?: Buffer
|
||||
iv: Uint8Array
|
||||
cipherKey: Uint8Array
|
||||
macKey?: Uint8Array
|
||||
}
|
||||
|
||||
export type MinimalMessage = Pick<WAMessage, 'key' | 'messageTimestamp'>
|
||||
|
||||
+22
-30
@@ -19,7 +19,8 @@ import {
|
||||
type MessageLabelAssociation
|
||||
} from '../Types/LabelAssociation'
|
||||
import { type BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary'
|
||||
import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from './crypto'
|
||||
import { expandAppStateKeys } from 'whatsapp-rust-bridge'
|
||||
import { aesDecrypt, aesEncrypt, hmacSign } from './crypto'
|
||||
import { toNumber } from './generics'
|
||||
import type { ILogger } from './logger'
|
||||
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
|
||||
@@ -30,22 +31,15 @@ type FetchAppStateSyncKey = (keyId: string) => Promise<proto.Message.IAppStateSy
|
||||
|
||||
export type ChatMutationMap = { [index: string]: ChatMutation }
|
||||
|
||||
const mutationKeys = async (keydata: Uint8Array) => {
|
||||
const expanded = await hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
||||
return {
|
||||
indexKey: expanded.slice(0, 32),
|
||||
valueEncryptionKey: expanded.slice(32, 64),
|
||||
valueMacKey: expanded.slice(64, 96),
|
||||
snapshotMacKey: expanded.slice(96, 128),
|
||||
patchMacKey: expanded.slice(128, 160)
|
||||
}
|
||||
const mutationKeys = (keydata: Uint8Array) => {
|
||||
return expandAppStateKeys(keydata)
|
||||
}
|
||||
|
||||
const generateMac = (
|
||||
operation: proto.SyncdMutation.SyncdOperation,
|
||||
data: Buffer,
|
||||
data: Uint8Array,
|
||||
keyId: Uint8Array | string,
|
||||
key: Buffer
|
||||
key: Uint8Array
|
||||
) => {
|
||||
const getKeyData = () => {
|
||||
let r: number
|
||||
@@ -83,8 +77,8 @@ type Mac = { indexMac: Uint8Array; valueMac: Uint8Array; operation: proto.SyncdM
|
||||
|
||||
const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' | 'indexValueMap'>) => {
|
||||
indexValueMap = { ...indexValueMap }
|
||||
const addBuffs: ArrayBuffer[] = []
|
||||
const subBuffs: ArrayBuffer[] = []
|
||||
const addBuffs: Uint8Array[] = []
|
||||
const subBuffs: Uint8Array[] = []
|
||||
|
||||
return {
|
||||
mix: ({ indexMac, valueMac, operation }: Mac) => {
|
||||
@@ -98,29 +92,27 @@ const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' |
|
||||
// remove from index value mac, since this mutation is erased
|
||||
delete indexValueMap[indexMacBase64]
|
||||
} else {
|
||||
addBuffs.push(new Uint8Array(valueMac).buffer)
|
||||
addBuffs.push(new Uint8Array(valueMac))
|
||||
// add this index into the history map
|
||||
indexValueMap[indexMacBase64] = { valueMac }
|
||||
}
|
||||
|
||||
if (prevOp) {
|
||||
subBuffs.push(new Uint8Array(prevOp.valueMac).buffer)
|
||||
subBuffs.push(new Uint8Array(prevOp.valueMac))
|
||||
}
|
||||
},
|
||||
finish: async () => {
|
||||
const hashArrayBuffer = new Uint8Array(hash).buffer
|
||||
const result = await LT_HASH_ANTI_TAMPERING.subtractThenAdd(hashArrayBuffer, addBuffs, subBuffs)
|
||||
const buffer = Buffer.from(result)
|
||||
finish: () => {
|
||||
const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(new Uint8Array(hash), addBuffs, subBuffs)
|
||||
|
||||
return {
|
||||
hash: buffer,
|
||||
hash: Buffer.from(result),
|
||||
indexValueMap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateSnapshotMac = (lthash: Uint8Array, version: number, name: WAPatchName, key: Buffer) => {
|
||||
const generateSnapshotMac = (lthash: Uint8Array, version: number, name: WAPatchName, key: Uint8Array) => {
|
||||
const total = Buffer.concat([lthash, to64BitNetworkOrder(version), Buffer.from(name, 'utf-8')])
|
||||
return hmacSign(total, key, 'sha256')
|
||||
}
|
||||
@@ -130,7 +122,7 @@ const generatePatchMac = (
|
||||
valueMacs: Uint8Array[],
|
||||
version: number,
|
||||
type: WAPatchName,
|
||||
key: Buffer
|
||||
key: Uint8Array
|
||||
) => {
|
||||
const total = Buffer.concat([snapshotMac, ...valueMacs, to64BitNetworkOrder(version), Buffer.from(type, 'utf-8')])
|
||||
return hmacSign(total, key)
|
||||
@@ -162,7 +154,7 @@ export const encodeSyncdPatch = async (
|
||||
})
|
||||
const encoded = proto.SyncActionData.encode(dataProto).finish()
|
||||
|
||||
const keyValue = await mutationKeys(key.keyData!)
|
||||
const keyValue = mutationKeys(key.keyData!)
|
||||
|
||||
const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
|
||||
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
|
||||
@@ -171,7 +163,7 @@ export const encodeSyncdPatch = async (
|
||||
// update LT hash
|
||||
const generator = makeLtHashGenerator(state)
|
||||
generator.mix({ indexMac, valueMac, operation })
|
||||
Object.assign(state, await generator.finish())
|
||||
Object.assign(state, generator.finish())
|
||||
|
||||
state.version += 1
|
||||
|
||||
@@ -252,7 +244,7 @@ export const decodeSyncdMutations = async (
|
||||
})
|
||||
}
|
||||
|
||||
return await ltGenerator.finish()
|
||||
return ltGenerator.finish()
|
||||
|
||||
async function getKey(keyId: Uint8Array) {
|
||||
const base64Key = Buffer.from(keyId).toString('base64')
|
||||
@@ -264,7 +256,7 @@ export const decodeSyncdMutations = async (
|
||||
})
|
||||
}
|
||||
|
||||
return await mutationKeys(keyEnc.keyData!)
|
||||
return mutationKeys(keyEnc.keyData!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +275,7 @@ export const decodeSyncdPatch = async (
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } })
|
||||
}
|
||||
|
||||
const mainKey = await mutationKeys(mainKeyObj.keyData!)
|
||||
const mainKey = mutationKeys(mainKeyObj.keyData!)
|
||||
const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32))
|
||||
|
||||
const patchMac = generatePatchMac(
|
||||
@@ -405,7 +397,7 @@ export const decodeSyncdSnapshot = async (
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
||||
}
|
||||
|
||||
const result = await mutationKeys(keyEnc.keyData!)
|
||||
const result = mutationKeys(keyEnc.keyData!)
|
||||
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||
if (Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
|
||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`)
|
||||
@@ -473,7 +465,7 @@ export const decodePatches = async (
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
||||
}
|
||||
|
||||
const result = await mutationKeys(keyEnc.keyData!)
|
||||
const result = mutationKeys(keyEnc.keyData!)
|
||||
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||
if (Buffer.compare(snapshotMac!, computedSnapshotMac) !== 0) {
|
||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`)
|
||||
|
||||
+5
-41
@@ -82,18 +82,18 @@ export function aesDecryptCTR(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8
|
||||
}
|
||||
|
||||
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
|
||||
export function aesDecrypt(buffer: Buffer, key: Buffer) {
|
||||
return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16))
|
||||
export function aesDecrypt(buffer: Uint8Array, key: Uint8Array) {
|
||||
return aesDecryptWithIV(buffer.subarray(16, buffer.length), key, buffer.subarray(0, 16))
|
||||
}
|
||||
|
||||
/** decrypt AES 256 CBC */
|
||||
export function aesDecryptWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
|
||||
export function aesDecryptWithIV(buffer: Uint8Array, key: Uint8Array, IV: Uint8Array) {
|
||||
const aes = createDecipheriv('aes-256-cbc', key, IV)
|
||||
return Buffer.concat([aes.update(buffer), aes.final()])
|
||||
}
|
||||
|
||||
// encrypt AES 256 CBC; where a random IV is prefixed to the buffer
|
||||
export function aesEncrypt(buffer: Buffer | Uint8Array, key: Buffer) {
|
||||
export function aesEncrypt(buffer: Buffer | Uint8Array, key: Uint8Array) {
|
||||
const IV = randomBytes(16)
|
||||
const aes = createCipheriv('aes-256-cbc', key, IV)
|
||||
return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer
|
||||
@@ -118,43 +118,7 @@ export function sha256(buffer: Buffer) {
|
||||
return createHash('sha256').update(buffer).digest()
|
||||
}
|
||||
|
||||
export function md5(buffer: Buffer) {
|
||||
return createHash('md5').update(buffer).digest()
|
||||
}
|
||||
|
||||
// HKDF key expansion
|
||||
export async function hkdf(
|
||||
buffer: Uint8Array | Buffer,
|
||||
expandedLength: number,
|
||||
info: { salt?: Buffer; info?: string }
|
||||
): Promise<Buffer> {
|
||||
// Normalize to a Uint8Array whose underlying buffer is a regular ArrayBuffer (not ArrayBufferLike)
|
||||
// Cloning via new Uint8Array(...) guarantees the generic parameter is ArrayBuffer which satisfies WebCrypto types.
|
||||
const inputKeyMaterial = new Uint8Array(buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer))
|
||||
|
||||
// Set default values if not provided
|
||||
const salt = info.salt ? new Uint8Array(info.salt) : new Uint8Array(0)
|
||||
const infoBytes = info.info ? new TextEncoder().encode(info.info) : new Uint8Array(0)
|
||||
|
||||
// Import the input key material (cast to BufferSource to appease TS DOM typings)
|
||||
const importedKey = await subtle.importKey('raw', inputKeyMaterial as BufferSource, { name: 'HKDF' }, false, [
|
||||
'deriveBits'
|
||||
])
|
||||
|
||||
// Derive bits using HKDF
|
||||
const derivedBits = await subtle.deriveBits(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: salt,
|
||||
info: infoBytes
|
||||
},
|
||||
importedKey,
|
||||
expandedLength * 8 // Convert bytes to bits
|
||||
)
|
||||
|
||||
return Buffer.from(derivedBits)
|
||||
}
|
||||
export { hkdf, md5 } from 'whatsapp-rust-bridge'
|
||||
|
||||
export async function derivePairingCodeKey(pairingCode: string, salt: Buffer): Promise<Buffer> {
|
||||
// Convert inputs to formats Web Crypto API can work with
|
||||
|
||||
+2
-64
@@ -1,65 +1,3 @@
|
||||
import { hkdf } from './crypto'
|
||||
import { LTHashAntiTampering } from 'whatsapp-rust-bridge'
|
||||
|
||||
/**
|
||||
* LT Hash is a summation based hash algorithm that maintains the integrity of a piece of data
|
||||
* over a series of mutations. You can add/remove mutations and it'll return a hash equal to
|
||||
* if the same series of mutations was made sequentially.
|
||||
*/
|
||||
const o = 128
|
||||
|
||||
class LTHash {
|
||||
salt: string
|
||||
|
||||
constructor(e: string) {
|
||||
this.salt = e
|
||||
}
|
||||
|
||||
async add(e: ArrayBuffer, t: ArrayBuffer[]): Promise<ArrayBuffer> {
|
||||
for (const item of t) {
|
||||
e = await this._addSingle(e, item)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
async subtract(e: ArrayBuffer, t: ArrayBuffer[]): Promise<ArrayBuffer> {
|
||||
for (const item of t) {
|
||||
e = await this._subtractSingle(e, item)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
async subtractThenAdd(e: ArrayBuffer, addList: ArrayBuffer[], subtractList: ArrayBuffer[]): Promise<ArrayBuffer> {
|
||||
const subtracted = await this.subtract(e, subtractList)
|
||||
return this.add(subtracted, addList)
|
||||
}
|
||||
|
||||
private async _addSingle(e: ArrayBuffer, t: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const derived = new Uint8Array(await hkdf(Buffer.from(t), o, { info: this.salt })).buffer
|
||||
return this.performPointwiseWithOverflow(e, derived, (a, b) => a + b)
|
||||
}
|
||||
|
||||
private async _subtractSingle(e: ArrayBuffer, t: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const derived = new Uint8Array(await hkdf(Buffer.from(t), o, { info: this.salt })).buffer
|
||||
return this.performPointwiseWithOverflow(e, derived, (a, b) => a - b)
|
||||
}
|
||||
|
||||
private performPointwiseWithOverflow(
|
||||
e: ArrayBuffer,
|
||||
t: ArrayBuffer,
|
||||
op: (a: number, b: number) => number
|
||||
): ArrayBuffer {
|
||||
const n = new DataView(e)
|
||||
const i = new DataView(t)
|
||||
const out = new ArrayBuffer(n.byteLength)
|
||||
const s = new DataView(out)
|
||||
|
||||
for (let offset = 0; offset < n.byteLength; offset += 2) {
|
||||
s.setUint16(offset, op(n.getUint16(offset, true), i.getUint16(offset, true)), true)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
}
|
||||
export const LT_HASH_ANTI_TAMPERING = new LTHash('WhatsApp Patch Integrity')
|
||||
export const LT_HASH_ANTI_TAMPERING = new LTHashAntiTampering()
|
||||
|
||||
@@ -98,10 +98,10 @@ export const getRawMediaUploadData = async (media: WAMediaUpload, mediaType: Med
|
||||
}
|
||||
|
||||
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
||||
export async function getMediaKeys(
|
||||
export function getMediaKeys(
|
||||
buffer: Uint8Array | string | null | undefined,
|
||||
mediaType: MediaType
|
||||
): Promise<MediaDecryptionKeyInfo> {
|
||||
): MediaDecryptionKeyInfo {
|
||||
if (!buffer) {
|
||||
throw new Boom('Cannot derive from empty media key')
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export async function getMediaKeys(
|
||||
}
|
||||
|
||||
// expand using HKDF to 112 bytes, also pass in the relevant app info
|
||||
const expandedMediaKey = await hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
|
||||
const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
|
||||
return {
|
||||
iv: expandedMediaKey.slice(0, 16),
|
||||
cipherKey: expandedMediaKey.slice(16, 48),
|
||||
@@ -400,7 +400,7 @@ export const encryptedStream = async (
|
||||
|
||||
// Use provided mediaKey or generate new one
|
||||
const mediaKey = providedMediaKey || Crypto.randomBytes(32)
|
||||
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType)
|
||||
const { cipherKey, iv, macKey } = getMediaKeys(mediaKey, mediaType)
|
||||
|
||||
const encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + '-enc')
|
||||
const encFileWriteStream = createWriteStream(encFilePath)
|
||||
@@ -534,7 +534,7 @@ export const downloadContentFromMessage = async (
|
||||
throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 })
|
||||
}
|
||||
|
||||
const keys = await getMediaKeys(mediaKey, type)
|
||||
const keys = getMediaKeys(mediaKey, type)
|
||||
|
||||
return downloadEncryptedContent(downloadUrl, keys, opts)
|
||||
}
|
||||
@@ -896,12 +896,12 @@ const getMediaRetryKey = (mediaKey: Buffer | Uint8Array) => {
|
||||
/**
|
||||
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
|
||||
*/
|
||||
export const encryptMediaRetryRequest = async (key: WAMessageKey, mediaKey: Buffer | Uint8Array, meId: string) => {
|
||||
export const encryptMediaRetryRequest = (key: WAMessageKey, mediaKey: Buffer | Uint8Array, meId: string) => {
|
||||
const recp: proto.IServerErrorReceipt = { stanzaId: key.id }
|
||||
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
|
||||
|
||||
const iv = Crypto.randomBytes(12)
|
||||
const retryKey = await getMediaRetryKey(mediaKey)
|
||||
const retryKey = getMediaRetryKey(mediaKey)
|
||||
const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id!))
|
||||
|
||||
const req: BinaryNode = {
|
||||
@@ -971,12 +971,12 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
|
||||
return event
|
||||
}
|
||||
|
||||
export const decryptMediaRetryData = async (
|
||||
export const decryptMediaRetryData = (
|
||||
{ ciphertext, iv }: { ciphertext: Uint8Array; iv: Uint8Array },
|
||||
mediaKey: Uint8Array,
|
||||
msgId: string
|
||||
) => {
|
||||
const retryKey = await getMediaRetryKey(mediaKey)
|
||||
const retryKey = getMediaRetryKey(mediaKey)
|
||||
const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId))
|
||||
return proto.MediaRetryNotification.decode(plaintext)
|
||||
}
|
||||
|
||||
+15
-15
@@ -24,8 +24,8 @@ class TransportState {
|
||||
private readonly iv = new Uint8Array(IV_LENGTH)
|
||||
|
||||
constructor(
|
||||
private readonly encKey: Buffer,
|
||||
private readonly decKey: Buffer
|
||||
private readonly encKey: Uint8Array,
|
||||
private readonly decKey: Uint8Array
|
||||
) {}
|
||||
|
||||
encrypt(plaintext: Uint8Array): Uint8Array {
|
||||
@@ -116,22 +116,22 @@ export const makeNoiseHandler = ({
|
||||
return result
|
||||
}
|
||||
|
||||
const localHKDF = async (data: Uint8Array): Promise<[Buffer, Buffer]> => {
|
||||
const key = await hkdf(Buffer.from(data), 64, { salt, info: '' })
|
||||
return [key.subarray(0, 32) as Buffer, key.subarray(32) as Buffer]
|
||||
const localHKDF = (data: Uint8Array): [Uint8Array, Uint8Array] => {
|
||||
const key = hkdf(Buffer.from(data), 64, { salt, info: '' })
|
||||
return [key.subarray(0, 32), key.subarray(32)]
|
||||
}
|
||||
|
||||
const mixIntoKey = async (data: Uint8Array) => {
|
||||
const [write, read] = await localHKDF(data)
|
||||
salt = write
|
||||
encKey = read
|
||||
decKey = read
|
||||
const mixIntoKey = (data: Uint8Array) => {
|
||||
const [write, read] = localHKDF(data)
|
||||
salt = write as Buffer
|
||||
encKey = read as Buffer
|
||||
decKey = read as Buffer
|
||||
counter = 0
|
||||
}
|
||||
|
||||
const finishInit = async () => {
|
||||
isWaitingForTransport = true
|
||||
const [write, read] = await localHKDF(new Uint8Array(0))
|
||||
const [write, read] = localHKDF(new Uint8Array(0))
|
||||
transport = new TransportState(write, read)
|
||||
isWaitingForTransport = false
|
||||
|
||||
@@ -179,12 +179,12 @@ export const makeNoiseHandler = ({
|
||||
authenticate,
|
||||
mixIntoKey,
|
||||
finishInit,
|
||||
processHandshake: async ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
|
||||
processHandshake: ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
|
||||
authenticate(serverHello!.ephemeral!)
|
||||
await mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!))
|
||||
mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!))
|
||||
|
||||
const decStaticContent = decrypt(serverHello!.static!)
|
||||
await mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
|
||||
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
|
||||
|
||||
const certDecoded = decrypt(serverHello!.payload!)
|
||||
|
||||
@@ -223,7 +223,7 @@ export const makeNoiseHandler = ({
|
||||
}
|
||||
|
||||
const keyEnc = encrypt(noiseKey.public)
|
||||
await mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!))
|
||||
mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!))
|
||||
|
||||
return keyEnc
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ export const shouldIncludeReportingToken = (message: proto.IMessage): boolean =>
|
||||
!message.encEventResponseMessage &&
|
||||
!message.pollUpdateMessage
|
||||
|
||||
const generateMsgSecretKey = async (
|
||||
const generateMsgSecretKey = (
|
||||
modificationType: string,
|
||||
origMsgId: string,
|
||||
origMsgSender: string,
|
||||
@@ -321,7 +321,7 @@ export const getMessageReportingToken = async (
|
||||
const from = key.fromMe ? key.remoteJid! : key.participant || key.remoteJid!
|
||||
const to = key.fromMe ? key.participant || key.remoteJid! : key.remoteJid!
|
||||
|
||||
const reportingSecret = await generateMsgSecretKey(ENC_SECRET_REPORT_TOKEN, key.id, from, to, msgSecret)
|
||||
const reportingSecret = generateMsgSecretKey(ENC_SECRET_REPORT_TOKEN, key.id, from, to, msgSecret)
|
||||
|
||||
const content = extractReportingTokenContent(msgProtobuf, compiledReportingFields)
|
||||
if (!content || content.length === 0) {
|
||||
|
||||
@@ -7071,6 +7071,11 @@ webidl-conversions@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
whatsapp-rust-bridge@0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.npmjs.org/whatsapp-rust-bridge/-/whatsapp-rust-bridge-0.5.2.tgz"
|
||||
integrity sha512-6KBRNvxg6WMIwZ/euA8qVzj16qxMBzLllfmaJIP1JGAAfSvwn6nr8JDOMXeqpXPEOl71UfOG+79JwKEoT2b1Fw==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user