mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Unread fix + regen QR code changes + Browser message decoding
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -7,4 +7,7 @@ package-lock.json
|
||||
.env
|
||||
lib
|
||||
auth_info_browser.json
|
||||
yarn.lock
|
||||
yarn.lock
|
||||
browser-messages.json
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
|
||||
3158
package-lock.json
generated
3158
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,8 @@
|
||||
"test": "mocha --timeout 30000 -r ts-node/register src/*/Tests.ts",
|
||||
"lint": "eslint '*/*.ts' --quiet --fix",
|
||||
"build": "tsc",
|
||||
"example": "npx ts-node Example/example.ts"
|
||||
"example": "npx ts-node Example/example.ts",
|
||||
"browser-decode": "npx ts-node src/WAConnection/BrowserMessageDecoding.ts"
|
||||
},
|
||||
"author": "Adhiraj Singh",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -20,16 +20,16 @@ import { proto } from '../../WAMessage/WAMessage'
|
||||
export default class WhatsAppWebMessages extends WhatsAppWebBase {
|
||||
/**
|
||||
* Send a read receipt to the given ID for a certain message
|
||||
* @param {string} jid the ID of the person/group whose message you want to mark read
|
||||
* @param {string} [messageID] optionally, the message ID
|
||||
* @param jid the ID of the person/group whose message you want to mark read
|
||||
* @param messageID optionally, the message ID
|
||||
* @param type whether to read or unread the message
|
||||
*/
|
||||
async sendReadReceipt(jid: string, messageID?: string, type: 'read' | 'unread' = 'read') {
|
||||
const attributes = {
|
||||
jid: jid,
|
||||
count: messageID ? '1' : null,
|
||||
count: type === 'read' ? '1' : '-2',
|
||||
index: messageID,
|
||||
owner: 'false',
|
||||
type: type==='unread' ? 'false' : null
|
||||
owner: messageID ? 'false' : null
|
||||
}
|
||||
return this.setQuery ([['read', attributes, null]])
|
||||
}
|
||||
|
||||
@@ -4,13 +4,11 @@ import * as fs from 'fs'
|
||||
import * as assert from 'assert'
|
||||
|
||||
import { decodeMediaMessage, validateJIDForSending } from './Utils'
|
||||
import { promiseTimeout } from '../WAConnection/Utils'
|
||||
import { promiseTimeout, createTimeout } from '../WAConnection/Utils'
|
||||
|
||||
require ('dotenv').config () // dotenv to load test jid
|
||||
const testJid = process.env.TEST_JID || '1234@s.whatsapp.net' // set TEST_JID=xyz@s.whatsapp.net in a .env file in the root directory
|
||||
|
||||
const createTimeout = (timeout) => new Promise(resolve => setTimeout(resolve, timeout))
|
||||
|
||||
async function sendAndRetreiveMessage(client: WAClient, content, type: MessageType, options: MessageOptions = {}) {
|
||||
const response = await client.sendMessage(testJid, content, type, options)
|
||||
assert.strictEqual(response.status, 200)
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
WATag,
|
||||
MessageLogLevel,
|
||||
AuthenticationCredentialsBrowser,
|
||||
Browsers,
|
||||
} from './Constants'
|
||||
|
||||
/** Generate a QR code from the ref & the curve public key. This is scanned by the phone */
|
||||
@@ -22,9 +23,9 @@ const generateQRCode = function ([ref, publicKey, clientID]) {
|
||||
|
||||
export default class WAConnectionBase {
|
||||
/** The version of WhatsApp Web we're telling the servers we are */
|
||||
version: [number, number, number] = [2, 2025, 6]
|
||||
version: [number, number, number] = [2, 2027, 10]
|
||||
/** The Browser we're telling the WhatsApp Web servers we are */
|
||||
browserDescription: [string, string] = ['Baileys', 'Baileys']
|
||||
browserDescription: [string, string, string] = Browsers.baileys ('Baileys')
|
||||
/** Metadata like WhatsApp id, name set on WhatsApp etc. */
|
||||
userMetaData: UserMetaData = { id: null, name: null, phone: null }
|
||||
/** Should reconnect automatically after an unexpected disconnect */
|
||||
|
||||
39
src/WAConnection/BrowserMessageDecoding.ts
Normal file
39
src/WAConnection/BrowserMessageDecoding.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import fs from 'fs'
|
||||
import { decryptWA } from './Utils'
|
||||
import Decoder from '../Binary/Decoder'
|
||||
|
||||
interface BrowserMessagesInfo {
|
||||
encKey: string,
|
||||
macKey: string,
|
||||
messages: string[]
|
||||
}
|
||||
const file = fs.readFileSync ('./browser-messages.json', {encoding: 'utf-8'})
|
||||
const json: BrowserMessagesInfo = JSON.parse (file)
|
||||
|
||||
const encKey = Buffer.from (json.encKey, 'base64')
|
||||
const macKey = Buffer.from (json.macKey, 'base64')
|
||||
|
||||
const decrypt = buffer => {
|
||||
try {
|
||||
return decryptWA (buffer, macKey, encKey, new Decoder())
|
||||
} catch {
|
||||
return decryptWA (buffer, macKey, encKey, new Decoder(), true)
|
||||
}
|
||||
}
|
||||
|
||||
json.messages.forEach ((str, i) => {
|
||||
const buffer = Buffer.from (str, 'hex')
|
||||
try {
|
||||
const [tag, json, binaryTags] = decrypt (buffer)
|
||||
console.log (
|
||||
`
|
||||
${i}.
|
||||
messageTag: ${tag}
|
||||
output: ${JSON.stringify(json)}
|
||||
binaryTags: ${binaryTags}
|
||||
`
|
||||
)
|
||||
} catch (error) {
|
||||
console.error (`received error in decoding ${i}: ${error}`)
|
||||
}
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import WS from 'ws'
|
||||
import * as Utils from './Utils'
|
||||
import { AuthenticationCredentialsBase64, UserMetaData, WAMessage, WAChat, WAContact, MessageLogLevel } from './Constants'
|
||||
import WAConnectionValidator from './Validation'
|
||||
import Decoder from '../Binary/Decoder'
|
||||
|
||||
export default class WAConnectionConnector extends WAConnectionValidator {
|
||||
/**
|
||||
@@ -140,48 +141,12 @@ export default class WAConnectionConnector extends WAConnectionValidator {
|
||||
const timestamp = message.slice(1, message.length)
|
||||
this.lastSeen = new Date(parseInt(timestamp))
|
||||
} else {
|
||||
const commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message
|
||||
|
||||
if (commaIndex < 0) {
|
||||
// if there was no comma, then this message must be not be valid
|
||||
throw [2, 'invalid message', message]
|
||||
}
|
||||
|
||||
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 = message.slice(0, commaIndex).toString()
|
||||
if (data.length === 0) {
|
||||
// got an empty message, usually get one after sending a query with the 128 tag
|
||||
const decrypted = Utils.decryptWA (message, this.authInfo.macKey, this.authInfo.encKey, new Decoder())
|
||||
if (!decrypted) {
|
||||
return
|
||||
}
|
||||
|
||||
let json
|
||||
if (data[0] === '[' || data[0] === '{') {
|
||||
// if the first character is a "[", then the data must just be plain JSON array or object
|
||||
json = JSON.parse(data) // parse the JSON
|
||||
} else if (this.authInfo.macKey && this.authInfo.encKey) {
|
||||
/*
|
||||
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
|
||||
*/
|
||||
|
||||
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 = Utils.hmacSign(data, this.authInfo.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 = Utils.aesDecrypt(data, this.authInfo.encKey) // decrypt using AES
|
||||
json = this.decoder.read(decrypted) // decode the binary message into a JSON array
|
||||
} else {
|
||||
throw [7, "checksums don't match"]
|
||||
}
|
||||
} else {
|
||||
// if we recieved a message that was encrypted but we don't have the keys, then there must be an error
|
||||
throw [3, 'recieved encrypted message when auth creds not available', message]
|
||||
}
|
||||
const [messageTag, json] = decrypted
|
||||
|
||||
if (this.logLevel === MessageLogLevel.all) {
|
||||
this.log(messageTag + ', ' + JSON.stringify(json))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { WA } from '../Binary/Constants'
|
||||
import { proto } from '../../WAMessage/WAMessage'
|
||||
|
||||
export const Browsers: Record<string, (string) => [string, string, string]> = {
|
||||
ubuntu: browser => ['Ubuntu', browser, '18.04'],
|
||||
macOS: browser => ['Mac OS', browser, '10.15.3'],
|
||||
baileys: browser => ['Baileys', browser, '2.0']
|
||||
}
|
||||
export enum MessageLogLevel {
|
||||
none=0,
|
||||
unhandled=1,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as assert from 'assert'
|
||||
import * as QR from 'qrcode-terminal'
|
||||
import WAConnection from './WAConnection'
|
||||
import { AuthenticationCredentialsBase64 } from './Constants'
|
||||
import { createTimeout } from './Utils'
|
||||
|
||||
describe('QR generation', () => {
|
||||
it('should generate QR', async () => {
|
||||
@@ -29,6 +31,23 @@ describe('Test Connect', () => {
|
||||
conn.close()
|
||||
auth = conn.base64EncodedAuthInfo()
|
||||
})
|
||||
it('should re-generate QR & connect', async () => {
|
||||
const conn = new WAConnection()
|
||||
conn.onReadyForPhoneAuthentication = async ([ref, publicKey, clientID]) => {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
console.log ('called QR ' + i + ' times')
|
||||
await createTimeout (3000)
|
||||
ref = await conn.generateNewQRCode ()
|
||||
}
|
||||
const str = ref + ',' + publicKey + ',' + clientID
|
||||
QR.generate(str, { small: true })
|
||||
}
|
||||
const user = await conn.connectSlim(null)
|
||||
assert.ok(user)
|
||||
assert.ok(user.id)
|
||||
|
||||
conn.close()
|
||||
})
|
||||
it('should reconnect', async () => {
|
||||
const conn = new WAConnection()
|
||||
const [user, chats, contacts, unread] = await conn.connect(auth, 20*1000)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as Crypto from 'crypto'
|
||||
import HKDF from 'futoin-hkdf'
|
||||
import Decoder from '../Binary/Decoder'
|
||||
import { off } from 'process'
|
||||
|
||||
/** 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))
|
||||
}
|
||||
@@ -37,6 +38,7 @@ export function hkdf(buffer: Buffer, expandedLength: number, info = null) {
|
||||
export function randomBytes(length) {
|
||||
return Crypto.randomBytes(length)
|
||||
}
|
||||
export const createTimeout = (timeout) => new Promise(resolve => setTimeout(resolve, timeout))
|
||||
export function promiseTimeout<T>(ms: number, promise: Promise<T>) {
|
||||
if (!ms) {
|
||||
return promise
|
||||
@@ -66,10 +68,59 @@ export function generateMessageID() {
|
||||
}
|
||||
|
||||
export function errorOnNon200Status(p: Promise<any>) {
|
||||
return p.then((json) => {
|
||||
return p.then(json => {
|
||||
if (json.status && typeof json.status === 'number' && Math.floor(json.status / 100) !== 2) {
|
||||
throw new Error(`Unexpected status code: ${json.status}`)
|
||||
}
|
||||
return json
|
||||
})
|
||||
}
|
||||
|
||||
export function decryptWA (message: any, macKey: Buffer, encKey: Buffer, decoder: Decoder, fromMe: boolean=false): [string, Object, [number, number]?] {
|
||||
const commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message
|
||||
if (commaIndex < 0) {
|
||||
// if there was no comma, then this message must be not be valid
|
||||
throw [2, 'invalid message', message]
|
||||
}
|
||||
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()
|
||||
if (data.length === 0) {
|
||||
// got an empty message, usually get one after sending a query with the 128 tag
|
||||
return
|
||||
}
|
||||
|
||||
let json
|
||||
let tags = null
|
||||
if (data[0] === '[' || data[0] === '{') {
|
||||
// if the first character is a "[", then the data must just be plain JSON array or object
|
||||
json = JSON.parse(data) // parse the JSON
|
||||
} else {
|
||||
if (!macKey || !encKey) {
|
||||
// if we recieved a message that was encrypted but we don't have the keys, then there must be an error
|
||||
throw [3, 'recieved encrypted message when auth creds not available', data]
|
||||
}
|
||||
/*
|
||||
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)) {
|
||||
throw [7, "checksums don't match"]
|
||||
}
|
||||
|
||||
// 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 = decoder.read(decrypted) // decode the binary message into a JSON array
|
||||
}
|
||||
return [messageTag, json, tags]
|
||||
}
|
||||
@@ -74,10 +74,14 @@ export default class WAConnectionValidator extends WAConnectionBase {
|
||||
return this.userMetaData
|
||||
})
|
||||
}
|
||||
/** Refresh QR Code */
|
||||
protected refreshQRCode() {
|
||||
/**
|
||||
* Refresh QR Code
|
||||
* @returns the new ref
|
||||
*/
|
||||
async generateNewQRCode() {
|
||||
const data = ['admin', 'Conn', 'reref']
|
||||
return this.query(data)
|
||||
const response = await this.query(data)
|
||||
return response.ref as string
|
||||
}
|
||||
/**
|
||||
* Once the QR code is scanned and we can validate our connection, or we resolved the challenge when logging back in
|
||||
@@ -154,31 +158,14 @@ export default class WAConnectionValidator extends WAConnectionBase {
|
||||
this.log('resolving login challenge')
|
||||
return this.query(data)
|
||||
}
|
||||
/**
|
||||
* When starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends
|
||||
* @private
|
||||
*/
|
||||
/** When starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends */
|
||||
protected async generateKeysForAuth(ref: string) {
|
||||
this.curveKeys = Curve.generateKeyPair(Utils.randomBytes(32))
|
||||
|
||||
let retries = 0
|
||||
let _ref = ref
|
||||
|
||||
while (retries < 5) {
|
||||
retries++
|
||||
|
||||
this.onReadyForPhoneAuthentication([
|
||||
_ref,
|
||||
Buffer.from(this.curveKeys.public).toString('base64'),
|
||||
this.authInfo.clientID,
|
||||
])
|
||||
|
||||
try {
|
||||
return await this.waitForMessage('s1', [], 20 * 1000)
|
||||
} catch (err) {
|
||||
const json = await this.refreshQRCode()
|
||||
_ref = json.ref
|
||||
}
|
||||
}
|
||||
this.onReadyForPhoneAuthentication([
|
||||
ref,
|
||||
Buffer.from(this.curveKeys.public).toString('base64'),
|
||||
this.authInfo.clientID,
|
||||
])
|
||||
return this.waitForMessage('s1', [])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user