diff --git a/Example/example.ts b/Example/example.ts index ec641c2..5b59de8 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -1,5 +1,4 @@ import { Boom } from '@hapi/boom' -import parsePhoneNumber from 'libphonenumber-js' import NodeCache from 'node-cache' import readline from 'readline' import makeWASocket, { AnyMessageContent, delay, DisconnectReason, fetchLatestBaileysVersion, getAggregateVotesInPollMessage, makeCacheableSignalKeyStore, makeInMemoryStore, PHONENUMBER_MCC, proto, useMultiFileAuthState, WAMessageContent, WAMessageKey } from '../src' @@ -65,7 +64,8 @@ const startSock = async() => { } const phoneNumber = await question('Please enter your mobile phone number:\n') - await sock.requestPairingCode(phoneNumber) + const code = await sock.requestPairingCode(phoneNumber) + console.log(`Pairing code: ${code}`) } // If mobile was chosen, ask for the code @@ -76,7 +76,8 @@ const startSock = async() => { registration.phoneNumber = await question('Please enter your mobile phone number:\n') } - const phoneNumber = parsePhoneNumber(registration!.phoneNumber) + const libPhonenumber = await import("libphonenumber-js") + const phoneNumber = libPhonenumber.parsePhoneNumber(registration!.phoneNumber) if(!phoneNumber?.isValid()) { throw new Error('Invalid phone number: ' + registration!.phoneNumber) } diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 6e228e4..aae9c12 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -2,16 +2,17 @@ import { Boom } from '@hapi/boom' import { randomBytes } from 'crypto' import NodeCache from 'node-cache' -import { getBinaryNodeChildBuffer } from '../../lib' import { proto } from '../../WAProto' import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults' import { MessageReceiptType, MessageRelayOptions, MessageUserReceipt, SocketConfig, WACallEvent, WAMessageKey, WAMessageStatus, WAMessageStubType, WAPatchName } from '../Types' import { + aesDecryptCTR, aesEncryptGCM, Curve, decodeMediaRetryNode, decryptMessageNode, - delay, derivePairingKey, + delay, + derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, getCallStatusFromNode, @@ -24,7 +25,19 @@ import { } from '../Utils' import { cleanMessage } from '../Utils' import { makeMutex } from '../Utils/make-mutex' -import { areJidsSameUser, BinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary' +import { + areJidsSameUser, + BinaryNode, + getAllBinaryNodeChildren, + getBinaryNodeChild, + getBinaryNodeChildBuffer, + getBinaryNodeChildren, + isJidGroup, + isJidUser, + jidDecode, + jidNormalizedUser, + S_WHATSAPP_NET +} from '../WABinary' import { extractGroupMetadata } from './groups' import { makeMessagesSocket } from './messages-send' @@ -387,22 +400,21 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_ref')) const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub')) const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub')) - const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped) - const companionSharedKey = Curve.sharedKey(codePairingPublicKey, authState.creds.advKeyPair.private) + const codePairingPublicKey = decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped) + const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey) const random = randomBytes(32) const linkCodeSalt = randomBytes(32) const linkCodePairingExpanded = hkdf(companionSharedKey, 32, { salt: linkCodeSalt, info: 'link_code_pairing_key_bundle_encryption_key' }) - const encryptPayload = Buffer.concat([Buffer.from(authState.creds.signedIdentityKey.public), Buffer.from(primaryIdentityPublicKey), random]) + const encryptPayload = Buffer.concat([Buffer.from(authState.creds.signedIdentityKey.public), primaryIdentityPublicKey, random]) const encryptIv = randomBytes(12) const encrypted = aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0)) const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted]) - const identitySharedKey = Curve.sharedKey(primaryIdentityPublicKey, authState.creds.signedIdentityKey.private) + const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey) const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random]) - authState.creds.advKeyPair.public = hkdf(identityPayload, 32, { info: 'adv_secret' }) - authState.creds.advKeyPair.private = Buffer.alloc(0) + authState.creds.advSecretKey = hkdf(identityPayload, 32, { info: 'adv_secret' }).toString('base64') await sendNode({ tag: 'iq', attrs: { @@ -446,18 +458,13 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } } - async function decipherLinkPublicKey(data: Uint8Array | Buffer) { + function decipherLinkPublicKey(data: Uint8Array | Buffer) { const buffer = toRequiredBuffer(data) const salt = buffer.slice(0, 32) - const secretKey = await derivePairingKey(authState.creds.pairingCode!, salt) + const secretKey = derivePairingCodeKey(authState.creds.pairingCode!, salt) const iv = buffer.slice(32, 48) const payload = buffer.slice(48, 80) - const result = await crypto.subtle.decrypt({ - name: 'AES-CTR', - length: 64, - counter: iv - }, secretKey, payload) - return Buffer.from(result) + return aesDecryptCTR(payload, secretKey, iv) } function toRequiredBuffer(data: Uint8Array | Buffer | undefined) { diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index 4b7e4c7..e599daa 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -2,7 +2,6 @@ import { Boom } from '@hapi/boom' import { randomBytes } from 'crypto' import { URL } from 'url' import { promisify } from 'util' -import { getBinaryNodeChildBuffer } from '../../lib' import { proto } from '../../WAProto' import { DEF_CALLBACK_PREFIX, @@ -16,18 +15,20 @@ import { } from '../Defaults' import { DisconnectReason, SocketConfig } from '../Types' import { - addTransactionCapability, aesEncryptGCM, + addTransactionCapability, + aesEncryptCTR, bindWaitForConnectionUpdate, bytesToCrockford, configureSuccessfulPairing, - Curve, derivePairingKey, + Curve, + derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateMobileNode, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, - getNextPreKeysNode, hkdf, + getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, printQRIfNecessaryListener, @@ -481,7 +482,8 @@ export const makeSocket = (config: SocketConfig) => { end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut })) } - const requestPairingCode = async(phoneNumber: string) => { + const requestPairingCode = async(phoneNumber: string): Promise => { + authState.creds.pairingCode = bytesToCrockford(randomBytes(5)) authState.creds.me = { id: jidEncode(phoneNumber, 's.whatsapp.net'), name: '~' @@ -507,7 +509,7 @@ export const makeSocket = (config: SocketConfig) => { { tag: 'link_code_pairing_wrapped_companion_ephemeral_pub', attrs: {}, - content: await generatePairingKey(randomBytes(32)) + content: await generatePairingKey() }, { tag: 'companion_server_auth_key_pub', @@ -533,18 +535,15 @@ export const makeSocket = (config: SocketConfig) => { } ] }) + return authState.creds.pairingCode } - async function generatePairingKey(salt: Buffer) { - authState.creds.pairingCode = bytesToCrockford(randomBytes(5)) - const key = await derivePairingKey(authState.creds.pairingCode, salt) + async function generatePairingKey() { + const salt = randomBytes(32) const randomIv = randomBytes(16) - const ciphered = await crypto.subtle.encrypt({ - name: 'AES-CTR', - length: 64, - counter: randomIv - }, key, authState.creds.advKeyPair.public) - return Buffer.concat([salt, randomIv, Buffer.from(ciphered)]) + const key = derivePairingCodeKey(authState.creds.pairingCode!, salt) + const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv) + return Buffer.concat([salt, randomIv, ciphered]) } ws.on('message', onMessageRecieved) @@ -576,7 +575,7 @@ export const makeSocket = (config: SocketConfig) => { const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref') const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64') const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64') - const advB64 = Buffer.from(creds.advKeyPair.public).toString('base64') + const advB64 = creds.advSecretKey let qrMs = qrTimeout || 60_000 // time to let a QR live const genPairQR = () => { diff --git a/src/Types/Auth.ts b/src/Types/Auth.ts index 38e7c25..2b853a2 100644 --- a/src/Types/Auth.ts +++ b/src/Types/Auth.ts @@ -43,7 +43,8 @@ export type AccountSettings = { export type AuthenticationCreds = SignalCreds & { readonly noiseKey: KeyPair - advKeyPair: KeyPair + readonly pairingEphemeralKeyPair: KeyPair + advSecretKey: string me?: Contact account?: proto.IADVSignedDeviceIdentity @@ -66,7 +67,6 @@ export type AuthenticationCreds = SignalCreds & { registered: boolean backupToken: Buffer registration: RegistrationOptions - pairingCode: string | undefined } diff --git a/src/Utils/auth-utils.ts b/src/Utils/auth-utils.ts index 69c5b82..4424e9e 100644 --- a/src/Utils/auth-utils.ts +++ b/src/Utils/auth-utils.ts @@ -196,10 +196,11 @@ export const initAuthCreds = (): AuthenticationCreds => { const identityKey = Curve.generateKeyPair() return { noiseKey: Curve.generateKeyPair(), + pairingEphemeralKeyPair: Curve.generateKeyPair(), signedIdentityKey: identityKey, signedPreKey: signedKeyPair(identityKey, 1), registrationId: generateRegistrationId(), - advKeyPair: Curve.generateKeyPair(), + advSecretKey: randomBytes(32).toString('base64'), processedHistoryMessages: [], nextPreKeyId: 1, firstUnuploadedPreKeyId: 1, @@ -214,6 +215,6 @@ export const initAuthCreds = (): AuthenticationCreds => { registered: false, backupToken: randomBytes(20), registration: {} as never, - pairingCode: undefined + pairingCode: undefined, } } \ No newline at end of file diff --git a/src/Utils/crypto.ts b/src/Utils/crypto.ts index 46c020f..079510a 100644 --- a/src/Utils/crypto.ts +++ b/src/Utils/crypto.ts @@ -1,8 +1,8 @@ -import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto' +import {createCipheriv, createDecipheriv, createHash, createHmac, pbkdf2Sync, randomBytes} from 'crypto' import HKDF from 'futoin-hkdf' import * as libsignal from 'libsignal' -import { KEY_BUNDLE_TYPE } from '../Defaults' -import { KeyPair } from '../Types' +import {KEY_BUNDLE_TYPE} from '../Defaults' +import {KeyPair} from '../Types' /** prefix version byte to the pub keys, required for some curve crypto functions */ export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) => ( @@ -74,6 +74,16 @@ export function aesDecryptGCM(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8 return Buffer.concat([ decipher.update(enc), decipher.final() ]) } +export function aesEncryptCTR(plaintext: Uint8Array, key: Uint8Array, iv: Uint8Array) { + const cipher = createCipheriv('aes-256-ctr', key, iv) + return Buffer.concat([cipher.update(plaintext), cipher.final()]) +} + +export function aesDecryptCTR(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8Array) { + const decipher = createDecipheriv('aes-256-ctr', key, iv) + return Buffer.concat([decipher.update(ciphertext), decipher.final()]) +} + /** 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)) @@ -114,4 +124,8 @@ export function md5(buffer: Buffer) { // 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) +} + +export function derivePairingCodeKey(pairingCode: string, salt: Buffer) { + return pbkdf2Sync(pairingCode, salt, 2 << 16, 32, 'sha256') } \ No newline at end of file diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index 2fda427..2438682 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -411,31 +411,4 @@ export function bytesToCrockford(buffer: Buffer): string { } return crockford.join('') -} - -export async function derivePairingKey(pairingCode: string, salt: Buffer) { - const encoded = new TextEncoder().encode(pairingCode) - const cryptoKey = await crypto.subtle.importKey( - 'raw', - encoded, - { - name: 'PBKDF2' - }, - !1, - [ - 'deriveKey' - ] - ) - return await crypto.subtle.deriveKey({ - name: 'PBKDF2', - hash: 'SHA-256', - salt: salt, - iterations: 2 << 16 - }, cryptoKey, { - name: 'AES-CTR', - length: 256 - }, false, [ - 'encrypt', - 'decrypt' - ]) } \ No newline at end of file diff --git a/src/Utils/validate-connection.ts b/src/Utils/validate-connection.ts index ffd2e5a..83e1838 100644 --- a/src/Utils/validate-connection.ts +++ b/src/Utils/validate-connection.ts @@ -117,7 +117,7 @@ export const generateRegistrationNode = ( export const configureSuccessfulPairing = ( stanza: BinaryNode, - { advKeyPair, signedIdentityKey, signalIdentities }: Pick + { advSecretKey, signedIdentityKey, signalIdentities }: Pick ) => { const msgId = stanza.attrs.id @@ -137,7 +137,7 @@ export const configureSuccessfulPairing = ( const { details, hmac } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content as Buffer) // check HMAC matches - const advSign = hmacSign(details, advKeyPair.public) + const advSign = hmacSign(details, Buffer.from(advSecretKey, 'base64')) if(Buffer.compare(hmac, advSign) !== 0) { throw new Boom('Invalid account signature') } @@ -208,8 +208,7 @@ export const encodeSignedDeviceIdentity = ( account.accountSignatureKey = null } - const accountEnc = proto.ADVSignedDeviceIdentity + return proto.ADVSignedDeviceIdentity .encode(account) .finish() - return accountEnc } \ No newline at end of file