feat: native-mobile-api

This commit is contained in:
SamuelScheit
2023-04-20 13:01:11 +02:00
parent 28be45a9b4
commit ef673f62ca
17 changed files with 940 additions and 74 deletions

View File

@@ -4,22 +4,36 @@ import type { AuthenticationState, MediaType, SocketConfig, WAVersion } from '..
import { Browsers } from '../Utils'
import logger from '../Utils/logger'
import { version } from './baileys-version.json'
import phoneNumberMCC from './phonenumber-mcc.json'
export const UNAUTHORIZED_CODES = [401, 403, 419]
export const PHONENUMBER_MCC = phoneNumberMCC
export const DEFAULT_ORIGIN = 'https://web.whatsapp.com'
export const MOBILE_ENDPOINT = 'g.whatsapp.net'
export const MOBILE_PORT = 443
export const DEF_CALLBACK_PREFIX = 'CB:'
export const DEF_TAG_PREFIX = 'TAG:'
export const PHONE_CONNECTION_CB = 'CB:Pong'
export const WA_DEFAULT_EPHEMERAL = 7 * 24 * 60 * 60
export const MOBILE_TOKEN = Buffer.from('0a1mLfGUIBVrMKF1RdvLI5lkRBvof6vn0fD2QRSM4174c0243f5277a5d7720ce842cc4ae6')
export const MOBILE_REGISTRATION_ENDPOINT = 'https://v.whatsapp.net/v2'
export const MOBILE_USERAGENT = 'WhatsApp/2.22.24.81 iOS/15.3.1 Device/Apple-iPhone_7'
export const REGISTRATION_PUBLIC_KEY = Buffer.from([
5, 142, 140, 15, 116, 195, 235, 197, 215, 166, 134, 92, 108, 60, 132, 56, 86, 176, 97, 33, 204, 232, 234, 119, 77,
34, 251, 111, 18, 37, 18, 48, 45,
])
export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0'
export const DICT_VERSION = 2
export const KEY_BUNDLE_TYPE = Buffer.from([5])
export const NOISE_WA_HEADER = Buffer.from(
[ 87, 65, 6, DICT_VERSION ]
) // last is "DICT_VERSION"
export const PROTOCOL_VERSION = [5, 2]
export const MOBILE_NOISE_HEADER = Buffer.concat([Buffer.from('WA'), Buffer.from(PROTOCOL_VERSION)])
/** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */
export const URL_REGEX = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/
export const URL_EXCLUDE_REGEX = /.*@.*/

View File

