Skip to content

Commit 8083754

Browse files
jlucaso1purpshell
andauthored
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 <[email protected]>
1 parent e6f98c3 commit 8083754

11 files changed

+78
-48
lines changed

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
"audio-decode": "^2.1.3",
5050
"axios": "^1.6.0",
5151
"cache-manager": "^5.7.6",
52-
"futoin-hkdf": "^1.5.1",
5352
"libphonenumber-js": "^1.10.20",
5453
"libsignal": "github:WhiskeySockets/libsignal-node",
5554
"lodash": "^4.17.21",

src/Socket/messages-recv.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
476476
const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey)
477477
const random = randomBytes(32)
478478
const linkCodeSalt = randomBytes(32)
479-
const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
479+
const linkCodePairingExpanded = await hkdf(companionSharedKey, 32, {
480480
salt: linkCodeSalt,
481481
info: 'link_code_pairing_key_bundle_encryption_key'
482482
})
@@ -486,7 +486,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
486486
const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
487487
const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey)
488488
const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random])
489-
authState.creds.advSecretKey = hkdf(identityPayload, 32, { info: 'adv_secret' }).toString('base64')
489+
authState.creds.advSecretKey = (await hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64')
490490
await query({
491491
tag: 'iq',
492492
attrs: {

src/Socket/messages-send.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -654,20 +654,20 @@ export const makeMessagesSocket = (config: SocketConfig) => {
654654
const content = assertMediaContent(message.message)
655655
const mediaKey = content.mediaKey!
656656
const meId = authState.creds.me!.id
657-
const node = encryptMediaRetryRequest(message.key, mediaKey, meId)
657+
const node = await encryptMediaRetryRequest(message.key, mediaKey, meId)
658658

659659
let error: Error | undefined = undefined
660660
await Promise.all(
661661
[
662662
sendNode(node),
663-
waitForMsgMediaUpdate(update => {
663+
waitForMsgMediaUpdate(async(update) => {
664664
const result = update.find(c => c.key.id === message.key.id)
665665
if(result) {
666666
if(result.error) {
667667
error = result.error
668668
} else {
669669
try {
670-
const media = decryptMediaRetryData(result.media!, mediaKey, result.key.id!)
670+
const media = await decryptMediaRetryData(result.media!, mediaKey, result.key.id!)
671671
if(media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) {
672672
const resultStr = proto.MediaRetryNotification.ResultType[media.result]
673673
throw new Boom(

src/Socket/socket.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export const makeSocket = (config: SocketConfig) => {
238238

239239
logger.trace({ handshake }, 'handshake recv from WA')
240240

241-
const keyEnc = noise.processHandshake(handshake, creds.noiseKey)
241+
const keyEnc = await noise.processHandshake(handshake, creds.noiseKey)
242242

243243
let node: proto.IClientPayload
244244
if(!creds.me) {

src/Utils/chat-utils.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ type FetchAppStateSyncKey = (keyId: string) => Promise<proto.Message.IAppStateSy
1414

1515
export type ChatMutationMap = { [index: string]: ChatMutation }
1616

17-
const mutationKeys = (keydata: Uint8Array) => {
18-
const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
17+
const mutationKeys = async(keydata: Uint8Array) => {
18+
const expanded = await hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
1919
return {
2020
indexKey: expanded.slice(0, 32),
2121
valueEncryptionKey: expanded.slice(32, 64),
@@ -144,7 +144,7 @@ export const encodeSyncdPatch = async(
144144
})
145145
const encoded = proto.SyncActionData.encode(dataProto).finish()
146146

147-
const keyValue = mutationKeys(key.keyData!)
147+
const keyValue = await mutationKeys(key.keyData!)
148148

149149
const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
150150
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
@@ -261,7 +261,7 @@ export const decodeSyncdPatch = async(
261261
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } })
262262
}
263263

264-
const mainKey = mutationKeys(mainKeyObj.keyData!)
264+
const mainKey = await mutationKeys(mainKeyObj.keyData!)
265265
const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32))
266266

267267
const patchMac = generatePatchMac(msg.snapshotMac!, mutationmacs, toNumber(msg.version!.version), name, mainKey.patchMacKey)
@@ -390,7 +390,7 @@ export const decodeSyncdSnapshot = async(
390390
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
391391
}
392392

393-
const result = mutationKeys(keyEnc.keyData!)
393+
const result = await mutationKeys(keyEnc.keyData!)
394394
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
395395
if(Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
396396
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`)
@@ -458,7 +458,7 @@ export const decodePatches = async(
458458
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
459459
}
460460

461-
const result = mutationKeys(keyEnc.keyData!)
461+
const result = await mutationKeys(keyEnc.keyData!)
462462
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
463463
if(Buffer.compare(snapshotMac!, computedSnapshotMac) !== 0) {
464464
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`)

src/Utils/crypto.ts

+39-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto'
2-
import HKDF from 'futoin-hkdf'
32
import * as libsignal from 'libsignal'
43
import { KEY_BUNDLE_TYPE } from '../Defaults'
54
import { KeyPair } from '../Types'
@@ -122,10 +121,47 @@ export function md5(buffer: Buffer) {
122121
}
123122

124123
// HKDF key expansion
125-
export function hkdf(buffer: Uint8Array | Buffer, expandedLength: number, info: { salt?: Buffer, info?: string }) {
126-
return HKDF(!Buffer.isBuffer(buffer) ? Buffer.from(buffer) : buffer, expandedLength, info)
124+
export async function hkdf(
125+
buffer: Uint8Array | Buffer,
126+
expandedLength: number,
127+
info: { salt?: Buffer, info?: string }
128+
): Promise<Buffer> {
129+
// Ensure we have a Uint8Array for the key material
130+
const inputKeyMaterial = buffer instanceof Uint8Array
131+
? buffer
132+
: new Uint8Array(buffer)
133+
134+
// Set default values if not provided
135+
const salt = info.salt ? new Uint8Array(info.salt) : new Uint8Array(0)
136+
const infoBytes = info.info
137+
? new TextEncoder().encode(info.info)
138+
: new Uint8Array(0)
139+
140+
// Import the input key material
141+
const importedKey = await crypto.subtle.importKey(
142+
'raw',
143+
inputKeyMaterial,
144+
{ name: 'HKDF' },
145+
false,
146+
['deriveBits']
147+
)
148+
149+
// Derive bits using HKDF
150+
const derivedBits = await crypto.subtle.deriveBits(
151+
{
152+
name: 'HKDF',
153+
hash: 'SHA-256',
154+
salt: salt,
155+
info: infoBytes
156+
},
157+
importedKey,
158+
expandedLength * 8 // Convert bytes to bits
159+
)
160+
161+
return Buffer.from(derivedBits)
127162
}
128163

164+
129165
export async function derivePairingCodeKey(pairingCode: string, salt: Buffer): Promise<Buffer> {
130166
// Convert inputs to formats Web Crypto API can work with
131167
const encoder = new TextEncoder()

src/Utils/generics.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ export const generateMessageIDV2 = (userId?: string): string => {
206206
export const generateMessageID = () => '3EB0' + randomBytes(18).toString('hex').toUpperCase()
207207

208208
export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEventEmitter, event: T) {
209-
return async(check: (u: BaileysEventMap[T]) => boolean | undefined, timeoutMs?: number) => {
209+
return async(check: (u: BaileysEventMap[T]) => Promise<boolean | undefined>, timeoutMs?: number) => {
210210
let listener: (item: BaileysEventMap[T]) => void
211211
let closeListener: (state: Partial<ConnectionState>) => void
212212
await (
@@ -223,8 +223,8 @@ export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEve
223223
}
224224

225225
ev.on('connection.update', closeListener)
226-
listener = (update) => {
227-
if(check(update)) {
226+
listener = async(update) => {
227+
if(await check(update)) {
228228
resolve()
229229
}
230230
}

src/Utils/lt-hash.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ class d {
3535
var n = this
3636
return n.add(n.subtract(e, r), t)
3737
}
38-
_addSingle(e, t) {
38+
async _addSingle(e, t) {
3939
var r = this
40-
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer
40+
const n = new Uint8Array(await hkdf(Buffer.from(t), o, { info: r.salt })).buffer
4141
return r.performPointwiseWithOverflow(e, n, ((e, t) => e + t))
4242
}
43-
_subtractSingle(e, t) {
43+
async _subtractSingle(e, t) {
4444
var r = this
4545

46-
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer
46+
const n = new Uint8Array(await hkdf(Buffer.from(t), o, { info: r.salt })).buffer
4747
return r.performPointwiseWithOverflow(e, n, ((e, t) => e - t))
4848
}
4949
performPointwiseWithOverflow(e, t, r) {

src/Utils/messages-media.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const hkdfInfoKey = (type: MediaType) => {
5555
}
5656

5757
/** generates all the keys required to encrypt/decrypt & sign a media message */
58-
export function getMediaKeys(buffer: Uint8Array | string | null | undefined, mediaType: MediaType): MediaDecryptionKeyInfo {
58+
export async function getMediaKeys(buffer: Uint8Array | string | null | undefined, mediaType: MediaType): Promise<MediaDecryptionKeyInfo> {
5959
if(!buffer) {
6060
throw new Boom('Cannot derive from empty media key')
6161
}
@@ -65,7 +65,7 @@ export function getMediaKeys(buffer: Uint8Array | string | null | undefined, med
6565
}
6666

6767
// expand using HKDF to 112 bytes, also pass in the relevant app info
68-
const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
68+
const expandedMediaKey = await hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
6969
return {
7070
iv: expandedMediaKey.slice(0, 16),
7171
cipherKey: expandedMediaKey.slice(16, 48),
@@ -344,7 +344,7 @@ export const encryptedStream = async(
344344
logger?.debug('fetched media stream')
345345

346346
const mediaKey = Crypto.randomBytes(32)
347-
const { cipherKey, iv, macKey } = getMediaKeys(mediaKey, mediaType)
347+
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType)
348348
const encWriteStream = new Readable({ read: () => {} })
349349

350350
let bodyPath: string | undefined
@@ -458,13 +458,13 @@ export type MediaDownloadOptions = {
458458

459459
export const getUrlFromDirectPath = (directPath: string) => `https://${DEF_HOST}${directPath}`
460460

461-
export const downloadContentFromMessage = (
461+
export const downloadContentFromMessage = async(
462462
{ mediaKey, directPath, url }: DownloadableMessage,
463463
type: MediaType,
464464
opts: MediaDownloadOptions = { }
465465
) => {
466466
const downloadUrl = url || getUrlFromDirectPath(directPath!)
467-
const keys = getMediaKeys(mediaKey, type)
467+
const keys = await getMediaKeys(mediaKey, type)
468468

469469
return downloadEncryptedContent(downloadUrl, keys, opts)
470470
}
@@ -673,7 +673,7 @@ const getMediaRetryKey = (mediaKey: Buffer | Uint8Array) => {
673673
/**
674674
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
675675
*/
676-
export const encryptMediaRetryRequest = (
676+
export const encryptMediaRetryRequest = async(
677677
key: proto.IMessageKey,
678678
mediaKey: Buffer | Uint8Array,
679679
meId: string
@@ -682,7 +682,7 @@ export const encryptMediaRetryRequest = (
682682
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
683683

684684
const iv = Crypto.randomBytes(12)
685-
const retryKey = getMediaRetryKey(mediaKey)
685+
const retryKey = await getMediaRetryKey(mediaKey)
686686
const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id!))
687687

688688
const req: BinaryNode = {
@@ -752,12 +752,12 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
752752
return event
753753
}
754754

755-
export const decryptMediaRetryData = (
755+
export const decryptMediaRetryData = async(
756756
{ ciphertext, iv }: { ciphertext: Uint8Array, iv: Uint8Array },
757757
mediaKey: Uint8Array,
758758
msgId: string
759759
) => {
760-
const retryKey = getMediaRetryKey(mediaKey)
760+
const retryKey = await getMediaRetryKey(mediaKey)
761761
const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId))
762762
return proto.MediaRetryNotification.decode(plaintext)
763763
}

src/Utils/noise-handler.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,22 @@ export const makeNoiseHandler = ({
5757
return result
5858
}
5959

60-
const localHKDF = (data: Uint8Array) => {
61-
const key = hkdf(Buffer.from(data), 64, { salt, info: '' })
60+
const localHKDF = async(data: Uint8Array) => {
61+
const key = await hkdf(Buffer.from(data), 64, { salt, info: '' })
6262
return [key.slice(0, 32), key.slice(32)]
6363
}
6464

65-
const mixIntoKey = (data: Uint8Array) => {
66-
const [write, read] = localHKDF(data)
65+
const mixIntoKey = async(data: Uint8Array) => {
66+
const [write, read] = await localHKDF(data)
6767
salt = write
6868
encKey = read
6969
decKey = read
7070
readCounter = 0
7171
writeCounter = 0
7272
}
7373

74-
const finishInit = () => {
75-
const [write, read] = localHKDF(new Uint8Array(0))
74+
const finishInit = async() => {
75+
const [write, read] = await localHKDF(new Uint8Array(0))
7676
encKey = write
7777
decKey = read
7878
hash = Buffer.from([])
@@ -82,7 +82,7 @@ export const makeNoiseHandler = ({
8282
}
8383

8484
const data = Buffer.from(NOISE_MODE)
85-
let hash = Buffer.from(data.byteLength === 32 ? data : sha256(data))
85+
let hash = data.byteLength === 32 ? data : sha256(data)
8686
let salt = hash
8787
let encKey = hash
8888
let decKey = hash
@@ -102,12 +102,12 @@ export const makeNoiseHandler = ({
102102
authenticate,
103103
mixIntoKey,
104104
finishInit,
105-
processHandshake: ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
105+
processHandshake: async({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
106106
authenticate(serverHello!.ephemeral!)
107-
mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!))
107+
await mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!))
108108

109109
const decStaticContent = decrypt(serverHello!.static!)
110-
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
110+
await mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
111111

112112
const certDecoded = decrypt(serverHello!.payload!)
113113

@@ -120,7 +120,7 @@ export const makeNoiseHandler = ({
120120
}
121121

122122
const keyEnc = encrypt(noiseKey.public)
123-
mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!))
123+
await mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!))
124124

125125
return keyEnc
126126
},

yarn.lock

-5
Original file line numberDiff line numberDiff line change
@@ -3461,11 +3461,6 @@ functions-have-names@^1.2.3:
34613461
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
34623462
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
34633463

3464-
futoin-hkdf@^1.5.1:
3465-
version "1.5.3"
3466-
resolved "https://registry.yarnpkg.com/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz#6c8024f2e1429da086d4e18289ef2239ad33ee35"
3467-
integrity sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==
3468-
34693464
gensync@^1.0.0-beta.2:
34703465
version "1.0.0-beta.2"
34713466
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"

0 commit comments

Comments
 (0)