refactor: turn hkdf functions to async and remove extra deps (#1272)

* refactor: remove futoin-hkdf dependency and update hkdf implementation

* refactor: use crypto subtle and update functions to async

---------

Co-authored-by: Rajeh Taher <rajeh@reforward.dev>
This commit is contained in:
João Lucas de Oliveira Lopes
2025-03-01 13:31:48 -03:00
committed by GitHub
parent e6f98c3902
commit 8083754621
11 changed files with 78 additions and 48 deletions

View File

@@ -49,7 +49,6 @@
"audio-decode": "^2.1.3", "audio-decode": "^2.1.3",
"axios": "^1.6.0", "axios": "^1.6.0",
"cache-manager": "^5.7.6", "cache-manager": "^5.7.6",
"futoin-hkdf": "^1.5.1",
"libphonenumber-js": "^1.10.20", "libphonenumber-js": "^1.10.20",
"libsignal": "github:WhiskeySockets/libsignal-node", "libsignal": "github:WhiskeySockets/libsignal-node",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View File

@@ -476,7 +476,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey) const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey)
const random = randomBytes(32) const random = randomBytes(32)
const linkCodeSalt = randomBytes(32) const linkCodeSalt = randomBytes(32)
const linkCodePairingExpanded = hkdf(companionSharedKey, 32, { const linkCodePairingExpanded = await hkdf(companionSharedKey, 32, {
salt: linkCodeSalt, salt: linkCodeSalt,
info: 'link_code_pairing_key_bundle_encryption_key' info: 'link_code_pairing_key_bundle_encryption_key'
}) })
@@ -486,7 +486,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 = hkdf(identityPayload, 32, { info: 'adv_secret' }).toString('base64') authState.creds.advSecretKey = (await hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64')
await query({ await query({
tag: 'iq', tag: 'iq',
attrs: { attrs: {

View File

@@ -654,20 +654,20 @@ 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 = encryptMediaRetryRequest(message.key, mediaKey, meId) const node = await encryptMediaRetryRequest(message.key, mediaKey, meId)
let error: Error | undefined = undefined let error: Error | undefined = undefined
await Promise.all( await Promise.all(
[ [
sendNode(node), sendNode(node),
waitForMsgMediaUpdate(update => { waitForMsgMediaUpdate(async(update) => {
const result = update.find(c => c.key.id === message.key.id) const result = update.find(c => c.key.id === message.key.id)
if(result) { if(result) {
if(result.error) { if(result.error) {
error = result.error error = result.error
} else { } else {
try { try {
const media = decryptMediaRetryData(result.media!, mediaKey, result.key.id!) const media = await 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( throw new Boom(

View File

@@ -238,7 +238,7 @@ export const makeSocket = (config: SocketConfig) => {
logger.trace({ handshake }, 'handshake recv from WA') logger.trace({ handshake }, 'handshake recv from WA')
const keyEnc = noise.processHandshake(handshake, creds.noiseKey) const keyEnc = await noise.processHandshake(handshake, creds.noiseKey)
let node: proto.IClientPayload let node: proto.IClientPayload
if(!creds.me) { if(!creds.me) {

View File

@@ -14,8 +14,8 @@ type FetchAppStateSyncKey = (keyId: string) => Promise<proto.Message.IAppStateSy
export type ChatMutationMap = { [index: string]: ChatMutation } export type ChatMutationMap = { [index: string]: ChatMutation }
const mutationKeys = (keydata: Uint8Array) => { const mutationKeys = async(keydata: Uint8Array) => {
const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' }) const expanded = await hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
return { return {
indexKey: expanded.slice(0, 32), indexKey: expanded.slice(0, 32),
valueEncryptionKey: expanded.slice(32, 64), valueEncryptionKey: expanded.slice(32, 64),
@@ -144,7 +144,7 @@ export const encodeSyncdPatch = async(
}) })
const encoded = proto.SyncActionData.encode(dataProto).finish() const encoded = proto.SyncActionData.encode(dataProto).finish()
const keyValue = mutationKeys(key.keyData!) const keyValue = await 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)
@@ -261,7 +261,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 = mutationKeys(mainKeyObj.keyData!) const mainKey = await 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(msg.snapshotMac!, mutationmacs, toNumber(msg.version!.version), name, mainKey.patchMacKey) const patchMac = generatePatchMac(msg.snapshotMac!, mutationmacs, toNumber(msg.version!.version), name, mainKey.patchMacKey)
@@ -390,7 +390,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 = mutationKeys(keyEnc.keyData!) const result = await 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`)
@@ -458,7 +458,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 = mutationKeys(keyEnc.keyData!) const result = await 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}`)

View File

@@ -1,5 +1,4 @@
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto' import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto'
import HKDF from 'futoin-hkdf'
import * as libsignal from 'libsignal' import * as libsignal from 'libsignal'
import { KEY_BUNDLE_TYPE } from '../Defaults' import { KEY_BUNDLE_TYPE } from '../Defaults'
import { KeyPair } from '../Types' import { KeyPair } from '../Types'
@@ -122,10 +121,47 @@ export function md5(buffer: Buffer) {
} }
// HKDF key expansion // HKDF key expansion
export function hkdf(buffer: Uint8Array | Buffer, expandedLength: number, info: { salt?: Buffer, info?: string }) { export async function hkdf(
return HKDF(!Buffer.isBuffer(buffer) ? Buffer.from(buffer) : buffer, expandedLength, info) buffer: Uint8Array | Buffer,
expandedLength: number,
info: { salt?: Buffer, info?: string }
): Promise<Buffer> {
// Ensure we have a Uint8Array for the key material
const inputKeyMaterial = 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
const importedKey = await crypto.subtle.importKey(
'raw',
inputKeyMaterial,
{ name: 'HKDF' },
false,
['deriveBits']
)
// Derive bits using HKDF
const derivedBits = await crypto.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()

View File

@@ -206,7 +206,7 @@ export const generateMessageIDV2 = (userId?: string): string => {
export const generateMessageID = () => '3EB0' + randomBytes(18).toString('hex').toUpperCase() export const generateMessageID = () => '3EB0' + randomBytes(18).toString('hex').toUpperCase()
export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEventEmitter, event: T) { export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEventEmitter, event: T) {
return async(check: (u: BaileysEventMap[T]) => boolean | undefined, timeoutMs?: number) => { return async(check: (u: BaileysEventMap[T]) => Promise<boolean | undefined>, timeoutMs?: number) => {
let listener: (item: BaileysEventMap[T]) => void let listener: (item: BaileysEventMap[T]) => void
let closeListener: (state: Partial<ConnectionState>) => void let closeListener: (state: Partial<ConnectionState>) => void
await ( await (
@@ -223,8 +223,8 @@ export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEve
} }
ev.on('connection.update', closeListener) ev.on('connection.update', closeListener)
listener = (update) => { listener = async(update) => {
if(check(update)) { if(await check(update)) {
resolve() resolve()
} }
} }

View File

@@ -35,15 +35,15 @@ class d {
var n = this var n = this
return n.add(n.subtract(e, r), t) return n.add(n.subtract(e, r), t)
} }
_addSingle(e, t) { async _addSingle(e, t) {
var r = this var r = this
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer const n = new Uint8Array(await hkdf(Buffer.from(t), o, { info: r.salt })).buffer
return r.performPointwiseWithOverflow(e, n, ((e, t) => e + t)) return r.performPointwiseWithOverflow(e, n, ((e, t) => e + t))
} }
_subtractSingle(e, t) { async _subtractSingle(e, t) {
var r = this var r = this
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer const n = new Uint8Array(await hkdf(Buffer.from(t), o, { info: r.salt })).buffer
return r.performPointwiseWithOverflow(e, n, ((e, t) => e - t)) return r.performPointwiseWithOverflow(e, n, ((e, t) => e - t))
} }
performPointwiseWithOverflow(e, t, r) { performPointwiseWithOverflow(e, t, r) {

View File

@@ -55,7 +55,7 @@ export const hkdfInfoKey = (type: MediaType) => {
} }
/** generates all the keys required to encrypt/decrypt & sign a media message */ /** generates all the keys required to encrypt/decrypt & sign a media message */
export function getMediaKeys(buffer: Uint8Array | string | null | undefined, mediaType: MediaType): MediaDecryptionKeyInfo { export async function getMediaKeys(buffer: Uint8Array | string | null | undefined, mediaType: MediaType): Promise<MediaDecryptionKeyInfo> {
if(!buffer) { if(!buffer) {
throw new Boom('Cannot derive from empty media key') throw new Boom('Cannot derive from empty media key')
} }
@@ -65,7 +65,7 @@ export function getMediaKeys(buffer: Uint8Array | string | null | undefined, med
} }
// 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 = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) }) const expandedMediaKey = await 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),
@@ -344,7 +344,7 @@ export const encryptedStream = async(
logger?.debug('fetched media stream') logger?.debug('fetched media stream')
const mediaKey = Crypto.randomBytes(32) const mediaKey = Crypto.randomBytes(32)
const { cipherKey, iv, macKey } = getMediaKeys(mediaKey, mediaType) const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType)
const encWriteStream = new Readable({ read: () => {} }) const encWriteStream = new Readable({ read: () => {} })
let bodyPath: string | undefined let bodyPath: string | undefined
@@ -458,13 +458,13 @@ export type MediaDownloadOptions = {
export const getUrlFromDirectPath = (directPath: string) => `https://${DEF_HOST}${directPath}` export const getUrlFromDirectPath = (directPath: string) => `https://${DEF_HOST}${directPath}`
export const downloadContentFromMessage = ( export const downloadContentFromMessage = async(
{ mediaKey, directPath, url }: DownloadableMessage, { mediaKey, directPath, url }: DownloadableMessage,
type: MediaType, type: MediaType,
opts: MediaDownloadOptions = { } opts: MediaDownloadOptions = { }
) => { ) => {
const downloadUrl = url || getUrlFromDirectPath(directPath!) const downloadUrl = url || getUrlFromDirectPath(directPath!)
const keys = getMediaKeys(mediaKey, type) const keys = await getMediaKeys(mediaKey, type)
return downloadEncryptedContent(downloadUrl, keys, opts) return downloadEncryptedContent(downloadUrl, keys, opts)
} }
@@ -673,7 +673,7 @@ 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 = ( export const encryptMediaRetryRequest = async(
key: proto.IMessageKey, key: proto.IMessageKey,
mediaKey: Buffer | Uint8Array, mediaKey: Buffer | Uint8Array,
meId: string meId: string
@@ -682,7 +682,7 @@ export const encryptMediaRetryRequest = (
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 = getMediaRetryKey(mediaKey) const retryKey = await 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 = {
@@ -752,12 +752,12 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
return event return event
} }
export const decryptMediaRetryData = ( export const decryptMediaRetryData = async(
{ ciphertext, iv }: { ciphertext: Uint8Array, iv: Uint8Array }, { ciphertext, iv }: { ciphertext: Uint8Array, iv: Uint8Array },
mediaKey: Uint8Array, mediaKey: Uint8Array,
msgId: string msgId: string
) => { ) => {
const retryKey = getMediaRetryKey(mediaKey) const retryKey = await 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)
} }

View File

@@ -57,13 +57,13 @@ export const makeNoiseHandler = ({
return result return result
} }
const localHKDF = (data: Uint8Array) => { const localHKDF = async(data: Uint8Array) => {
const key = hkdf(Buffer.from(data), 64, { salt, info: '' }) const key = await hkdf(Buffer.from(data), 64, { salt, info: '' })
return [key.slice(0, 32), key.slice(32)] return [key.slice(0, 32), key.slice(32)]
} }
const mixIntoKey = (data: Uint8Array) => { const mixIntoKey = async(data: Uint8Array) => {
const [write, read] = localHKDF(data) const [write, read] = await localHKDF(data)
salt = write salt = write
encKey = read encKey = read
decKey = read decKey = read
@@ -71,8 +71,8 @@ export const makeNoiseHandler = ({
writeCounter = 0 writeCounter = 0
} }
const finishInit = () => { const finishInit = async() => {
const [write, read] = localHKDF(new Uint8Array(0)) const [write, read] = await localHKDF(new Uint8Array(0))
encKey = write encKey = write
decKey = read decKey = read
hash = Buffer.from([]) hash = Buffer.from([])
@@ -82,7 +82,7 @@ export const makeNoiseHandler = ({
} }
const data = Buffer.from(NOISE_MODE) const data = Buffer.from(NOISE_MODE)
let hash = Buffer.from(data.byteLength === 32 ? data : sha256(data)) let hash = data.byteLength === 32 ? data : sha256(data)
let salt = hash let salt = hash
let encKey = hash let encKey = hash
let decKey = hash let decKey = hash
@@ -102,12 +102,12 @@ export const makeNoiseHandler = ({
authenticate, authenticate,
mixIntoKey, mixIntoKey,
finishInit, finishInit,
processHandshake: ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => { processHandshake: async({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
authenticate(serverHello!.ephemeral!) authenticate(serverHello!.ephemeral!)
mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!)) await mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!))
const decStaticContent = decrypt(serverHello!.static!) const decStaticContent = decrypt(serverHello!.static!)
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent)) await mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
const certDecoded = decrypt(serverHello!.payload!) const certDecoded = decrypt(serverHello!.payload!)
@@ -120,7 +120,7 @@ export const makeNoiseHandler = ({
} }
const keyEnc = encrypt(noiseKey.public) const keyEnc = encrypt(noiseKey.public)
mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!)) await mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!))
return keyEnc return keyEnc
}, },

View File

@@ -3461,11 +3461,6 @@ functions-have-names@^1.2.3:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
futoin-hkdf@^1.5.1:
version "1.5.3"
resolved "https://registry.yarnpkg.com/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz#6c8024f2e1429da086d4e18289ef2239ad33ee35"
integrity sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==
gensync@^1.0.0-beta.2: gensync@^1.0.0-beta.2:
version "1.0.0-beta.2" version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"