@@ -0,0 +1,223 @@
{
"93": 412,
"355": 276,
"213": 603,
"1-684": 544,
"376": 213,
"244": 631,
"1-264": 365,
"1-268": 344,
"54": 722,
"374": 283,
"297": 363,
"61": 505,
"43": 232,
"994": 400,
"1-242": 364,
"973": 426,
"880": 470,
"1-246": 342,
"375": 257,
"32": 206,
"501": 702,
"229": 616,
"1-441": 350,
"975": 402,
"591": 736,
"387": 218,
"267": 652,
"55": 724,
"1-284": 348,
"673": 528,
"359": 284,
"226": 613,
"257": 642,
"855": 456,
"237": 624,
"238": 625,
"1-345": 346,
"236": 623,
"235": 622,
"56": 730,
"86": 454,
"57": 732,
"269": 654,
"682": 548,
"506": 712,
"385": 219,
"53": 368,
"357": 280,
"420": 230,
"243": 630,
"45": 238,
"253": 638,
"1-767": 366,
"1-809": 370,
"1-849": 370,
"1-829": 370,
"593": 740,
"20": 602,
"503": 706,
"240": 627,
"291": 657,
"372": 248,
"251": 636,
"500": 750,
"298": 288,
"679": 542,
"358": 244,
"33": 208,
"689": 547,
"241": 628,
"220": 607,
"995": 282,
"49": 262,
"233": 620,
"350": 266,
"30": 202,
"299": 290,
"1-473": 352,
"1-671": 535,
"502": 704,
"224": 537,
"592": 738,
"509": 372,
"504": 708,
"852": 454,
"36": 216,
"354": 274,
"91": 404,
"62": 510,
"98": 432,
"964": 418,
"353": 234,
"972": 425,
"39": 222,
"225": 612,
"1-876": 338,
"81": 440,
"962": 416,
"254": 639,
"686": 545,
"383": 221,
"965": 419,
"371": 247,
"961": 415,
"266": 651,
"231": 618,
"218": 606,
"423": 295,
"370": 246,
"352": 270,
"389": 294,
"261": 646,
"265": 650,
"60": 502,
"960": 472,
"223": 610,
"356": 278,
"692": 551,
"222": 609,
"230": 617,
"52": 334,
"691": 550,
"373": 259,
"377": 212,
"976": 428,
"382": 297,
"1-664": 354,
"212": 604,
"258": 643,
"95": 414,
"264": 649,
"674": 536,
"977": 429,
"31": 204,
"687": 546,
"64": 530,
"505": 710,
"227": 614,
"234": 621,
"683": 555,
"1-670": 534,
"47": 242,
"968": 226,
"92": 410,
"680": 552,
"970": 423,
"507": 714,
"675": 537,
"595": 744,
"51": 716,
"63": 515,
"48": 260,
"351": 268,
"1-787, 1-939": 330,
"974": 427,
"242": 630,
"40": 226,
"7": 250,
"250": 635,
"290": 658,
"1-869": 356,
"1-758": 358,
"508": 308,
"1-784": 360,
"685": 544,
"378": 292,
"239": 626,
"966": 420,
"221": 608,
"381": 220,
"248": 633,
"232": 619,
"65": 525,
"386": 293,
"677": 540,
"27": 655,
"211": 659,
"34": 214,
"94": 413,
"249": 634,
"597": 746,
"268": 653,
"46": 240,
"41": 228,
"963": 417,
"886": 466,
"992": 436,
"255": 640,
"66": 520,
"228": 615,
"690": 554,
"676": 539,
"1-868": 374,
"216": 605,
"90": 286,
"993": 438,
"1-649": 376,
"688": 553,
"1-340": 332,
"256": 641,
"380": 255,
"971": 424,
"44": 234,
"1": 310,
"598": 748,
"998": 434,
"678": 541,
"379": 225,
"58": 734,
"681": 543,
"967": 421,
"260": 645,
"263": 648,
"670": 514,
"245": 632,
"856": 457,
"599": 362,
"850": 467,
"262": 647,
"82": 450,
"84": 452
}

View File

