feat: replace async crypto with sync Rust WASM for app state sync (#2315)
* feat: replace async crypto with sync Rust WASM for app state sync * fix: remove unecessary buffer copying * fix: update whatsapp-rust-bridge to version 0.5.2 and refactor async calls to sync. HKDF and MD5 in rust
This commit is contained in:
@@ -50,6 +50,7 @@
|
|||||||
"p-queue": "^9.0.0",
|
"p-queue": "^9.0.0",
|
||||||
"pino": "^9.6",
|
"pino": "^9.6",
|
||||||
"protobufjs": "^7.2.4",
|
"protobufjs": "^7.2.4",
|
||||||
|
"whatsapp-rust-bridge": "0.5.2",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -818,7 +818,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
)
|
)
|
||||||
const random = randomBytes(32)
|
const random = randomBytes(32)
|
||||||
const linkCodeSalt = randomBytes(32)
|
const linkCodeSalt = randomBytes(32)
|
||||||
const linkCodePairingExpanded = await hkdf(companionSharedKey, 32, {
|
const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
|
||||||
salt: linkCodeSalt,
|
salt: linkCodeSalt,
|
||||||
info: 'link_code_pairing_key_bundle_encryption_key'
|
info: 'link_code_pairing_key_bundle_encryption_key'
|
||||||
})
|
})
|
||||||
@@ -832,7 +832,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
|
const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
|
||||||
const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey)
|
const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey)
|
||||||
const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random])
|
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({
|
await query({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
|
|||||||
@@ -1159,7 +1159,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
const content = assertMediaContent(message.message)
|
const content = assertMediaContent(message.message)
|
||||||
const mediaKey = content.mediaKey!
|
const mediaKey = content.mediaKey!
|
||||||
const meId = authState.creds.me!.id
|
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
|
let error: Error | undefined = undefined
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -1171,7 +1171,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
error = result.error
|
error = result.error
|
||||||
} else {
|
} else {
|
||||||
try {
|
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) {
|
if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) {
|
||||||
const resultStr = proto.MediaRetryNotification.ResultType[media.result!]
|
const resultStr = proto.MediaRetryNotification.ResultType[media.result!]
|
||||||
throw new Boom(`Media re-upload failed by device (${resultStr})`, {
|
throw new Boom(`Media re-upload failed by device (${resultStr})`, {
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
logger.trace({ handshake }, 'handshake recv from WA')
|
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
|
let node: proto.IClientPayload
|
||||||
if (!creds.me) {
|
if (!creds.me) {
|
||||||
|
|||||||
@@ -372,9 +372,9 @@ export type WAMessageCursor = { before: WAMessageKey | undefined } | { after: WA
|
|||||||
export type MessageUserReceiptUpdate = { key: WAMessageKey; receipt: MessageUserReceipt }
|
export type MessageUserReceiptUpdate = { key: WAMessageKey; receipt: MessageUserReceipt }
|
||||||
|
|
||||||
export type MediaDecryptionKeyInfo = {
|
export type MediaDecryptionKeyInfo = {
|
||||||
iv: Buffer
|
iv: Uint8Array
|
||||||
cipherKey: Buffer
|
cipherKey: Uint8Array
|
||||||
macKey?: Buffer
|
macKey?: Uint8Array
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MinimalMessage = Pick<WAMessage, 'key' | 'messageTimestamp'>
|
export type MinimalMessage = Pick<WAMessage, 'key' | 'messageTimestamp'>
|
||||||
|
|||||||
+52
-51
@@ -1,4 +1,5 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
|
import { expandAppStateKeys } from 'whatsapp-rust-bridge'
|
||||||
import { proto } from '../../WAProto/index.js'
|
import { proto } from '../../WAProto/index.js'
|
||||||
import type {
|
import type {
|
||||||
BaileysEventEmitter,
|
BaileysEventEmitter,
|
||||||
@@ -19,7 +20,7 @@ import {
|
|||||||
type MessageLabelAssociation
|
type MessageLabelAssociation
|
||||||
} from '../Types/LabelAssociation'
|
} from '../Types/LabelAssociation'
|
||||||
import { type BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary'
|
import { type BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary'
|
||||||
import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from './crypto'
|
import { aesDecrypt, aesEncrypt, hmacSign } from './crypto'
|
||||||
import { toNumber } from './generics'
|
import { toNumber } from './generics'
|
||||||
import type { ILogger } from './logger'
|
import type { ILogger } from './logger'
|
||||||
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
|
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
|
||||||
@@ -30,47 +31,39 @@ type FetchAppStateSyncKey = (keyId: string) => Promise<proto.Message.IAppStateSy
|
|||||||
|
|
||||||
export type ChatMutationMap = { [index: string]: ChatMutation }
|
export type ChatMutationMap = { [index: string]: ChatMutation }
|
||||||
|
|
||||||
const mutationKeys = async (keydata: Uint8Array) => {
|
const mutationKeys = (keydata: Uint8Array) => {
|
||||||
const expanded = await hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
const keys = expandAppStateKeys(keydata)
|
||||||
return {
|
return {
|
||||||
indexKey: expanded.slice(0, 32),
|
indexKey: keys.indexKey,
|
||||||
valueEncryptionKey: expanded.slice(32, 64),
|
valueEncryptionKey: keys.valueEncryptionKey,
|
||||||
valueMacKey: expanded.slice(64, 96),
|
valueMacKey: keys.valueMacKey,
|
||||||
snapshotMacKey: expanded.slice(96, 128),
|
snapshotMacKey: keys.snapshotMacKey,
|
||||||
patchMacKey: expanded.slice(128, 160)
|
patchMacKey: keys.patchMacKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateMac = (
|
const generateMac = (
|
||||||
operation: proto.SyncdMutation.SyncdOperation,
|
operation: proto.SyncdMutation.SyncdOperation,
|
||||||
data: Buffer,
|
data: Uint8Array,
|
||||||
keyId: Uint8Array | string,
|
keyId: Uint8Array | string,
|
||||||
key: Buffer
|
key: Uint8Array
|
||||||
) => {
|
) => {
|
||||||
const getKeyData = () => {
|
const opByte = operation === proto.SyncdMutation.SyncdOperation.SET ? 0x01 : 0x02
|
||||||
let r: number
|
const keyIdBuffer = typeof keyId === 'string' ? Buffer.from(keyId, 'base64') : keyId
|
||||||
switch (operation) {
|
const keyData = new Uint8Array(1 + keyIdBuffer.length)
|
||||||
case proto.SyncdMutation.SyncdOperation.SET:
|
keyData[0] = opByte
|
||||||
r = 0x01
|
keyData.set(keyIdBuffer, 1)
|
||||||
break
|
|
||||||
case proto.SyncdMutation.SyncdOperation.REMOVE:
|
|
||||||
r = 0x02
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const buff = Buffer.from([r])
|
const last = new Uint8Array(8)
|
||||||
return Buffer.concat([buff, Buffer.from(keyId as string, 'base64')])
|
last[7] = keyData.length
|
||||||
}
|
|
||||||
|
|
||||||
const keyData = getKeyData()
|
const total = new Uint8Array(keyData.length + data.length + last.length)
|
||||||
|
total.set(keyData, 0)
|
||||||
|
total.set(data, keyData.length)
|
||||||
|
total.set(last, keyData.length + data.length)
|
||||||
|
|
||||||
const last = Buffer.alloc(8) // 8 bytes
|
|
||||||
last.set([keyData.length], last.length - 1)
|
|
||||||
|
|
||||||
const total = Buffer.concat([keyData, data, last])
|
|
||||||
const hmac = hmacSign(total, key, 'sha512')
|
const hmac = hmacSign(total, key, 'sha512')
|
||||||
|
return hmac.subarray(0, 32)
|
||||||
return hmac.slice(0, 32)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const to64BitNetworkOrder = (e: number) => {
|
const to64BitNetworkOrder = (e: number) => {
|
||||||
@@ -83,8 +76,8 @@ type Mac = { indexMac: Uint8Array; valueMac: Uint8Array; operation: proto.SyncdM
|
|||||||
|
|
||||||
const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' | 'indexValueMap'>) => {
|
const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' | 'indexValueMap'>) => {
|
||||||
indexValueMap = { ...indexValueMap }
|
indexValueMap = { ...indexValueMap }
|
||||||
const addBuffs: ArrayBuffer[] = []
|
const addBuffs: Uint8Array[] = []
|
||||||
const subBuffs: ArrayBuffer[] = []
|
const subBuffs: Uint8Array[] = []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mix: ({ indexMac, valueMac, operation }: Mac) => {
|
mix: ({ indexMac, valueMac, operation }: Mac) => {
|
||||||
@@ -98,29 +91,27 @@ const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' |
|
|||||||
// remove from index value mac, since this mutation is erased
|
// remove from index value mac, since this mutation is erased
|
||||||
delete indexValueMap[indexMacBase64]
|
delete indexValueMap[indexMacBase64]
|
||||||
} else {
|
} else {
|
||||||
addBuffs.push(new Uint8Array(valueMac).buffer)
|
addBuffs.push(valueMac)
|
||||||
// add this index into the history map
|
// add this index into the history map
|
||||||
indexValueMap[indexMacBase64] = { valueMac }
|
indexValueMap[indexMacBase64] = { valueMac }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevOp) {
|
if (prevOp) {
|
||||||
subBuffs.push(new Uint8Array(prevOp.valueMac).buffer)
|
subBuffs.push(prevOp.valueMac as Uint8Array)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
finish: async () => {
|
finish: () => {
|
||||||
const hashArrayBuffer = new Uint8Array(hash).buffer
|
const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(hash, subBuffs, addBuffs)
|
||||||
const result = await LT_HASH_ANTI_TAMPERING.subtractThenAdd(hashArrayBuffer, addBuffs, subBuffs)
|
|
||||||
const buffer = Buffer.from(result)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hash: buffer,
|
hash: Buffer.from(result),
|
||||||
indexValueMap
|
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')])
|
const total = Buffer.concat([lthash, to64BitNetworkOrder(version), Buffer.from(name, 'utf-8')])
|
||||||
return hmacSign(total, key, 'sha256')
|
return hmacSign(total, key, 'sha256')
|
||||||
}
|
}
|
||||||
@@ -130,7 +121,7 @@ const generatePatchMac = (
|
|||||||
valueMacs: Uint8Array[],
|
valueMacs: Uint8Array[],
|
||||||
version: number,
|
version: number,
|
||||||
type: WAPatchName,
|
type: WAPatchName,
|
||||||
key: Buffer
|
key: Uint8Array
|
||||||
) => {
|
) => {
|
||||||
const total = Buffer.concat([snapshotMac, ...valueMacs, to64BitNetworkOrder(version), Buffer.from(type, 'utf-8')])
|
const total = Buffer.concat([snapshotMac, ...valueMacs, to64BitNetworkOrder(version), Buffer.from(type, 'utf-8')])
|
||||||
return hmacSign(total, key)
|
return hmacSign(total, key)
|
||||||
@@ -162,7 +153,7 @@ export const encodeSyncdPatch = async (
|
|||||||
})
|
})
|
||||||
const encoded = proto.SyncActionData.encode(dataProto).finish()
|
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 encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
|
||||||
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
|
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
|
||||||
@@ -171,7 +162,7 @@ export const encodeSyncdPatch = async (
|
|||||||
// update LT hash
|
// update LT hash
|
||||||
const generator = makeLtHashGenerator(state)
|
const generator = makeLtHashGenerator(state)
|
||||||
generator.mix({ indexMac, valueMac, operation })
|
generator.mix({ indexMac, valueMac, operation })
|
||||||
Object.assign(state, await generator.finish())
|
Object.assign(state, generator.finish())
|
||||||
|
|
||||||
state.version += 1
|
state.version += 1
|
||||||
|
|
||||||
@@ -211,6 +202,8 @@ export const decodeSyncdMutations = async (
|
|||||||
validateMacs: boolean
|
validateMacs: boolean
|
||||||
) => {
|
) => {
|
||||||
const ltGenerator = makeLtHashGenerator(initialState)
|
const ltGenerator = makeLtHashGenerator(initialState)
|
||||||
|
const derivedKeyCache = new Map<string, ReturnType<typeof mutationKeys>>()
|
||||||
|
|
||||||
// indexKey used to HMAC sign record.index.blob
|
// indexKey used to HMAC sign record.index.blob
|
||||||
// valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
|
// valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
|
||||||
// the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId
|
// the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId
|
||||||
@@ -222,9 +215,9 @@ export const decodeSyncdMutations = async (
|
|||||||
'record' in msgMutation && !!msgMutation.record ? msgMutation.record : (msgMutation as proto.ISyncdRecord)
|
'record' in msgMutation && !!msgMutation.record ? msgMutation.record : (msgMutation as proto.ISyncdRecord)
|
||||||
|
|
||||||
const key = await getKey(record.keyId!.id!)
|
const key = await getKey(record.keyId!.id!)
|
||||||
const content = Buffer.from(record.value!.blob!)
|
const content = record.value!.blob!
|
||||||
const encContent = content.slice(0, -32)
|
const encContent = content.subarray(0, -32)
|
||||||
const ogValueMac = content.slice(-32)
|
const ogValueMac = content.subarray(-32)
|
||||||
if (validateMacs) {
|
if (validateMacs) {
|
||||||
const contentHmac = generateMac(operation!, encContent, record.keyId!.id!, key.valueMacKey)
|
const contentHmac = generateMac(operation!, encContent, record.keyId!.id!, key.valueMacKey)
|
||||||
if (Buffer.compare(contentHmac, ogValueMac) !== 0) {
|
if (Buffer.compare(contentHmac, ogValueMac) !== 0) {
|
||||||
@@ -252,10 +245,16 @@ export const decodeSyncdMutations = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return await ltGenerator.finish()
|
return ltGenerator.finish()
|
||||||
|
|
||||||
async function getKey(keyId: Uint8Array) {
|
async function getKey(keyId: Uint8Array) {
|
||||||
const base64Key = Buffer.from(keyId).toString('base64')
|
const base64Key = Buffer.from(keyId).toString('base64')
|
||||||
|
|
||||||
|
const cached = derivedKeyCache.get(base64Key)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||||
if (!keyEnc) {
|
if (!keyEnc) {
|
||||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, {
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, {
|
||||||
@@ -264,7 +263,9 @@ export const decodeSyncdMutations = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return mutationKeys(keyEnc.keyData!)
|
const keys = mutationKeys(keyEnc.keyData!)
|
||||||
|
derivedKeyCache.set(base64Key, keys)
|
||||||
|
return keys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +284,7 @@ export const decodeSyncdPatch = async (
|
|||||||
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } })
|
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 mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32))
|
||||||
|
|
||||||
const patchMac = generatePatchMac(
|
const patchMac = generatePatchMac(
|
||||||
@@ -405,7 +406,7 @@ export const decodeSyncdSnapshot = async (
|
|||||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
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)
|
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||||
if (Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
|
if (Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
|
||||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`)
|
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`)
|
||||||
@@ -473,7 +474,7 @@ export const decodePatches = async (
|
|||||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
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)
|
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||||
if (Buffer.compare(snapshotMac!, computedSnapshotMac) !== 0) {
|
if (Buffer.compare(snapshotMac!, computedSnapshotMac) !== 0) {
|
||||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`)
|
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`)
|
||||||
|
|||||||
+5
-42
@@ -2,6 +2,7 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes }
|
|||||||
import * as curve from 'libsignal/src/curve'
|
import * as curve from 'libsignal/src/curve'
|
||||||
import { KEY_BUNDLE_TYPE } from '../Defaults'
|
import { KEY_BUNDLE_TYPE } from '../Defaults'
|
||||||
import type { KeyPair } from '../Types'
|
import type { KeyPair } from '../Types'
|
||||||
|
export { md5, hkdf } from 'whatsapp-rust-bridge'
|
||||||
|
|
||||||
// insure browser & node compatibility
|
// insure browser & node compatibility
|
||||||
const { subtle } = globalThis.crypto
|
const { subtle } = globalThis.crypto
|
||||||
@@ -82,18 +83,18 @@ export function aesDecryptCTR(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
|
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
|
||||||
export function aesDecrypt(buffer: Buffer, key: Buffer) {
|
export function aesDecrypt(buffer: Uint8Array, key: Uint8Array) {
|
||||||
return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16))
|
return aesDecryptWithIV(buffer.subarray(16), key, buffer.subarray(0, 16))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** decrypt AES 256 CBC */
|
/** 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)
|
const aes = createDecipheriv('aes-256-cbc', key, IV)
|
||||||
return Buffer.concat([aes.update(buffer), aes.final()])
|
return Buffer.concat([aes.update(buffer), aes.final()])
|
||||||
}
|
}
|
||||||
|
|
||||||
// encrypt AES 256 CBC; where a random IV is prefixed to the buffer
|
// 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: Uint8Array, key: Uint8Array) {
|
||||||
const IV = randomBytes(16)
|
const IV = randomBytes(16)
|
||||||
const aes = createCipheriv('aes-256-cbc', key, IV)
|
const aes = createCipheriv('aes-256-cbc', key, IV)
|
||||||
return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer
|
return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer
|
||||||
@@ -118,44 +119,6 @@ export function sha256(buffer: Buffer) {
|
|||||||
return createHash('sha256').update(buffer).digest()
|
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 async function derivePairingCodeKey(pairingCode: string, salt: Buffer): Promise<Buffer> {
|
export async function derivePairingCodeKey(pairingCode: string, salt: Buffer): Promise<Buffer> {
|
||||||
// Convert inputs to formats Web Crypto API can work with
|
// Convert inputs to formats Web Crypto API can work with
|
||||||
const encoder = new TextEncoder()
|
const encoder = new TextEncoder()
|
||||||
|
|||||||
+2
-59
@@ -1,65 +1,8 @@
|
|||||||
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
|
* 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
|
* 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.
|
* if the same series of mutations was made sequentially.
|
||||||
*/
|
*/
|
||||||
const o = 128
|
export const LT_HASH_ANTI_TAMPERING = new LTHashAntiTampering()
|
||||||
|
|
||||||
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')
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export async function getMediaKeys(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// expand using HKDF to 112 bytes, also pass in the relevant app info
|
// 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 {
|
return {
|
||||||
iv: expandedMediaKey.slice(0, 16),
|
iv: expandedMediaKey.slice(0, 16),
|
||||||
cipherKey: expandedMediaKey.slice(16, 48),
|
cipherKey: expandedMediaKey.slice(16, 48),
|
||||||
@@ -888,12 +888,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
|
* 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 recp: proto.IServerErrorReceipt = { stanzaId: key.id }
|
||||||
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
|
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
|
||||||
|
|
||||||
const iv = Crypto.randomBytes(12)
|
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 ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id!))
|
||||||
|
|
||||||
const req: BinaryNode = {
|
const req: BinaryNode = {
|
||||||
@@ -963,12 +963,12 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decryptMediaRetryData = async (
|
export const decryptMediaRetryData = (
|
||||||
{ ciphertext, iv }: { ciphertext: Uint8Array; iv: Uint8Array },
|
{ ciphertext, iv }: { ciphertext: Uint8Array; iv: Uint8Array },
|
||||||
mediaKey: Uint8Array,
|
mediaKey: Uint8Array,
|
||||||
msgId: string
|
msgId: string
|
||||||
) => {
|
) => {
|
||||||
const retryKey = await getMediaRetryKey(mediaKey)
|
const retryKey = getMediaRetryKey(mediaKey)
|
||||||
const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId))
|
const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId))
|
||||||
return proto.MediaRetryNotification.decode(plaintext)
|
return proto.MediaRetryNotification.decode(plaintext)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -24,8 +24,8 @@ class TransportState {
|
|||||||
private readonly iv = new Uint8Array(IV_LENGTH)
|
private readonly iv = new Uint8Array(IV_LENGTH)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly encKey: Buffer,
|
private readonly encKey: Uint8Array,
|
||||||
private readonly decKey: Buffer
|
private readonly decKey: Uint8Array
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
encrypt(plaintext: Uint8Array): Uint8Array {
|
encrypt(plaintext: Uint8Array): Uint8Array {
|
||||||
@@ -64,9 +64,9 @@ export const makeNoiseHandler = ({
|
|||||||
|
|
||||||
const data = Buffer.from(NOISE_MODE)
|
const data = Buffer.from(NOISE_MODE)
|
||||||
let hash = data.byteLength === 32 ? data : sha256(data)
|
let hash = data.byteLength === 32 ? data : sha256(data)
|
||||||
let salt: Buffer = hash
|
let salt: Uint8Array = hash
|
||||||
let encKey: Buffer = hash
|
let encKey: Uint8Array = hash
|
||||||
let decKey: Buffer = hash
|
let decKey: Uint8Array = hash
|
||||||
let counter = 0
|
let counter = 0
|
||||||
let sentIntro = false
|
let sentIntro = false
|
||||||
|
|
||||||
@@ -116,13 +116,13 @@ export const makeNoiseHandler = ({
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const localHKDF = async (data: Uint8Array): Promise<[Buffer, Buffer]> => {
|
const localHKDF = (data: Uint8Array): [Uint8Array, Uint8Array] => {
|
||||||
const key = await hkdf(Buffer.from(data), 64, { salt, info: '' })
|
const key = hkdf(Buffer.from(data), 64, { salt, info: '' })
|
||||||
return [key.subarray(0, 32), key.subarray(32)]
|
return [key.subarray(0, 32), key.subarray(32)]
|
||||||
}
|
}
|
||||||
|
|
||||||
const mixIntoKey = async (data: Uint8Array) => {
|
const mixIntoKey = (data: Uint8Array) => {
|
||||||
const [write, read] = await localHKDF(data)
|
const [write, read] = localHKDF(data)
|
||||||
salt = write
|
salt = write
|
||||||
encKey = read
|
encKey = read
|
||||||
decKey = read
|
decKey = read
|
||||||
@@ -131,7 +131,7 @@ export const makeNoiseHandler = ({
|
|||||||
|
|
||||||
const finishInit = async () => {
|
const finishInit = async () => {
|
||||||
isWaitingForTransport = true
|
isWaitingForTransport = true
|
||||||
const [write, read] = await localHKDF(new Uint8Array(0))
|
const [write, read] = localHKDF(new Uint8Array(0))
|
||||||
transport = new TransportState(write, read)
|
transport = new TransportState(write, read)
|
||||||
isWaitingForTransport = false
|
isWaitingForTransport = false
|
||||||
|
|
||||||
@@ -179,12 +179,12 @@ export const makeNoiseHandler = ({
|
|||||||
authenticate,
|
authenticate,
|
||||||
mixIntoKey,
|
mixIntoKey,
|
||||||
finishInit,
|
finishInit,
|
||||||
processHandshake: async ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
|
processHandshake: ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
|
||||||
authenticate(serverHello!.ephemeral!)
|
authenticate(serverHello!.ephemeral!)
|
||||||
await mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!))
|
mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!))
|
||||||
|
|
||||||
const decStaticContent = decrypt(serverHello!.static!)
|
const decStaticContent = decrypt(serverHello!.static!)
|
||||||
await mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
|
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
|
||||||
|
|
||||||
const certDecoded = decrypt(serverHello!.payload!)
|
const certDecoded = decrypt(serverHello!.payload!)
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ export const makeNoiseHandler = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const keyEnc = encrypt(noiseKey.public)
|
const keyEnc = encrypt(noiseKey.public)
|
||||||
await mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!))
|
mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!))
|
||||||
|
|
||||||
return keyEnc
|
return keyEnc
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export const shouldIncludeReportingToken = (message: proto.IMessage): boolean =>
|
|||||||
!message.encEventResponseMessage &&
|
!message.encEventResponseMessage &&
|
||||||
!message.pollUpdateMessage
|
!message.pollUpdateMessage
|
||||||
|
|
||||||
const generateMsgSecretKey = async (
|
const generateMsgSecretKey = (
|
||||||
modificationType: string,
|
modificationType: string,
|
||||||
origMsgId: string,
|
origMsgId: string,
|
||||||
origMsgSender: string,
|
origMsgSender: string,
|
||||||
@@ -321,7 +321,7 @@ export const getMessageReportingToken = async (
|
|||||||
const from = key.fromMe ? key.remoteJid! : key.participant || key.remoteJid!
|
const from = key.fromMe ? key.remoteJid! : key.participant || key.remoteJid!
|
||||||
const to = key.fromMe ? key.participant || key.remoteJid! : 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)
|
const content = extractReportingTokenContent(msgProtobuf, compiledReportingFields)
|
||||||
if (!content || content.length === 0) {
|
if (!content || content.length === 0) {
|
||||||
|
|||||||
@@ -3025,6 +3025,7 @@ __metadata:
|
|||||||
typedoc: "npm:^0.27.9"
|
typedoc: "npm:^0.27.9"
|
||||||
typedoc-plugin-markdown: "npm:4.4.2"
|
typedoc-plugin-markdown: "npm:4.4.2"
|
||||||
typescript: "npm:^5.8.2"
|
typescript: "npm:^5.8.2"
|
||||||
|
whatsapp-rust-bridge: "npm:0.5.2"
|
||||||
ws: "npm:^8.13.0"
|
ws: "npm:^8.13.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
audio-decode: ^2.1.3
|
audio-decode: ^2.1.3
|
||||||
@@ -10184,6 +10185,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"whatsapp-rust-bridge@npm:0.5.2":
|
||||||
|
version: 0.5.2
|
||||||
|
resolution: "whatsapp-rust-bridge@npm:0.5.2"
|
||||||
|
checksum: 10c0/022bff4659398144afe6834c279a9fc617a7cbc9177212874150ff2b39019044d2833af85d8ad8d5a40887ad84e7584edead15333e3acce58f989ef7cfd1e6f9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"whatwg-url@npm:^5.0.0":
|
"whatwg-url@npm:^5.0.0":
|
||||||
version: 5.0.0
|
version: 5.0.0
|
||||||
resolution: "whatwg-url@npm:5.0.0"
|
resolution: "whatwg-url@npm:5.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user