diff --git a/README.md b/README.md index ac52bfd..4fa4a5f 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,10 @@ If the connection is successful, you will see a QR code printed on your terminal ## Notable Differences Between Baileys Web & MD -1. Baileys no longer maintains an internal state of chats/contacts/messages. You must take this on your own, simply because your state in MD is its own source of truth & there is no one-size-fits-all way to handle the storage for this. -2. A baileys "socket" is meant to be a temporary & disposable object -- this is done to maintain simplicity & prevent bugs. I felt the entire Baileys object became too bloated as it supported too many configurations. -3. Moreover, Baileys does not offer an inbuilt reconnect mechanism anymore (though it's super easy to set one up on your own with your own rules, check the example script) +1. Baileys has been written from the ground up to have a more "functional" structure. This is done primarily for simplicity & more testability +2. Baileys no longer maintains an internal state of chats/contacts/messages. You must take this on your own, simply because your state in MD is its own source of truth & there is no one-size-fits-all way to handle the storage for this. +3. A baileys "socket" is meant to be a temporary & disposable object -- this is done to maintain simplicity & prevent bugs. I felt the entire Baileys object became too bloated as it supported too many configurations. You're encouraged to write your own implementation to handle missing functionality. +4. Moreover, Baileys does not offer an inbuilt reconnect mechanism anymore (though it's super easy to set one up on your own with your own rules, check the example script) ## Configuring the Connection diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index d8c2ec1..a3c4b25 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -4,7 +4,7 @@ import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUse import { makeSocket } from "./socket"; import { proto } from '../../WAProto' import { toNumber } from "../Utils/generics"; -import { compressImage, generateProfilePicture } from ".."; +import { generateProfilePicture } from "../Utils"; export const makeChatsSocket = (config: SocketConfig) => { const { logger } = config diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 6f5a14c..33c68da 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -1,8 +1,8 @@ import { makeGroupsSocket } from "./groups" import { SocketConfig, WAMessageStubType, ParticipantAction, Chat, GroupMetadata } from "../Types" -import { decodeMessageStanza, encodeBigEndian, toNumber, whatsappID } from "../Utils" -import { BinaryNode, jidDecode, jidEncode, isJidStatusBroadcast, S_WHATSAPP_NET, areJidsSameUser, getBinaryNodeChildren, getBinaryNodeChild } from '../WABinary' +import { decodeMessageStanza, encodeBigEndian, toNumber } from "../Utils" +import { BinaryNode, jidDecode, jidEncode, isJidStatusBroadcast, areJidsSameUser, getBinaryNodeChildren } from '../WABinary' import { downloadIfHistory } from '../Utils/history' import { proto } from "../../WAProto" import { generateSignalPubKey, xmppPreKey, xmppSignedPreKey } from "../Utils/signal" @@ -139,7 +139,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { switch (message.messageStubType) { case WAMessageStubType.GROUP_PARTICIPANT_LEAVE: case WAMessageStubType.GROUP_PARTICIPANT_REMOVE: - participants = message.messageStubParameters.map(whatsappID) + participants = message.messageStubParameters emitParticipantsUpdate('remove') // mark the chat read only if you left the group if (participants.includes(meJid)) { @@ -149,7 +149,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { case WAMessageStubType.GROUP_PARTICIPANT_ADD: case WAMessageStubType.GROUP_PARTICIPANT_INVITE: case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN: - participants = message.messageStubParameters.map(whatsappID) + participants = message.messageStubParameters if (participants.includes(meJid)) { chatUpdate.readOnly = false } diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index e3ad179..309a1f1 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -5,7 +5,7 @@ import WebSocket from "ws" import { randomBytes } from 'crypto' import { proto } from '../../WAProto' import { DisconnectReason, SocketConfig, BaileysEventEmitter } from "../Types" -import { generateCurveKeyPair, initAuthState, generateRegistrationNode, configureSuccessfulPairing, generateLoginNode, encodeBigEndian, promiseTimeout } from "../Utils" +import { Curve, initAuthState, generateRegistrationNode, configureSuccessfulPairing, generateLoginNode, encodeBigEndian, promiseTimeout } from "../Utils" import { DEFAULT_ORIGIN, DEF_TAG_PREFIX, DEF_CALLBACK_PREFIX, KEY_BUNDLE_TYPE } from "../Defaults" import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, S_WHATSAPP_NET } from '../WABinary' import noiseHandler from '../Utils/noise-handler' @@ -42,7 +42,7 @@ export const makeSocket = ({ }) ws.setMaxListeners(0) /** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */ - const ephemeralKeyPair = generateCurveKeyPair() + const ephemeralKeyPair = Curve.generateKeyPair() /** WA noise protocol wrapper */ const noise = noiseHandler(ephemeralKeyPair) const authState = initialAuthState || initAuthState() diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index 075fa91..35c02b6 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -1,5 +1,5 @@ import { Boom } from '@hapi/boom' -import { aesDecrypt, hmacSign, aesEncrypt, hkdf } from "./generics" +import { aesDecrypt, hmacSign, aesEncrypt, hkdf } from "./crypto" import { AuthenticationState, ChatModification } from "../Types" import { proto } from '../../WAProto' import { LT_HASH_ANTI_TAMPERING } from '../WABinary/LTHash' diff --git a/src/Utils/crypto.ts b/src/Utils/crypto.ts new file mode 100644 index 0000000..201ae7e --- /dev/null +++ b/src/Utils/crypto.ts @@ -0,0 +1,100 @@ +import CurveCrypto from 'libsignal/src/curve25519_wrapper' +import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto' +import { KeyPair } from '../Types' + +export const Curve = { + generateKeyPair: (): KeyPair => { + const { pubKey, privKey } = CurveCrypto.keyPair(randomBytes(32)) + return { + private: Buffer.from(privKey), + public: Buffer.from(pubKey) + } + }, + sharedKey: (privateKey: Uint8Array, publicKey: Uint8Array) => { + const shared = CurveCrypto.sharedSecret(publicKey, privateKey) + return Buffer.from(shared) + }, + sign: (privateKey: Uint8Array, buf: Uint8Array) => ( + Buffer.from(CurveCrypto.sign(privateKey, buf)) + ), + verify: (pubKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => { + try { + CurveCrypto.verify(pubKey, message, signature) + return true + } catch(error) { + if(error.message.includes('Invalid')) { + return false + } + throw error + } + } +} + +export const signedKeyPair = (keyPair: KeyPair, keyId: number) => { + const signKeys = Curve.generateKeyPair() + const pubKey = new Uint8Array(33) + pubKey.set([5], 0) + pubKey.set(signKeys.public, 1) + + const signature = Curve.sign(keyPair.private, pubKey) + + return { keyPair: signKeys, signature, keyId } +} + +/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */ +export function aesDecrypt(buffer: Buffer, key: Buffer) { + return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16)) +} +/** decrypt AES 256 CBC */ +export function aesDecryptWithIV(buffer: Buffer, key: Buffer, IV: Buffer) { + const aes = createDecipheriv('aes-256-cbc', key, IV) + return Buffer.concat([aes.update(buffer), aes.final()]) +} +// encrypt AES 256 CBC; where a random IV is prefixed to the buffer +export function aesEncrypt(buffer: Buffer | Uint8Array, key: Buffer) { + const IV = randomBytes(16) + const aes = createCipheriv('aes-256-cbc', key, IV) + return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer +} +// encrypt AES 256 CBC with a given IV +export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) { + const aes = createCipheriv('aes-256-cbc', key, IV) + return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer +} +// sign HMAC using SHA 256 +export function hmacSign(buffer: Buffer | Uint8Array, key: Buffer | Uint8Array, variant: 'sha256' | 'sha512' = 'sha256') { + return createHmac(variant, key).update(buffer).digest() +} +export function sha256(buffer: Buffer) { + return createHash('sha256').update(buffer).digest() +} +// HKDF key expansion +// from: https://github.com/benadida/node-hkdf +export function hkdf(buffer: Buffer, expandedLength: number, { info, salt }: { salt?: Buffer, info?: string }) { + const hashAlg = 'sha256' + const hashLength = 32 + salt = salt || Buffer.alloc(hashLength) + // now we compute the PRK + const prk = createHmac(hashAlg, salt).update(buffer).digest() + + let prev = Buffer.from([]) + const buffers = [] + const num_blocks = Math.ceil(expandedLength / hashLength) + + const infoBuff = Buffer.from(info || []) + + for (var i=0; i ( ) ) -export const generateCurveKeyPair = (): KeyPair => { - const { pubKey, privKey } = CurveCrypto.keyPair(randomBytes(32)) - return { - private: Buffer.from(privKey), - public: Buffer.from(pubKey) - } -} - -export const generateSharedKey = (privateKey: Uint8Array, publicKey: Uint8Array) => { - const shared = CurveCrypto.sharedSecret(publicKey, privateKey) - return Buffer.from(shared) -} - -export const curveSign = (privateKey: Uint8Array, buf: Uint8Array) => ( - Buffer.from(CurveCrypto.sign(privateKey, buf)) -) - -export const curveVerify = (pubKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => { - try { - CurveCrypto.verify(pubKey, message, signature) - return true - } catch(error) { - if(error.message.includes('Invalid')) { - return false - } - throw error - } -} - -export const signedKeyPair = (keyPair: KeyPair, keyId: number) => { - const signKeys = generateCurveKeyPair() - const pubKey = new Uint8Array(33) - pubKey.set([5], 0) - pubKey.set(signKeys.public, 1) - - const signature = curveSign(keyPair.private, pubKey) - - return { keyPair: signKeys, signature, keyId } -} - export const generateRegistrationId = () => ( Uint16Array.from(randomBytes(2))[0] & 0x3fff ) @@ -134,9 +92,6 @@ export const encodeBigEndian = (e: number, t=4) => { export const toNumber = (t: Long | number) => (typeof t?.['low'] !== 'undefined' ? t['low'] : t) as number -export const whatsappID = (jid: string) => jid?.replace ('@c.us', '@s.whatsapp.net') -export const isGroupID = (jid: string) => jid?.endsWith ('@g.us') - export function shallowChanges (old: T, current: T, {lookForDeletedKeys}: {lookForDeletedKeys: boolean}): Partial { let changes: Partial = {} for (let key in current) { @@ -153,64 +108,6 @@ export function shallowChanges (old: T, current: T, {lookForDeletedKeys}: {l } return changes } - -/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */ -export function aesDecrypt(buffer: Buffer, key: Buffer) { - return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16)) -} -/** decrypt AES 256 CBC */ -export function aesDecryptWithIV(buffer: Buffer, key: Buffer, IV: Buffer) { - const aes = createDecipheriv('aes-256-cbc', key, IV) - return Buffer.concat([aes.update(buffer), aes.final()]) -} -// encrypt AES 256 CBC; where a random IV is prefixed to the buffer -export function aesEncrypt(buffer: Buffer | Uint8Array, key: Buffer) { - const IV = randomBytes(16) - const aes = createCipheriv('aes-256-cbc', key, IV) - return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer -} -// encrypt AES 256 CBC with a given IV -export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) { - const aes = createCipheriv('aes-256-cbc', key, IV) - return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer -} -// sign HMAC using SHA 256 -export function hmacSign(buffer: Buffer | Uint8Array, key: Buffer | Uint8Array, variant: 'sha256' | 'sha512' = 'sha256') { - return createHmac(variant, key).update(buffer).digest() -} -export function sha256(buffer: Buffer) { - return createHash('sha256').update(buffer).digest() -} -// HKDF key expansion -// from: https://github.com/benadida/node-hkdf -export function hkdf(buffer: Buffer, expandedLength: number, { info, salt }: { salt?: Buffer, info?: string }) { - const hashAlg = 'sha256' - const hashLength = 32 - salt = salt || Buffer.alloc(hashLength) - // now we compute the PRK - const prk = createHmac(hashAlg, salt).update(buffer).digest() - - let prev = Buffer.from([]) - const buffers = [] - const num_blocks = Math.ceil(expandedLength / hashLength) - - const infoBuff = Buffer.from(info || []) - - for (var i=0; i Math.floor(date.getTime()/1000) diff --git a/src/Utils/index.ts b/src/Utils/index.ts index 15973e8..8ea6ea7 100644 --- a/src/Utils/index.ts +++ b/src/Utils/index.ts @@ -2,4 +2,5 @@ export * from './decode-wa-message' export * from './generics' export * from './messages' export * from './messages-media' -export * from './validate-connection' \ No newline at end of file +export * from './validate-connection' +export * from './crypto' \ No newline at end of file diff --git a/src/Utils/messages-media.ts b/src/Utils/messages-media.ts index b987422..3df3a76 100644 --- a/src/Utils/messages-media.ts +++ b/src/Utils/messages-media.ts @@ -11,7 +11,8 @@ import { join } from 'path' import { once } from 'events' import got, { Options, Response } from 'got' import { MessageType, WAMessageContent, WAProto, WAGenericMediaMessage, WAMediaUpload, MediaType } from '../Types' -import { generateMessageID, hkdf } from './generics' +import { generateMessageID } from './generics' +import { hkdf } from './crypto' import { DEFAULT_ORIGIN } from '../Defaults' export const hkdfInfoKey = (type: MediaType) => { diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index 13c58ec..2f845d9 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -18,7 +18,7 @@ import { MediaType, WAMessageStatus } from "../Types" -import { generateMessageID, unixTimestampSeconds, whatsappID } from "./generics" +import { generateMessageID, unixTimestampSeconds } from "./generics" import { encryptedStream, generateThumbnail, getAudioDuration } from "./messages-media" type MediaUploadData = { @@ -268,8 +268,6 @@ export const generateWAMessageFromContent = ( options: MessageGenerationOptionsFromContent ) => { if (!options.timestamp) options.timestamp = new Date() // set timestamp to now - // prevent an annoying bug (WA doesn't accept sending messages with '@c.us') - jid = whatsappID(jid) const key = Object.keys(message)[0] const timestamp = unixTimestampSeconds(options.timestamp) diff --git a/src/Utils/noise-handler.ts b/src/Utils/noise-handler.ts index 1f94871..4f60c03 100644 --- a/src/Utils/noise-handler.ts +++ b/src/Utils/noise-handler.ts @@ -1,4 +1,4 @@ -import { sha256, generateSharedKey, hkdf } from "./generics"; +import { sha256, Curve, hkdf } from "./crypto"; import { Binary } from "../WABinary"; import { createCipheriv, createDecipheriv } from "crypto"; import { NOISE_MODE, NOISE_WA_HEADER } from "../Defaults"; @@ -100,10 +100,10 @@ export default ({ public: publicKey, private: privateKey }: KeyPair) => { finishInit, processHandshake: ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => { authenticate(serverHello!.ephemeral!) - mixIntoKey(generateSharedKey(privateKey, serverHello.ephemeral!)) + mixIntoKey(Curve.sharedKey(privateKey, serverHello.ephemeral!)) const decStaticContent = decrypt(serverHello!.static!) - mixIntoKey(generateSharedKey(privateKey, decStaticContent)) + mixIntoKey(Curve.sharedKey(privateKey, decStaticContent)) const certDecoded = decrypt(serverHello!.payload!) const { details: certDetails, signature: certSignature } = proto.NoiseCertificate.decode(certDecoded) @@ -115,7 +115,7 @@ export default ({ public: publicKey, private: privateKey }: KeyPair) => { } const keyEnc = encrypt(noiseKey.public) - mixIntoKey(generateSharedKey(noiseKey.private, serverHello!.ephemeral!)) + mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!)) return keyEnc }, diff --git a/src/Utils/signal.ts b/src/Utils/signal.ts index 2940c7c..3eab1fc 100644 --- a/src/Utils/signal.ts +++ b/src/Utils/signal.ts @@ -1,5 +1,6 @@ import * as libsignal from 'libsignal' -import { encodeBigEndian, generateCurveKeyPair } from "./generics" +import { encodeBigEndian } from "./generics" +import { Curve } from "./crypto" import { SenderKeyDistributionMessage, GroupSessionBuilder, SenderKeyRecord, SenderKeyName, GroupCipher } from '../../WASignalGroup' import { SignalIdentity, SignalKeyStore, SignedKeyPair, KeyPair, AuthenticationState } from "../Types/Auth" import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildUInt, jidDecode } from "../WABinary" @@ -48,7 +49,7 @@ export const generateOrGetPreKeys = ({ creds }: AuthenticationState, range: numb const newPreKeys: { [id: number]: KeyPair } = { } if(remaining > 0) { for(let i = creds.nextPreKeyId;i <= lastPreKeyId;i++) { - newPreKeys[i] = generateCurveKeyPair() + newPreKeys[i] = Curve.generateKeyPair() } } return { diff --git a/src/Utils/validate-connection.ts b/src/Utils/validate-connection.ts index fa2c903..02446e1 100644 --- a/src/Utils/validate-connection.ts +++ b/src/Utils/validate-connection.ts @@ -2,7 +2,8 @@ import { Boom } from '@hapi/boom' import { randomBytes } from 'crypto' import { proto } from '../../WAProto' import type { AuthenticationState, SocketConfig, SignalKeyStore, AuthenticationCreds, KeyPair } from "../Types" -import { curveSign, hmacSign, curveVerify, encodeInt, generateCurveKeyPair, generateRegistrationId, signedKeyPair } from './generics' +import { Curve, hmacSign, signedKeyPair } from './crypto' +import { encodeInt, generateRegistrationId } from './generics' import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary } from '../WABinary' import { createSignalIdentity } from './signal' @@ -116,10 +117,10 @@ export const initInMemoryKeyStore = ( } export const initAuthState = (): AuthenticationState => { - const identityKey = generateCurveKeyPair() + const identityKey = Curve.generateKeyPair() return { creds: { - noiseKey: generateCurveKeyPair(), + noiseKey: Curve.generateKeyPair(), signedIdentityKey: identityKey, signedPreKey: signedKeyPair(identityKey, 1), registrationId: generateRegistrationId(), @@ -158,12 +159,12 @@ export const configureSuccessfulPairing = ( const { accountSignatureKey, accountSignature } = account const accountMsg = Binary.build(new Uint8Array([6, 0]), account.details, signedIdentityKey.public).readByteArray() - if (!curveVerify(accountSignatureKey, accountMsg, accountSignature)) { + if (!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) { throw new Boom('Failed to verify account signature') } const deviceMsg = Binary.build(new Uint8Array([6, 1]), account.details, signedIdentityKey.public, account.accountSignatureKey).readByteArray() - account.deviceSignature = curveSign(signedIdentityKey.private, deviceMsg) + account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg) const identity = createSignalIdentity(jid, accountSignatureKey)