@@ -1,6 +1,6 @@
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
import { UserFacingSocketConfig } from '../Types'
import { makeBusinessSocket as _makeSocket } from './business'
import { makeRegistrationSocket as _makeSocket } from './registration'
// export the last socket layer
const makeWASocket = (config: UserFacingSocketConfig) => (

View File

@@ -0,0 +1,47 @@
import { Socket } from 'net'
import { MOBILE_ENDPOINT, MOBILE_PORT } from '../Defaults'
import { SocketConfig } from '../Types'
export class MobileSocket extends Socket {
constructor(public config: SocketConfig) {
super()
if(config.auth.creds.registered) {
this.connect()
}
this.on('data', (d) => {
this.emit('message', d)
})
}
override connect() {
return super.connect(MOBILE_PORT, MOBILE_ENDPOINT, () => {
this.emit('open')
})
}
get isOpen(): boolean {
return this.readyState === 'open'
}
get isClosed(): boolean {
return this.readyState === 'closed'
}
get isClosing(): boolean {
return this.isClosed
}
get isConnecting(): boolean {
return this.readyState === 'opening'
}
close(): void {
this.end()
}
send(data: unknown, cb?: ((err?: Error | undefined) => void) | undefined) {
return super.write(data as Uint8Array | string, cb as ((err?: Error | undefined) => void))
}
}

251
src/Socket/registration.ts Normal file
View File

@@ -0,0 +1,251 @@
/* eslint-disable camelcase */
import { MOBILE_REGISTRATION_ENDPOINT, MOBILE_TOKEN, MOBILE_USERAGENT, REGISTRATION_PUBLIC_KEY } from '../Defaults'
import { KeyPair, SignedKeyPair, SocketConfig } from '../Types'
import { aesEncryptGCM, Curve, md5 } from '../Utils/crypto'
import { jidEncode } from '../WABinary'
import { makeBusinessSocket } from './business'
import { MobileSocket } from './mobile-socket'
function urlencode(str: string) {
return str.replace(/-/g, '%2d').replace(/_/g, '%5f').replace(/~/g, '%7e')
}
const validRegistrationOptions = (config: RegistrationOptions) => config?.phoneNumberCountryCode &&
config.phoneNumberNationalNumber &&
config.phoneNumberMobileCountryCode
export const makeRegistrationSocket = (config: SocketConfig) => {
const sock = makeBusinessSocket(config)
const register = async(code: string) => {
if(!validRegistrationOptions(config.auth.creds.registration)) {
throw new Error('please specify the registration options')
}
const result = await mobileRegister({ ...sock.authState.creds, ...sock.authState.creds.registration as RegistrationOptions, code })
sock.authState.creds.me = {
id: jidEncode(result.login!, 's.whatsapp.net'),
name: '~'
}
sock.authState.creds.registered = true
sock.ev.emit('creds.update', sock.authState.creds)
if(sock.ws instanceof MobileSocket) {
sock.ws.connect()
}
return result
}
const requestRegistrationCode = async(registrationOptions?: RegistrationOptions) => {
registrationOptions = registrationOptions || config.auth.creds.registration
if(!validRegistrationOptions(registrationOptions)) {
throw new Error('Invalid registration options')
}
sock.authState.creds.registration = registrationOptions
sock.ev.emit('creds.update', sock.authState.creds)
return mobileRegisterCode({ ...config.auth.creds, ...registrationOptions })
}
return {
...sock,
register,
requestRegistrationCode,
}
}
// Backup_token: Base64.getEncoder().encodeToString(Arrays.copyOfRange(Base64.getDecoder().decode(UUID.randomUUID().toString().replace('-','')),0,15))
export interface RegistrationData {
registrationId: number
signedPreKey: SignedKeyPair
noiseKey: KeyPair
signedIdentityKey: KeyPair
identityId: Buffer
phoneId: string
deviceId: string
backupToken: Buffer
}
export interface RegistrationOptions {
/** your phone number */
phoneNumber?: string
/** the country code of your phone number */
phoneNumberCountryCode: string
/** your phone number without country code */
phoneNumberNationalNumber: string
/** the country code of your mobile network
* @see {@link https://de.wikipedia.org/wiki/Mobile_Country_Code}
*/
phoneNumberMobileCountryCode: string
/** the network code of your mobile network
* @see {@link https://de.wikipedia.org/wiki/Mobile_Network_Code}
*/
phoneNumberMobileNetworkCode: string
/**
* How to send the one time code
*/
method?: 'sms' | 'voice'
}
export type RegistrationParams = RegistrationData & RegistrationOptions
function convertBufferToUrlHex(buffer: Buffer) {
var id = ''
buffer.forEach((x) => {
// encode random identity_id buffer as percentage url encoding
id += `%${x.toString(16).padStart(2, '0').toLowerCase()}`
})
return id
}
export function registrationParams(params: RegistrationParams) {
const e_regid = Buffer.alloc(4)
e_regid.writeInt32BE(params.registrationId)
const e_skey_id = Buffer.alloc(3)
e_skey_id.writeInt16BE(params.signedPreKey.keyId)
params.phoneNumberCountryCode = params.phoneNumberCountryCode.replace('+', '').trim()
params.phoneNumberNationalNumber = params.phoneNumberNationalNumber.replace(/[/-\s)(]/g, '').trim()
return {
cc: params.phoneNumberCountryCode,
in: params.phoneNumberNationalNumber,
Rc: '0',
lg: 'en',
lc: 'GB',
mistyped: '6',
authkey: Buffer.from(params.noiseKey.public).toString('base64url'),
e_regid: e_regid.toString('base64url'),
e_keytype: 'BQ',
e_ident: Buffer.from(params.signedIdentityKey.public).toString('base64url'),
// e_skey_id: e_skey_id.toString('base64url'),
e_skey_id: 'AAAA',
e_skey_val: Buffer.from(params.signedPreKey.keyPair.public).toString('base64url'),
e_skey_sig: Buffer.from(params.signedPreKey.signature).toString('base64url'),
fdid: params.phoneId,
network_ratio_type: '1',
expid: params.deviceId,
simnum: '1',
hasinrc: '1',
pid: Math.floor(Math.random() * 1000).toString(),
id: convertBufferToUrlHex(params.identityId),
backup_token: convertBufferToUrlHex(params.backupToken),
token: md5(Buffer.concat([MOBILE_TOKEN, Buffer.from(params.phoneNumberNationalNumber)])).toString('hex'),
}
}
/**
* Requests a registration code for the given phone number.
*/
export function mobileRegisterCode(params: RegistrationParams) {
return mobileRegisterFetch('/code', {
params: {
...registrationParams(params),
mcc: `${params.phoneNumberMobileCountryCode}`.padStart(3, '0'),
mnc: `${params.phoneNumberMobileNetworkCode || '001'}`.padStart(3, '0'),
sim_mcc: '000',
sim_mnc: '000',
method: params?.method || 'sms',
reason: '',
hasav: '1'
},
})
}
export function mobileRegisterExists(params: RegistrationParams) {
return mobileRegisterFetch('/exist', {
params: registrationParams(params)
})
}
/**
* Registers the phone number on whatsapp with the received OTP code.
*/
export async function mobileRegister(params: RegistrationParams & { code: string }) {
//const result = await mobileRegisterFetch(`/reg_onboard_abprop?cc=${params.phoneNumberCountryCode}&in=${params.phoneNumberNationalNumber}&rc=0`)
return mobileRegisterFetch('/register', {
params: { ...registrationParams(params), code: params.code.replace('-', '') },
})
}
/**
* Encrypts the given string as AEAD aes-256-gcm with the public whatsapp key and a random keypair.
*/
export function mobileRegisterEncrypt(data: string) {
const keypair = Curve.generateKeyPair()
const key = Curve.sharedKey(keypair.private, REGISTRATION_PUBLIC_KEY)
const buffer = aesEncryptGCM(Buffer.from(data), new Uint8Array(key), Buffer.alloc(12), Buffer.alloc(0))
return Buffer.concat([Buffer.from(keypair.public), buffer]).toString('base64url')
}
export async function mobileRegisterFetch(path: string, opts: { params?: Record<string, string>, headers?: Record<string, string> } = {}) {
let url = `${MOBILE_REGISTRATION_ENDPOINT}${path}`
if(opts.params) {
const parameter = [] as string[]
for(const param in opts.params) {
parameter.push(param + '=' + urlencode(opts.params[param]))
}
console.log('parameter', opts.params, parameter)
// const params = urlencode(mobileRegisterEncrypt(parameter.join('&')))
// url += `?ENC=${params}`
url += `?${parameter.join('&')}`
}
if(!opts.headers) {
opts.headers = {}
}
opts.headers['User-Agent'] = MOBILE_USERAGENT
const response = await fetch(url, opts)
const text = await response.text()
try {
var json = JSON.parse(text)
} catch(error) {
throw text
}
if(!response.ok || json.reason) {
throw json
}
if(json.status && !['ok', 'sent'].includes(json.status)) {
throw json
}
return json as ExistsResponse
}
export interface ExistsResponse {
status: 'fail'
voice_length?: number
voice_wait?: number
sms_length?: number
sms_wait?: number
reason?: 'incorrect' | 'missing_param'
login?: string
flash_type?: number
ab_hash?: string
ab_key?: string
exp_cfg?: string
lid?: string
}

View File

@@ -1,12 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Boom } from '@hapi/boom'
import { promisify } from 'util'
import WebSocket from 'ws'
import { proto } from '../../WAProto'
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, DEFAULT_ORIGIN, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT } from '../Defaults'
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, MOBILE_NOISE_HEADER, NOISE_WA_HEADER } from '../Defaults'
import { DisconnectReason, SocketConfig } from '../Types'
import { addTransactionCapability, bindWaitForConnectionUpdate, configureSuccessfulPairing, Curve, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeNoiseHandler, printQRIfNecessaryListener, promiseTimeout } from '../Utils'
import { addTransactionCapability, bindWaitForConnectionUpdate, configureSuccessfulPairing, Curve, generateLoginNode, generateMdTagPrefix, generateMobileNode, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeNoiseHandler, printQRIfNecessaryListener, promiseTimeout } from '../Utils'
import { makeEventBuffer } from '../Utils/event-buffer'
import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary'
import { assertNodeErrorFree, BinaryNode, binaryNodeToString, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary'
import { MobileSocket } from './mobile-socket'
import { WebSocket } from './web-socket'
/**
* Connects to WA servers and performs:
@@ -14,37 +16,35 @@ import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild,
* - listen to messages and emit events
* - query phone connection
*/
export const makeSocket = ({
waWebSocketUrl,
connectTimeoutMs,
logger,
agent,
keepAliveIntervalMs,
version,
browser,
auth: authState,
printQRInTerminal,
defaultQueryTimeoutMs,
syncFullHistory,
transactionOpts,
qrTimeout,
options,
makeSignalRepository
}: SocketConfig) => {
const ws = new WebSocket(waWebSocketUrl, undefined, {
origin: DEFAULT_ORIGIN,
headers: options.headers as {},
handshakeTimeout: connectTimeoutMs,
timeout: connectTimeoutMs,
agent
})
export const makeSocket = (config: SocketConfig) => {
const {
connectTimeoutMs,
logger,
keepAliveIntervalMs,
browser,
auth: authState,
printQRInTerminal,
defaultQueryTimeoutMs,
transactionOpts,
qrTimeout,
makeSignalRepository,
} = config
config.mobile = config.mobile || config.auth.creds.registered
const ws = config.mobile ? new MobileSocket(config) : new WebSocket(config)
ws.setMaxListeners(0)
const ev = makeEventBuffer(logger)
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
const ephemeralKeyPair = Curve.generateKeyPair()
/** WA noise protocol wrapper */
const noise = makeNoiseHandler(ephemeralKeyPair, logger)
const noise = makeNoiseHandler({
keyPair: ephemeralKeyPair,
NOISE_HEADER: config.mobile ? MOBILE_NOISE_HEADER : NOISE_WA_HEADER,
mobile: config.mobile,
logger
})
const { creds } = authState
// add transaction capability
@@ -63,7 +63,7 @@ export const makeSocket = ({
const sendPromise = promisify<void>(ws.send)
/** send a raw buffer */
const sendRawMessage = async(data: Uint8Array | Buffer) => {
if(ws.readyState !== ws.OPEN) {
if(!ws.isOpen) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
}
@@ -84,7 +84,7 @@ export const makeSocket = ({
/** send a binary node */
const sendNode = (frame: BinaryNode) => {
if(logger.level === 'trace') {
logger.trace({ msgId: frame.attrs.id, fromMe: true, frame }, 'communication')
logger.trace(binaryNodeToString(frame), 'xml send')
}
const buff = encodeBinaryNode(frame)
@@ -101,7 +101,7 @@ export const makeSocket = ({
/** await the next incoming message */
const awaitNextMessage = async<T>(sendMsg?: Uint8Array) => {
if(ws.readyState !== ws.OPEN) {
if(!ws.isOpen) {
throw new Boom('Connection Closed', {
statusCode: DisconnectReason.connectionClosed
})
@@ -186,21 +186,21 @@ export const makeSocket = ({
}
helloMsg = proto.HandshakeMessage.fromObject(helloMsg)
logger.info({ browser, helloMsg }, 'connected to WA Web')
logger.info({ browser, helloMsg }, 'connected to WA')
const init = proto.HandshakeMessage.encode(helloMsg).finish()
const result = await awaitNextMessage<Uint8Array>(init)
const handshake = proto.HandshakeMessage.decode(result)
logger.trace({ handshake }, 'handshake recv from WA Web')
logger.trace({ handshake }, 'handshake recv from WA')
const keyEnc = noise.processHandshake(handshake, creds.noiseKey)
const config = { version, browser, syncFullHistory }
let node: proto.IClientPayload
if(!creds.me) {
if(config.mobile) {
node = generateMobileNode(config)
} else if(!creds.me) {
node = generateRegistrationNode(creds, config)
logger.info({ node }, 'not logged in, attempting registration...')
} else {
@@ -276,7 +276,7 @@ export const makeSocket = ({
const msgId = frame.attrs.id
if(logger.level === 'trace') {
logger.trace({ msgId, fromMe: false, frame }, 'communication')
logger.trace(binaryNodeToString(frame), 'recv xml')
}
/* Check if this is a response to a message we sent */
@@ -321,7 +321,7 @@ export const makeSocket = ({
ws.removeAllListeners('open')
ws.removeAllListeners('message')
if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
if(!ws.isClosed && !ws.isClosing) {
try {
ws.close()
} catch{ }
@@ -338,11 +338,11 @@ export const makeSocket = ({
}
const waitForSocketOpen = async() => {
if(ws.readyState === ws.OPEN) {
if(ws.isOpen) {
return
}
if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
if(ws.isClosed || ws.isClosing) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
}
@@ -375,7 +375,7 @@ export const makeSocket = ({
*/
if(diff > keepAliveIntervalMs + 5000) {
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
} else if(ws.readyState === ws.OPEN) {
} else if(ws.isOpen) {
// if its all good, send a keep alive request
query(
{
@@ -472,7 +472,7 @@ export const makeSocket = ({
let qrMs = qrTimeout || 60_000 // time to let a QR live
const genPairQR = () => {
if(ws.readyState !== ws.OPEN) {
if(!ws.isOpen) {
return
}

31
src/Socket/web-socket.ts Normal file
View File

@@ -0,0 +1,31 @@
import { WebSocket as WS } from 'ws'
import { DEFAULT_ORIGIN } from '../Defaults'
import { SocketConfig } from '../Types'
export class WebSocket extends WS {
constructor(public config: SocketConfig) {
super(config.waWebSocketUrl, undefined, {
origin: DEFAULT_ORIGIN,
headers: config.options.headers as Record<string, string>,
handshakeTimeout: config.connectTimeoutMs,
timeout: config.connectTimeoutMs,
agent: config.agent,
})
}
get isOpen() {
return this.readyState === WS.OPEN
}
get isClosed() {
return this.readyState === WS.CLOSED
}
get isClosing() {
return this.readyState === WS.CLOSING
}
get isConnecting() {
return this.readyState === WS.CONNECTING
}
}

View File

@@ -1,4 +1,5 @@
import type { proto } from '../../WAProto'
import { RegistrationOptions } from '../Socket/registration'
import type { Contact } from './Contact'
import type { MinimalMessage } from './Message'
@@ -58,6 +59,13 @@ export type AuthenticationCreds = SignalCreds & {
/** number of times history & app state has been synced */
accountSyncCounter: number
accountSettings: AccountSettings
// mobile creds
deviceId: string
phoneId: string
identityId: Buffer
registered: boolean
backupToken: Buffer
registration: RegistrationOptions
}
export type SignalDataTypeMap = {

View File

@@ -31,6 +31,8 @@ export type SocketConfig = {
defaultQueryTimeoutMs: number | undefined
/** ping-pong interval for WS connection */
keepAliveIntervalMs: number
/** should baileys use the mobile api instead of the multi device api */
mobile?: boolean
/** proxy agent */
agent?: Agent
/** pino logger */

View File

@@ -1,6 +1,7 @@
import { randomBytes } from 'crypto'
import NodeCache from 'node-cache'
import type { Logger } from 'pino'
import { v4 as uuidv4 } from 'uuid'
import { DEFAULT_CACHE_TTLS } from '../Defaults'
import type { AuthenticationCreds, CacheStore, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types'
import { Curve, signedKeyPair } from './crypto'
@@ -202,6 +203,13 @@ export const initAuthCreds = (): AuthenticationCreds => {
accountSyncCounter: 0,
accountSettings: {
unarchiveChats: false
}
},
// mobile creds
deviceId: Buffer.from(uuidv4().replace(/-/g, ''), 'hex').toString('base64url'),
phoneId: uuidv4(),
identityId: randomBytes(20),
registered: false,
backupToken: randomBytes(20),
registration: {} as RegistrationOptions
}
}

View File

@@ -107,6 +107,10 @@ export function sha256(buffer: Buffer) {
return createHash('sha256').update(buffer).digest()
}
export function md5(buffer: Buffer) {
return createHash('md5').update(buffer).digest()
}
// 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)

View File

@@ -1,7 +1,7 @@
import { Boom } from '@hapi/boom'
import { Logger } from 'pino'
import { proto } from '../../WAProto'
import { NOISE_MODE, NOISE_WA_HEADER, WA_CERT_DETAILS } from '../Defaults'
import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults'
import { KeyPair } from '../Types'
import { BinaryNode, decodeBinaryNode } from '../WABinary'
import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto'
@@ -13,10 +13,17 @@ const generateIV = (counter: number) => {
return new Uint8Array(iv)
}
export const makeNoiseHandler = (
{ public: publicKey, private: privateKey }: KeyPair,
export const makeNoiseHandler = ({
keyPair: { private: privateKey, public: publicKey },
NOISE_HEADER,
mobile,
logger,
}: {
keyPair: KeyPair
NOISE_HEADER: Uint8Array
mobile: boolean
logger: Logger
) => {
}) => {
logger = logger.child({ class: 'ns' })
const authenticate = (data: Uint8Array) => {
@@ -86,7 +93,7 @@ export const makeNoiseHandler = (
let inBytes = Buffer.alloc(0)
authenticate(NOISE_WA_HEADER)
authenticate(NOISE_HEADER)
authenticate(publicKey)
return {
@@ -103,12 +110,18 @@ export const makeNoiseHandler = (
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
const certDecoded = decrypt(serverHello!.payload!)
const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded)
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
if(mobile) {
const cert = proto.CertChain.NoiseCertificate.decode(certDecoded)
logger.debug(cert)
} else {
const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded)
if(issuerSerial !== WA_CERT_DETAILS.SERIAL) {
throw new Boom('certification match failed', { statusCode: 400 })
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
if(issuerSerial !== WA_CERT_DETAILS.SERIAL) {
throw new Boom('certification match failed', { statusCode: 400 })
}
}
const keyEnc = encrypt(noiseKey.public)
@@ -121,11 +134,11 @@ export const makeNoiseHandler = (
data = encrypt(data)
}
const introSize = sentIntro ? 0 : NOISE_WA_HEADER.length
const introSize = sentIntro ? 0 : NOISE_HEADER.length
const frame = Buffer.alloc(introSize + 3 + data.byteLength)
if(!sentIntro) {
frame.set(NOISE_WA_HEADER)
frame.set(NOISE_HEADER)
sentIntro = true
}

View File

@@ -8,26 +8,31 @@ import { Curve, hmacSign } from './crypto'
import { encodeBigEndian } from './generics'
import { createSignalIdentity } from './signal'
type ClientPayloadConfig = Pick<SocketConfig, 'version' | 'browser' | 'syncFullHistory'>
const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
const osVersion = config.mobile ? '15.3.1' : '0.1'
const version = config.mobile ? [2, 22, 24] : config.version
const device = config.mobile ? 'iPhone_7' : 'Desktop'
const manufacturer = config.mobile ? 'Apple' : ''
const platform = config.mobile ? proto.ClientPayload.UserAgent.Platform.IOS : proto.ClientPayload.UserAgent.Platform.WEB
const phoneId = config.mobile ? { phoneId: config.auth.creds.phoneId } : {}
const getUserAgent = ({ version }: ClientPayloadConfig): proto.ClientPayload.IUserAgent => {
const osVersion = '0.1'
return {
appVersion: {
primary: version[0],
secondary: version[1],
tertiary: version[2],
},
platform: proto.ClientPayload.UserAgent.Platform.WEB,
platform,
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
mcc: '000',
mnc: '000',
mcc: config.auth.creds.registration?.phoneNumberMobileCountryCode || '000',
mnc: config.auth.creds.registration?.phoneNumberMobileNetworkCode || '000',
osVersion: osVersion,
manufacturer: '',
device: 'Desktop',
manufacturer,
device,
osBuildNumber: osVersion,
localeLanguageIso6391: 'en',
localeCountryIso31661Alpha2: 'US',
...phoneId
}
}
@@ -36,7 +41,7 @@ const PLATFORM_MAP = {
'Windows': proto.ClientPayload.WebInfo.WebSubPlatform.WIN32
}
const getWebInfo = (config: ClientPayloadConfig): proto.ClientPayload.IWebInfo => {
const getWebInfo = (config: SocketConfig): proto.ClientPayload.IWebInfo => {
let webSubPlatform = proto.ClientPayload.WebInfo.WebSubPlatform.WEB_BROWSER
if(config.syncFullHistory && PLATFORM_MAP[config.browser[0]]) {
webSubPlatform = PLATFORM_MAP[config.browser[0]]
@@ -45,16 +50,43 @@ const getWebInfo = (config: ClientPayloadConfig): proto.ClientPayload.IWebInfo =
return { webSubPlatform }
}
const getClientPayload = (config: ClientPayloadConfig): proto.IClientPayload => {
return {
const getClientPayload = (config: SocketConfig) => {
const payload: proto.IClientPayload = {
connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN,
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
userAgent: getUserAgent(config),
webInfo: getWebInfo(config),
}
if(!config.mobile) {
payload.webInfo = getWebInfo(config)
}
return payload
}
export const generateLoginNode = (userJid: string, config: ClientPayloadConfig): proto.IClientPayload => {
export const generateMobileNode = (config: SocketConfig): proto.IClientPayload => {
if(!config.auth.creds) {
throw new Boom('No registration data found', { data: config })
}
const payload: proto.IClientPayload = {
...getClientPayload(config),
sessionId: Math.floor(Math.random() * 999999999 + 1),
shortConnect: true,
connectAttemptCount: 0,
device: 0,
dnsSource: {
appCached: false,
dnsMethod: proto.ClientPayload.DNSSource.DNSResolutionMethod.SYSTEM,
},
passive: false, // XMPP heartbeat setting (false: server actively pings) (true: client actively pings)
pushName: 'test',
username: Number(`${config.auth.creds.registration.phoneNumberCountryCode}${config.auth.creds.registration.phoneNumberNationalNumber}`),
}
return proto.ClientPayload.fromObject(payload)
}
export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => {
const { user, device } = jidDecode(userJid)!
const payload: proto.IClientPayload = {
...getClientPayload(config),
@@ -67,7 +99,7 @@ export const generateLoginNode = (userJid: string, config: ClientPayloadConfig):
export const generateRegistrationNode = (
{ registrationId, signedPreKey, signedIdentityKey }: SignalCreds,
config: ClientPayloadConfig
config: SocketConfig
) => {
// the app version needs to be md5 hashed
// and passed in

View File

@@ -87,4 +87,35 @@ function bufferToUInt(e: Uint8Array | Buffer, t: number) {
}
return a
}
const tabs = (n: number) => '\t'.repeat(n)
export function binaryNodeToString(node: BinaryNode | BinaryNode['content'], i = 0) {
if(!node) {
return node
}
if(typeof node === 'string') {
return tabs(i) + node
}
if(node instanceof Uint8Array) {
return tabs(i) + Buffer.from(node).toString('hex')
}
if(Array.isArray(node)) {
return node.map((x) => tabs(i + 1) + binaryNodeToString(x, i + 1)).join('\n')
}
const children = binaryNodeToString(node.content, i + 1)
const tag = `<${node.tag} ${Object.entries(node.attrs || {})
.filter(([, v]) => v !== undefined)
.map(([k, v]) => `${k}='${v}'`)
.join(' ')}`
const content: string = children ? `>\n${children}\n${tabs(i)}</${node.tag}>` : '/>'
return tag + content
}