mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
198 lines
6.9 KiB
TypeScript
198 lines
6.9 KiB
TypeScript
import { Boom } from '@hapi/boom'
|
|
import { randomBytes } from 'crypto'
|
|
import { AuthenticationCreds, Contact, CurveKeyPair, DisconnectReason, LegacyAuthenticationCreds, WATag } from '../Types'
|
|
import { decodeBinaryNodeLegacy, jidNormalizedUser } from '../WABinary'
|
|
import { aesDecrypt, Curve, hkdf, hmacSign } from './crypto'
|
|
import { BufferJSON } from './generics'
|
|
|
|
export const newLegacyAuthCreds = () => ({
|
|
clientID: randomBytes(16).toString('base64')
|
|
}) as LegacyAuthenticationCreds
|
|
|
|
export const decodeWAMessage = (
|
|
message: Buffer | string,
|
|
auth: { macKey: Buffer, encKey: Buffer },
|
|
fromMe: boolean = false
|
|
) => {
|
|
let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message
|
|
if(commaIndex < 0) {
|
|
throw new Boom('invalid message', { data: message })
|
|
} // if there was no comma, then this message must be not be valid
|
|
|
|
if(message[commaIndex + 1] === ',') {
|
|
commaIndex += 1
|
|
}
|
|
|
|
let data = message.slice(commaIndex + 1, message.length)
|
|
|
|
// get the message tag.
|
|
// If a query was done, the server will respond with the same message tag we sent the query with
|
|
const messageTag: string = message.slice(0, commaIndex).toString()
|
|
let json: any
|
|
let tags: WATag
|
|
if(data.length) {
|
|
const possiblyEnc = (data.length > 32 && data.length % 16 === 0)
|
|
if(typeof data === 'string' || !possiblyEnc) {
|
|
json = JSON.parse(data.toString()) // parse the JSON
|
|
} else {
|
|
try {
|
|
json = JSON.parse(data.toString())
|
|
} catch{
|
|
const { macKey, encKey } = auth || {}
|
|
if(!macKey || !encKey) {
|
|
throw new Boom('recieved encrypted buffer when auth creds unavailable', { data: message, statusCode: DisconnectReason.badSession })
|
|
}
|
|
|
|
/*
|
|
If the data recieved was not a JSON, then it must be an encrypted message.
|
|
Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys
|
|
*/
|
|
if(fromMe) {
|
|
tags = [data[0], data[1]]
|
|
data = data.slice(2, data.length)
|
|
}
|
|
|
|
const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message
|
|
data = data.slice(32, data.length) // the actual message
|
|
const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey
|
|
|
|
if(checksum.equals(computedChecksum)) {
|
|
// the checksum the server sent, must match the one we computed for the message to be valid
|
|
const decrypted = aesDecrypt(data, encKey) // decrypt using AES
|
|
json = decodeBinaryNodeLegacy(decrypted, { index: 0 }) // decode the binary message into a JSON array
|
|
} else {
|
|
throw new Boom('Bad checksum', {
|
|
data: {
|
|
received: checksum.toString('hex'),
|
|
computed: computedChecksum.toString('hex'),
|
|
data: data.slice(0, 80).toString(),
|
|
tag: messageTag,
|
|
message: message.slice(0, 80).toString()
|
|
},
|
|
statusCode: DisconnectReason.badSession
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return [messageTag, json, tags] as const
|
|
}
|
|
|
|
/**
|
|
* Once the QR code is scanned and we can validate our connection, or we resolved the challenge when logging back in
|
|
* @private
|
|
* @param json
|
|
*/
|
|
export const validateNewConnection = (
|
|
json: { [_: string]: any },
|
|
auth: LegacyAuthenticationCreds,
|
|
curveKeys: CurveKeyPair
|
|
) => {
|
|
// set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone
|
|
const onValidationSuccess = () => {
|
|
const user: Contact = {
|
|
id: jidNormalizedUser(json.wid),
|
|
name: json.pushname
|
|
}
|
|
return { user, auth, phone: json.phone }
|
|
}
|
|
|
|
if(!json.secret) {
|
|
// if we didn't get a secret, we don't need it, we're validated
|
|
if(json.clientToken && json.clientToken !== auth.clientToken) {
|
|
auth = { ...auth, clientToken: json.clientToken }
|
|
}
|
|
|
|
if(json.serverToken && json.serverToken !== auth.serverToken) {
|
|
auth = { ...auth, serverToken: json.serverToken }
|
|
}
|
|
|
|
return onValidationSuccess()
|
|
}
|
|
|
|
const secret = Buffer.from(json.secret, 'base64')
|
|
if(secret.length !== 144) {
|
|
throw new Error ('incorrect secret length received: ' + secret.length)
|
|
}
|
|
|
|
// generate shared key from our private key & the secret shared by the server
|
|
const sharedKey = Curve.sharedKey(curveKeys.private, secret.slice(0, 32))
|
|
// expand the key to 80 bytes using HKDF
|
|
const expandedKey = hkdf(sharedKey as Buffer, 80, { })
|
|
|
|
// perform HMAC validation.
|
|
const hmacValidationKey = expandedKey.slice(32, 64)
|
|
const hmacValidationMessage = Buffer.concat([secret.slice(0, 32), secret.slice(64, secret.length)])
|
|
|
|
const hmac = hmacSign(hmacValidationMessage, hmacValidationKey)
|
|
|
|
if(!hmac.equals(secret.slice(32, 64))) {
|
|
// if the checksums didn't match
|
|
throw new Boom('HMAC validation failed', { statusCode: 400 })
|
|
}
|
|
|
|
// computed HMAC should equal secret[32:64]
|
|
// expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp
|
|
// they are encrypted using key: expandedKey[0:32]
|
|
const encryptedAESKeys = Buffer.concat([
|
|
expandedKey.slice(64, expandedKey.length),
|
|
secret.slice(64, secret.length),
|
|
])
|
|
const decryptedKeys = aesDecrypt(encryptedAESKeys, expandedKey.slice(0, 32))
|
|
// set the credentials
|
|
auth = {
|
|
encKey: decryptedKeys.slice(0, 32), // first 32 bytes form the key to encrypt/decrypt messages
|
|
macKey: decryptedKeys.slice(32, 64), // last 32 bytes from the key to sign messages
|
|
clientToken: json.clientToken,
|
|
serverToken: json.serverToken,
|
|
clientID: auth.clientID,
|
|
}
|
|
return onValidationSuccess()
|
|
}
|
|
|
|
export const computeChallengeResponse = (challenge: string, auth: LegacyAuthenticationCreds) => {
|
|
const bytes = Buffer.from(challenge, 'base64') // decode the base64 encoded challenge string
|
|
const signed = hmacSign(bytes, auth.macKey).toString('base64') // sign the challenge string with our macKey
|
|
return ['admin', 'challenge', signed, auth.serverToken, auth.clientID] // prepare to send this signed string with the serverToken & clientID
|
|
}
|
|
|
|
export const useSingleFileLegacyAuthState = (file: string) => {
|
|
// require fs here so that in case "fs" is not available -- the app does not crash
|
|
const { readFileSync, writeFileSync, existsSync } = require('fs')
|
|
let state: LegacyAuthenticationCreds
|
|
|
|
if(existsSync(file)) {
|
|
state = JSON.parse(
|
|
readFileSync(file, { encoding: 'utf-8' }),
|
|
BufferJSON.reviver
|
|
)
|
|
if(typeof state.encKey === 'string') {
|
|
state.encKey = Buffer.from(state.encKey, 'base64')
|
|
}
|
|
|
|
if(typeof state.macKey === 'string') {
|
|
state.macKey = Buffer.from(state.macKey, 'base64')
|
|
}
|
|
} else {
|
|
state = newLegacyAuthCreds()
|
|
}
|
|
|
|
return {
|
|
state,
|
|
saveState: () => {
|
|
const str = JSON.stringify(state, BufferJSON.replacer, 2)
|
|
writeFileSync(file, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
export const getAuthenticationCredsType = (creds: LegacyAuthenticationCreds | AuthenticationCreds) => {
|
|
if('clientID' in creds && !!creds.clientID) {
|
|
return 'legacy'
|
|
}
|
|
|
|
if('noiseKey' in creds && !!creds.noiseKey) {
|
|
return 'md'
|
|
}
|
|
} |