diff --git a/package.json b/package.json index 8aec850..3d31ac5 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "audio-decode": "^2.1.3", "axios": "^1.6.0", "cache-manager": "^5.7.6", - "futoin-hkdf": "^1.5.1", "libphonenumber-js": "^1.10.20", "libsignal": "github:WhiskeySockets/libsignal-node", "lodash": "^4.17.21", diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 7a348d4..3b8dd64 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -476,7 +476,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey) const random = randomBytes(32) const linkCodeSalt = randomBytes(32) - const linkCodePairingExpanded = hkdf(companionSharedKey, 32, { + const linkCodePairingExpanded = await hkdf(companionSharedKey, 32, { salt: linkCodeSalt, 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 identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey) 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({ tag: 'iq', attrs: { diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 0c73add..a5f55a9 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -654,20 +654,20 @@ export const makeMessagesSocket = (config: SocketConfig) => { const content = assertMediaContent(message.message) const mediaKey = content.mediaKey! 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 await Promise.all( [ sendNode(node), - waitForMsgMediaUpdate(update => { + waitForMsgMediaUpdate(async(update) => { const result = update.find(c => c.key.id === message.key.id) if(result) { if(result.error) { error = result.error } else { 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) { const resultStr = proto.MediaRetryNotification.ResultType[media.result] throw new Boom( diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index c3042ec..dc5f1c0 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -238,7 +238,7 @@ export const makeSocket = (config: SocketConfig) => { 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 if(!creds.me) { diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index dd89bff..5d3fd2f 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -14,8 +14,8 @@ type FetchAppStateSyncKey = (keyId: string) => Promise { - const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' }) +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), @@ -144,7 +144,7 @@ export const encodeSyncdPatch = async( }) 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 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 } }) } - const mainKey = mutationKeys(mainKeyObj.keyData!) + const mainKey = await mutationKeys(mainKeyObj.keyData!) 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) @@ -390,7 +390,7 @@ export const decodeSyncdSnapshot = async( 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) if(Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) { 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`) } - const result = mutationKeys(keyEnc.keyData!) + const result = await 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}`) diff --git a/src/Utils/crypto.ts b/src/Utils/crypto.ts index 69f25b3..3856013 100644 --- a/src/Utils/crypto.ts +++ b/src/Utils/crypto.ts @@ -1,5 +1,4 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto' -import HKDF from 'futoin-hkdf' import * as libsignal from 'libsignal' import { KEY_BUNDLE_TYPE } from '../Defaults' import { KeyPair } from '../Types' @@ -122,10 +121,47 @@ export function md5(buffer: Buffer) { } // HKDF key expansion -export function hkdf(buffer: Uint8Array | Buffer, expandedLength: number, info: { salt?: Buffer, info?: string }) { - return HKDF(!Buffer.isBuffer(buffer) ? Buffer.from(buffer) : buffer, expandedLength, info) +export async function hkdf( + buffer: Uint8Array | Buffer, + expandedLength: number, + info: { salt?: Buffer, info?: string } +): Promise { + // 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 { // Convert inputs to formats Web Crypto API can work with const encoder = new TextEncoder() diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index 6e2288d..178da67 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -206,7 +206,7 @@ export const generateMessageIDV2 = (userId?: string): string => { export const generateMessageID = () => '3EB0' + randomBytes(18).toString('hex').toUpperCase() export function bindWaitForEvent(ev: BaileysEventEmitter, event: T) { - return async(check: (u: BaileysEventMap[T]) => boolean | undefined, timeoutMs?: number) => { + return async(check: (u: BaileysEventMap[T]) => Promise, timeoutMs?: number) => { let listener: (item: BaileysEventMap[T]) => void let closeListener: (state: Partial) => void await ( @@ -223,8 +223,8 @@ export function bindWaitForEvent(ev: BaileysEve } ev.on('connection.update', closeListener) - listener = (update) => { - if(check(update)) { + listener = async(update) => { + if(await check(update)) { resolve() } } diff --git a/src/Utils/lt-hash.ts b/src/Utils/lt-hash.ts index fa38484..1842a31 100644 --- a/src/Utils/lt-hash.ts +++ b/src/Utils/lt-hash.ts @@ -35,15 +35,15 @@ class d { var n = this return n.add(n.subtract(e, r), t) } - _addSingle(e, t) { + async _addSingle(e, t) { 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)) } - _subtractSingle(e, t) { + async _subtractSingle(e, t) { 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)) } performPointwiseWithOverflow(e, t, r) { diff --git a/src/Utils/messages-media.ts b/src/Utils/messages-media.ts index b2b1133..eb811df 100644 --- a/src/Utils/messages-media.ts +++ b/src/Utils/messages-media.ts @@ -55,7 +55,7 @@ export const hkdfInfoKey = (type: MediaType) => { } /** 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 { if(!buffer) { 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 - const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) }) + const expandedMediaKey = await hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) }) return { iv: expandedMediaKey.slice(0, 16), cipherKey: expandedMediaKey.slice(16, 48), @@ -344,7 +344,7 @@ export const encryptedStream = async( logger?.debug('fetched media stream') 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: () => {} }) let bodyPath: string | undefined @@ -458,13 +458,13 @@ export type MediaDownloadOptions = { export const getUrlFromDirectPath = (directPath: string) => `https://${DEF_HOST}${directPath}` -export const downloadContentFromMessage = ( +export const downloadContentFromMessage = async( { mediaKey, directPath, url }: DownloadableMessage, type: MediaType, opts: MediaDownloadOptions = { } ) => { const downloadUrl = url || getUrlFromDirectPath(directPath!) - const keys = getMediaKeys(mediaKey, type) + const keys = await getMediaKeys(mediaKey, type) 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 */ -export const encryptMediaRetryRequest = ( +export const encryptMediaRetryRequest = async( key: proto.IMessageKey, mediaKey: Buffer | Uint8Array, meId: string @@ -682,7 +682,7 @@ export const encryptMediaRetryRequest = ( const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish() 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 req: BinaryNode = { @@ -752,12 +752,12 @@ export const decodeMediaRetryNode = (node: BinaryNode) => { return event } -export const decryptMediaRetryData = ( +export const decryptMediaRetryData = async( { ciphertext, iv }: { ciphertext: Uint8Array, iv: Uint8Array }, mediaKey: Uint8Array, msgId: string ) => { - const retryKey = getMediaRetryKey(mediaKey) + const retryKey = await getMediaRetryKey(mediaKey) const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId)) return proto.MediaRetryNotification.decode(plaintext) } diff --git a/src/Utils/noise-handler.ts b/src/Utils/noise-handler.ts index 15096bb..7d4d873 100644 --- a/src/Utils/noise-handler.ts +++ b/src/Utils/noise-handler.ts @@ -57,13 +57,13 @@ export const makeNoiseHandler = ({ return result } - const localHKDF = (data: Uint8Array) => { - const key = hkdf(Buffer.from(data), 64, { salt, info: '' }) + const localHKDF = async(data: Uint8Array) => { + const key = await hkdf(Buffer.from(data), 64, { salt, info: '' }) return [key.slice(0, 32), key.slice(32)] } - const mixIntoKey = (data: Uint8Array) => { - const [write, read] = localHKDF(data) + const mixIntoKey = async(data: Uint8Array) => { + const [write, read] = await localHKDF(data) salt = write encKey = read decKey = read @@ -71,8 +71,8 @@ export const makeNoiseHandler = ({ writeCounter = 0 } - const finishInit = () => { - const [write, read] = localHKDF(new Uint8Array(0)) + const finishInit = async() => { + const [write, read] = await localHKDF(new Uint8Array(0)) encKey = write decKey = read hash = Buffer.from([]) @@ -82,7 +82,7 @@ export const makeNoiseHandler = ({ } 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 encKey = hash let decKey = hash @@ -102,12 +102,12 @@ export const makeNoiseHandler = ({ authenticate, mixIntoKey, finishInit, - processHandshake: ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => { + processHandshake: async({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => { authenticate(serverHello!.ephemeral!) - mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!)) + await mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!)) const decStaticContent = decrypt(serverHello!.static!) - mixIntoKey(Curve.sharedKey(privateKey, decStaticContent)) + await mixIntoKey(Curve.sharedKey(privateKey, decStaticContent)) const certDecoded = decrypt(serverHello!.payload!) @@ -120,7 +120,7 @@ export const makeNoiseHandler = ({ } const keyEnc = encrypt(noiseKey.public) - mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!)) + await mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!)) return keyEnc }, diff --git a/yarn.lock b/yarn.lock index 0a51492..03165ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" 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: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"