From 3d0704a317c1441789f0c995fc5748791167048f Mon Sep 17 00:00:00 2001 From: Adhiraj Singh Date: Sat, 20 Nov 2021 16:21:56 +0530 Subject: [PATCH] feat: cleaner auth state management 1. removes the "auth-state.update" event since it was bloated and fairly unnecessary 2. adds "creds.update" event that only sends the updated properties of the auth credentials 3. ensure the auth creds are not mutated anywhere, but only with the "creds.update" event 4. Separates the signal credentials from the auth credentials (kinda in progress) 5. in memory key store requires a save function to let one know when to save the keys !BREAKING_CHANGE --- Example/example.ts | 38 ++------- README.md | 51 +++--------- src/Defaults/index.ts | 2 +- src/Socket/chats.ts | 14 ++-- src/Socket/messages-recv.ts | 9 +-- src/Socket/messages-send.ts | 3 +- src/Socket/socket.ts | 43 +++++++--- src/Types/Auth.ts | 21 +++-- src/Types/index.ts | 6 +- src/Utils/auth-utils.ts | 133 +++++++++++++++++++++++++++++++ src/Utils/index.ts | 3 +- src/Utils/signal.ts | 20 ++--- src/Utils/validate-connection.ts | 87 +------------------- 13 files changed, 229 insertions(+), 201 deletions(-) create mode 100644 src/Utils/auth-utils.ts diff --git a/Example/example.ts b/Example/example.ts index 2f9832f..d3e48d8 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -1,41 +1,16 @@ -import { readFileSync, writeFileSync } from "fs" import P from "pino" import { Boom } from "@hapi/boom" -import makeWASocket, { AuthenticationState, DisconnectReason, AnyMessageContent, BufferJSON, initInMemoryKeyStore, delay } from '../src' +import makeWASocket, { DisconnectReason, AnyMessageContent, delay, useSingleFileAuthState } from '../src' -// load authentication state from a file -const loadState = () => { - let state: AuthenticationState | undefined = undefined - try { - const value = JSON.parse( - readFileSync('./auth_info_multi.json', { encoding: 'utf-8' }), - BufferJSON.reviver - ) - state = { - creds: value.creds, - // stores pre-keys, session & other keys in a JSON object - // we deserialize it here - keys: initInMemoryKeyStore(value.keys) - } - } catch{ } - return state -} -// save the authentication state to a file -const saveState = (state: any | undefined) => { - console.log('saving auth state') - writeFileSync( - './auth_info_multi.json', - // BufferJSON replacer utility saves buffers nicely - JSON.stringify(state, BufferJSON.replacer, 2) - ) -} +const { state, saveState } = useSingleFileAuthState('./auth_info_multi.json') // start a connection const startSock = () => { + const sock = makeWASocket({ logger: P({ level: 'trace' }), printQRInTerminal: true, - auth: loadState() + auth: state }) const sendMessageWTyping = async(msg: AnyMessageContent, jid: string) => { @@ -80,9 +55,8 @@ const startSock = () => { console.log('connection update', update) }) - // listen for when the auth state is updated - // it is imperative you save this data, it affects the signing keys you need to have conversations - sock.ev.on('auth-state.update', () => saveState(sock.authState)) + // listen for when the auth credentials is updated + sock.ev.on('creds.update', saveState) return sock } diff --git a/README.md b/README.md index 09ad0d2..b97fd3e 100644 --- a/README.md +++ b/README.md @@ -117,49 +117,22 @@ type SocketConfig = { You obviously don't want to keep scanning the QR code every time you want to connect. -So, you can save the credentials to log back in via: +So, you can load the credentials to log back in: ``` ts -import makeWASocket, { BufferJSON } from '@adiwajshing/baileys-md' +import makeWASocket, { BufferJSON, useSingleFileAuthState } from '@adiwajshing/baileys-md' import * as fs from 'fs' -// will initialize a default in-memory auth session -const conn = makeSocket() +// utility function to help save the auth state in a single file +// it's utility ends at demos -- as re-writing a large file over and over again is very inefficient +const { state, saveState } = useSingleFileAuthState('./auth_info_multi.json') +// will use the given state to connect +// so if valid credentials are available -- it'll connect without QR +const conn = makeSocket({ auth: state }) // this will be called as soon as the credentials are updated -conn.ev.on ('auth-state.update', () => { - // save credentials whenever updated - console.log (`credentials updated!`) - const authInfo = conn.authState // get all the auth info we need to restore this session - // save this info to a file - fs.writeFileSync( - './auth_info.json', - JSON.stringify(authInfo, BufferJSON.replacer, 2) - ) -}) +conn.ev.on ('creds.update', saveState) ``` -Then, to restore a session: -``` ts -import makeWASocket, { BufferJSON, initInMemoryKeyStore } from '@adiwajshing/baileys-md' -import * as fs from 'fs' - -const authJSON = JSON.parse( - fs.readFileSync( - './auth_info.json', - { encoding: 'utf-8' } - ), - BufferJSON.reviver -) -const auth = { - creds: authJSON.creds, - // stores pre-keys, session & other keys in a JSON object - // we deserialize it here - keys: initInMemoryKeyStore(authJSON.keys) -} -const conn = makeWASocket(auth) -// yay will connect without scanning QR -``` - -**Note**: Upon every successive connection, the auth state can update part of the stored credentials. It will also update when a message is received/sent due to signal sessions needing updating. Whenever that happens, the `auth-state.update` event is fired uploaded, and you must update your saved credentials upon receiving the event. Not doing so will prevent your messages from reaching the recipient & other unexpected consequences. +**Note**: When a message is received/sent, due to signal sessions needing updating, the auth keys (`authState.keys`) will update. Whenever that happens, you must save the updated keys. Not doing so will prevent your messages from reaching the recipient & other unexpected consequences. The `useSingleFileAuthState` function automatically takes care of that, but for any other serious implementation -- you will need to be very careful with the key state management. ## Listening to Connection Updates @@ -196,8 +169,8 @@ The events are typed up in a type map, as mentioned here: export type BaileysEventMap = { /** connection state has been updated -- WS closed, opened, connecting etc. */ 'connection.update': Partial - /** auth state updated -- some pre keys, or identity keys etc. */ - 'auth-state.update': AuthenticationState + /** auth credentials updated -- some pre key state, device ID etc. */ + 'creds.update': Partial /** set chats (history sync), messages are reverse chronologically sorted */ 'chats.set': { chats: Chat[], messages: WAMessage[] } /** upsert chats */ diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index 464369d..298d0aa 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -26,7 +26,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = { keepAliveIntervalMs: 25_000, logger: P().child({ class: 'baileys' }), printQRInTerminal: false, - emitOwnEvents: true + emitOwnEvents: true, } export const MEDIA_PATH_MAP: { [T in MediaType]: string } = { diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index 2329ad1..e4f63ff 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -219,7 +219,6 @@ export const makeChatsSocket = (config: SocketConfig) => { processSyncActions(newMutations) } } - ev.emit('auth-state.update', authState) } /** @@ -245,11 +244,12 @@ export const makeChatsSocket = (config: SocketConfig) => { } const sendPresenceUpdate = async(type: WAPresence, toJid?: string) => { + const me = authState.creds.me! if(type === 'available' || type === 'unavailable') { await sendNode({ tag: 'presence', attrs: { - name: authState.creds.me!.name, + name: me!.name, type } }) @@ -257,7 +257,7 @@ export const makeChatsSocket = (config: SocketConfig) => { await sendNode({ tag: 'chatstate', attrs: { - from: authState.creds.me!.id!, + from: me!.id!, to: toJid, }, content: [ @@ -344,8 +344,11 @@ export const makeChatsSocket = (config: SocketConfig) => { name: action.contactAction!.fullName } } else if(action?.pushNameSetting) { - authState.creds.me!.name = action?.pushNameSetting?.name! - ev.emit('auth-state.update', authState) + const me = { + ...authState.creds.me!, + name: action?.pushNameSetting?.name! + } + ev.emit('creds.update', { me }) } else { logger.warn({ action, id }, 'unprocessable update') } @@ -420,7 +423,6 @@ export const makeChatsSocket = (config: SocketConfig) => { await query(node) await authState.keys.setAppStateSyncVersion(name, state) - ev.emit('auth-state.update', authState) if(config.emitOwnEvents) { processSyncActions(result.newMutations) diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 6db257d..9ad9aee 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -158,15 +158,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { logger.info({ with: message.key.remoteJid! }, 'shared key') break case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE: + let newAppStateSyncKeyId = '' for(const { keyData, keyId } of protocolMsg.appStateSyncKeyShare!.keys || []) { const str = Buffer.from(keyId.keyId!).toString('base64') logger.info({ str }, 'injecting new app state sync key') await authState.keys.setAppStateSyncKey(str, keyData) - authState.creds.myAppStateKeyId = str + newAppStateSyncKeyId = str } - ev.emit('auth-state.update', authState) + ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId }) resyncMainAppState() break @@ -359,7 +360,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } // if there were some successful decryptions if(dec.successes.length) { - ev.emit('auth-state.update', authState) // send message receipt let recpAttrs: { [_: string]: any } if(isMe) { @@ -506,8 +506,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { contactNameUpdates[jid] = msg.pushName // update our pushname too if(msg.key.fromMe && authState.creds.me?.name !== msg.pushName) { - authState.creds.me!.name = msg.pushName - ev.emit('auth-state.update', authState) + ev.emit('creds.update', { me: { ...authState.creds.me!, name: msg.pushName! } }) } } diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 7046e0f..56eb181 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -259,7 +259,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { const destinationJid = jidEncode(user, isGroup ? 'g.us' : 's.whatsapp.net') if(isGroup) { - const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(destinationJid, encodedMsg, authState) + const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(destinationJid, encodedMsg, authState.creds.me!.id, authState) let groupData = cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined if(!groupData) groupData = await groupMetadata(jid) @@ -375,7 +375,6 @@ export const makeMessagesSocket = (config: SocketConfig) => { await sendNode(stanza) - ev.emit('auth-state.update', authState) return msgId } diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index 06dd674..a3b4f1d 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -4,10 +4,11 @@ import { promisify } from "util" import WebSocket from "ws" import { randomBytes } from 'crypto' import { proto } from '../../WAProto' -import { DisconnectReason, SocketConfig, BaileysEventEmitter, ConnectionState } from "../Types" -import { Curve, initAuthState, generateRegistrationNode, configureSuccessfulPairing, generateLoginNode, encodeBigEndian, promiseTimeout, generateOrGetPreKeys, xmppSignedPreKey, xmppPreKey, getPreKeys, makeNoiseHandler } from "../Utils" +import { DisconnectReason, SocketConfig, BaileysEventEmitter, ConnectionState, AuthenticationCreds } from "../Types" +import { Curve, generateRegistrationNode, configureSuccessfulPairing, generateLoginNode, encodeBigEndian, promiseTimeout, generateOrGetPreKeys, xmppSignedPreKey, xmppPreKey, getPreKeys, makeNoiseHandler, useSingleFileAuthState } from "../Utils" import { DEFAULT_ORIGIN, DEF_TAG_PREFIX, DEF_CALLBACK_PREFIX, KEY_BUNDLE_TYPE } from "../Defaults" import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, S_WHATSAPP_NET, getBinaryNodeChild } from '../WABinary' + /** * Connects to WA servers and performs: * - simple queries (no retry mechanism, wait for connection establishment) @@ -39,13 +40,22 @@ export const makeSocket = ({ } }) ws.setMaxListeners(0) + const ev = new EventEmitter() as BaileysEventEmitter /** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */ const ephemeralKeyPair = Curve.generateKeyPair() /** WA noise protocol wrapper */ const noise = makeNoiseHandler(ephemeralKeyPair) - const authState = initialAuthState || initAuthState() + let authState = initialAuthState + if(!authState) { + authState = useSingleFileAuthState('./auth-info-multi.json').state + + logger.warn(` + Baileys just created a single file state for your credentials. + This will not be supported soon. + Please pass the credentials in the config itself + `) + } const { creds } = authState - const ev = new EventEmitter() as BaileysEventEmitter let lastDateRecv: Date let epoch = 0 @@ -174,11 +184,16 @@ export const makeSocket = ({ } /** get some pre-keys and do something with them */ const assertingPreKeys = async(range: number, execute: (keys: { [_: number]: any }) => Promise) => { - const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(authState, range) - - creds.serverHasPreKeys = true - creds.nextPreKeyId = Math.max(lastPreKeyId+1, creds.nextPreKeyId) - creds.firstUnuploadedPreKeyId = Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId+1) + const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(authState.creds, range) + + const update: Partial = { + nextPreKeyId: Math.max(lastPreKeyId+1, creds.nextPreKeyId), + firstUnuploadedPreKeyId: Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId+1) + } + if(!creds.serverHasPreKeys) { + update.serverHasPreKeys = true + } + await Promise.all( Object.keys(newPreKeys).map(k => authState.keys.setPreKey(+k, newPreKeys[+k])) ) @@ -186,7 +201,7 @@ export const makeSocket = ({ const preKeys = await getPreKeys(authState.keys, preKeysRange[0], preKeysRange[0] + preKeysRange[1]) await execute(preKeys) - ev.emit('auth-state.update', authState) + ev.emit('creds.update', update) } /** generates and uploads a set of pre-keys */ const uploadPreKeys = async() => { @@ -464,10 +479,10 @@ export const makeSocket = ({ throw new Boom('Authentication failed', { statusCode: +(value.attrs.code || 500) }) } } - Object.assign(creds, updatedCreds) - logger.info({ jid: creds.me!.id }, 'registered connection, restart server') - ev.emit('auth-state.update', authState) + logger.info({ jid: updatedCreds.me!.id }, 'registered connection, restart server') + + ev.emit('creds.update', updatedCreds) ev.emit('connection.update', { isNewLogin: true, qr: undefined }) end(new Boom('Restart Required', { statusCode: DisconnectReason.restartRequired })) @@ -512,6 +527,8 @@ export const makeSocket = ({ process.nextTick(() => { ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined }) }) + // update credentials when required + ev.on('creds.update', update => Object.assign(creds, update)) return { ws, diff --git a/src/Types/Auth.ts b/src/Types/Auth.ts index 0e47fd8..5ff95fe 100644 --- a/src/Types/Auth.ts +++ b/src/Types/Auth.ts @@ -16,12 +16,16 @@ export type SignalIdentity = { export type LTHashState = { version: number, hash: Buffer, mutations: ChatMutation[] } -export type AuthenticationCreds = { - noiseKey: KeyPair - signedIdentityKey: KeyPair - signedPreKey: SignedKeyPair - registrationId: number - advSecretKey: string +export type SignalCreds = { + readonly signedIdentityKey: KeyPair + readonly signedPreKey: SignedKeyPair + readonly registrationId: number +} + +export type AuthenticationCreds = SignalCreds & { + readonly noiseKey: KeyPair + readonly advSecretKey: string + me?: Contact account?: proto.IADVSignedDeviceIdentity signalIdentities?: SignalIdentity[] @@ -49,6 +53,11 @@ export type SignalKeyStore = { setAppStateSyncVersion: (id: WAPatchName, item: LTHashState) => Awaitable } +export type SignalAuthState = { + creds: SignalCreds + keys: SignalKeyStore +} + export type AuthenticationState = { creds: AuthenticationCreds keys: SignalKeyStore diff --git a/src/Types/index.ts b/src/Types/index.ts index a8e3f77..1c69c2d 100644 --- a/src/Types/index.ts +++ b/src/Types/index.ts @@ -11,7 +11,7 @@ import type { Logger } from "pino" import type { URL } from "url" import type NodeCache from 'node-cache' -import { AuthenticationState } from './Auth' +import { AuthenticationState, AuthenticationCreds } from './Auth' import { Chat, PresenceData } from './Chat' import { Contact } from './Contact' import { ConnectionState } from './State' @@ -96,8 +96,8 @@ export type CurveKeyPair = { private: Uint8Array; public: Uint8Array } export type BaileysEventMap = { /** connection state has been updated -- WS closed, opened, connecting etc. */ 'connection.update': Partial - /** auth state updated -- some pre keys, or identity keys etc. */ - 'auth-state.update': AuthenticationState + /** credentials updated -- some metadata, keys or something */ + 'creds.update': Partial /** set chats (history sync), messages are reverse chronologically sorted */ 'chats.set': { chats: Chat[], messages: WAMessage[] } /** upsert chats */ diff --git a/src/Utils/auth-utils.ts b/src/Utils/auth-utils.ts new file mode 100644 index 0000000..16e6b4a --- /dev/null +++ b/src/Utils/auth-utils.ts @@ -0,0 +1,133 @@ +import { randomBytes } from 'crypto' +import { proto } from '../../WAProto' +import type { SignalKeyStore, AuthenticationCreds, KeyPair, LTHashState, AuthenticationState } from "../Types" +import { Curve, signedKeyPair } from './crypto' +import { generateRegistrationId, BufferJSON } from './generics' + +export const initInMemoryKeyStore = ( + { preKeys, sessions, senderKeys, appStateSyncKeys, appStateVersions }: { + preKeys?: { [k: number]: KeyPair }, + sessions?: { [k: string]: any }, + senderKeys?: { [k: string]: any } + appStateSyncKeys?: { [k: string]: proto.IAppStateSyncKeyData }, + appStateVersions?: { [k: string]: LTHashState }, + } = { }, + save: (data: any) => void +) => { + + preKeys = preKeys || { } + sessions = sessions || { } + senderKeys = senderKeys || { } + appStateSyncKeys = appStateSyncKeys || { } + appStateVersions = appStateVersions || { } + + const keyData = { + preKeys, + sessions, + senderKeys, + appStateSyncKeys, + appStateVersions, + } + + return { + ...keyData, + getPreKey: keyId => preKeys[keyId], + setPreKey: (keyId, pair) => { + if(pair) preKeys[keyId] = pair + else delete preKeys[keyId] + + save(keyData) + }, + getSession: id => sessions[id], + setSession: (id, item) => { + if(item) sessions[id] = item + else delete sessions[id] + + save(keyData) + }, + getSenderKey: id => { + return senderKeys[id] + }, + setSenderKey: (id, item) => { + if(item) senderKeys[id] = item + else delete senderKeys[id] + + save(keyData) + }, + getAppStateSyncKey: id => { + const obj = appStateSyncKeys[id] + if(obj) { + return proto.AppStateSyncKeyData.fromObject(obj) + } + }, + setAppStateSyncKey: (id, item) => { + if(item) appStateSyncKeys[id] = item + else delete appStateSyncKeys[id] + + save(keyData) + }, + getAppStateSyncVersion: id => { + const obj = appStateVersions[id] + if(obj) { + return obj + } + }, + setAppStateSyncVersion: (id, item) => { + if(item) appStateVersions[id] = item + else delete appStateVersions[id] + + save(keyData) + } + } as SignalKeyStore +} + +export const initAuthCreds = (): AuthenticationCreds => { + const identityKey = Curve.generateKeyPair() + return { + noiseKey: Curve.generateKeyPair(), + signedIdentityKey: identityKey, + signedPreKey: signedKeyPair(identityKey, 1), + registrationId: generateRegistrationId(), + advSecretKey: randomBytes(32).toString('base64'), + + nextPreKeyId: 1, + firstUnuploadedPreKeyId: 1, + serverHasPreKeys: false + } +} +/** stores the full authentication state in a single JSON file */ +export const useSingleFileAuthState = (filename: 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: AuthenticationState = undefined + + // save the authentication state to a file + const saveState = () => { + console.log('saving auth state') + writeFileSync( + filename, + // BufferJSON replacer utility saves buffers nicely + JSON.stringify(state, BufferJSON.replacer, 2) + ) + } + + if(existsSync(filename)) { + const { creds, keys } = JSON.parse( + readFileSync(filename, { encoding: 'utf-8' }), + BufferJSON.reviver + ) + state = { + creds: creds, + // stores pre-keys, session & other keys in a JSON object + // we deserialize it here + keys: initInMemoryKeyStore(keys, saveState) + } + } else { + const creds = initAuthCreds() + const keys = initInMemoryKeyStore({ }, saveState) + state = { creds: creds, keys: keys } + } + + return { state, saveState } +} \ No newline at end of file diff --git a/src/Utils/index.ts b/src/Utils/index.ts index 640c9f2..a4c41fe 100644 --- a/src/Utils/index.ts +++ b/src/Utils/index.ts @@ -8,4 +8,5 @@ export * from './signal' export * from './noise-handler' export * from './history' export * from './chat-utils' -export * from './lt-hash' \ No newline at end of file +export * from './lt-hash' +export * from './auth-utils' \ No newline at end of file diff --git a/src/Utils/signal.ts b/src/Utils/signal.ts index c7c90c8..7caa4d7 100644 --- a/src/Utils/signal.ts +++ b/src/Utils/signal.ts @@ -2,7 +2,7 @@ import * as libsignal from 'libsignal' 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 { SignalIdentity, SignalKeyStore, SignedKeyPair, KeyPair, SignalAuthState, AuthenticationCreds } from "../Types/Auth" import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildUInt, jidDecode, JidWithDevice } from "../WABinary" import { proto } from "../../WAProto" @@ -42,7 +42,7 @@ export const getPreKeys = async({ getPreKey }: SignalKeyStore, min: number, limi return dict } -export const generateOrGetPreKeys = ({ creds }: AuthenticationState, range: number) => { +export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number) => { const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId const remaining = range - avaliable const lastPreKeyId = creds.nextPreKeyId + remaining - 1 @@ -83,7 +83,7 @@ export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => ( } ) -export const signalStorage = ({ creds, keys }: AuthenticationState) => ({ +export const signalStorage = ({ creds, keys }: SignalAuthState) => ({ loadSession: async id => { const sess = await keys.getSession(id) if(sess) { @@ -132,7 +132,7 @@ export const signalStorage = ({ creds, keys }: AuthenticationState) => ({ } }) -export const decryptGroupSignalProto = (group: string, user: string, msg: Buffer | Uint8Array, auth: AuthenticationState) => { +export const decryptGroupSignalProto = (group: string, user: string, msg: Buffer | Uint8Array, auth: SignalAuthState) => { const senderName = jidToSignalSenderKeyName(group, user) const cipher = new GroupCipher(signalStorage(auth), senderName) @@ -142,7 +142,7 @@ export const decryptGroupSignalProto = (group: string, user: string, msg: Buffer export const processSenderKeyMessage = async( authorJid: string, item: proto.ISenderKeyDistributionMessage, - auth: AuthenticationState + auth: SignalAuthState ) => { const builder = new GroupSessionBuilder(signalStorage(auth)) const senderName = jidToSignalSenderKeyName(item.groupId, authorJid) @@ -156,7 +156,7 @@ export const processSenderKeyMessage = async( await builder.process(senderName, senderMsg) } -export const decryptSignalProto = async(user: string, type: 'pkmsg' | 'msg', msg: Buffer | Uint8Array, auth: AuthenticationState) => { +export const decryptSignalProto = async(user: string, type: 'pkmsg' | 'msg', msg: Buffer | Uint8Array, auth: SignalAuthState) => { const addr = jidToSignalProtocolAddress(user) const session = new libsignal.SessionCipher(signalStorage(auth), addr) let result: Buffer @@ -172,7 +172,7 @@ export const decryptSignalProto = async(user: string, type: 'pkmsg' | 'msg', msg } -export const encryptSignalProto = async(user: string, buffer: Buffer, auth: AuthenticationState) => { +export const encryptSignalProto = async(user: string, buffer: Buffer, auth: SignalAuthState) => { const addr = jidToSignalProtocolAddress(user) const cipher = new libsignal.SessionCipher(signalStorage(auth), addr) @@ -183,9 +183,9 @@ export const encryptSignalProto = async(user: string, buffer: Buffer, auth: Auth } } -export const encryptSenderKeyMsgSignalProto = async(group: string, data: Uint8Array | Buffer, auth: AuthenticationState) => { +export const encryptSenderKeyMsgSignalProto = async(group: string, data: Uint8Array | Buffer, meId: string, auth: SignalAuthState) => { const storage = signalStorage(auth) - const senderName = jidToSignalSenderKeyName(group, auth.creds.me!.id) + const senderName = jidToSignalSenderKeyName(group, meId) const builder = new GroupSessionBuilder(storage) const senderKey = await auth.keys.getSenderKey(senderName) @@ -202,7 +202,7 @@ export const encryptSenderKeyMsgSignalProto = async(group: string, data: Uint8Ar } } -export const parseAndInjectE2ESession = async(node: BinaryNode, auth: AuthenticationState) => { +export const parseAndInjectE2ESession = async(node: BinaryNode, auth: SignalAuthState) => { const extractKey = (key: BinaryNode) => ( key ? ({ keyId: getBinaryNodeChildUInt(key, 'id', 3), diff --git a/src/Utils/validate-connection.ts b/src/Utils/validate-connection.ts index f09c304..88468b8 100644 --- a/src/Utils/validate-connection.ts +++ b/src/Utils/validate-connection.ts @@ -1,9 +1,8 @@ import { Boom } from '@hapi/boom' -import { randomBytes } from 'crypto' import { proto } from '../../WAProto' -import type { AuthenticationState, SocketConfig, SignalKeyStore, AuthenticationCreds, KeyPair, LTHashState } from "../Types" -import { Curve, hmacSign, signedKeyPair } from './crypto' -import { encodeInt, generateRegistrationId } from './generics' +import type { SocketConfig, AuthenticationCreds, SignalCreds } from "../Types" +import { Curve, hmacSign } from './crypto' +import { encodeInt } from './generics' import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary, getAllBinaryNodeChildren } from '../WABinary' import { createSignalIdentity } from './signal' @@ -41,7 +40,7 @@ export const generateLoginNode = (userJid: string, config: Pick, + { registrationId, signedPreKey, signedIdentityKey }: SignalCreds, config: Pick ) => { const appVersionBuf = new Uint8Array(Buffer.from(ENCODED_VERSION, "base64")); @@ -82,84 +81,6 @@ export const generateRegistrationNode = ( return proto.ClientPayload.encode(registerPayload).finish() } -export const initInMemoryKeyStore = ( - { preKeys, sessions, senderKeys, appStateSyncKeys, appStateVersions }: { - preKeys?: { [k: number]: KeyPair }, - sessions?: { [k: string]: any }, - senderKeys?: { [k: string]: any } - appStateSyncKeys?: { [k: string]: proto.IAppStateSyncKeyData }, - appStateVersions?: { [k: string]: LTHashState }, - } = { }, -) => { - preKeys = preKeys || { } - sessions = sessions || { } - senderKeys = senderKeys || { } - appStateSyncKeys = appStateSyncKeys || { } - appStateVersions = appStateVersions || { } - return { - preKeys, - sessions, - senderKeys, - appStateSyncKeys, - appStateVersions, - getPreKey: keyId => preKeys[keyId], - setPreKey: (keyId, pair) => { - if(pair) preKeys[keyId] = pair - else delete preKeys[keyId] - }, - getSession: id => sessions[id], - setSession: (id, item) => { - if(item) sessions[id] = item - else delete sessions[id] - }, - getSenderKey: id => { - return senderKeys[id] - }, - setSenderKey: (id, item) => { - if(item) senderKeys[id] = item - else delete senderKeys[id] - }, - getAppStateSyncKey: id => { - const obj = appStateSyncKeys[id] - if(obj) { - return proto.AppStateSyncKeyData.fromObject(obj) - } - }, - setAppStateSyncKey: (id, item) => { - if(item) appStateSyncKeys[id] = item - else delete appStateSyncKeys[id] - }, - getAppStateSyncVersion: id => { - const obj = appStateVersions[id] - if(obj) { - return obj - } - }, - setAppStateSyncVersion: (id, item) => { - if(item) appStateVersions[id] = item - else delete appStateVersions[id] - } - } as SignalKeyStore -} - -export const initAuthState = (): AuthenticationState => { - const identityKey = Curve.generateKeyPair() - return { - creds: { - noiseKey: Curve.generateKeyPair(), - signedIdentityKey: identityKey, - signedPreKey: signedKeyPair(identityKey, 1), - registrationId: generateRegistrationId(), - advSecretKey: randomBytes(32).toString('base64'), - - nextPreKeyId: 1, - firstUnuploadedPreKeyId: 1, - serverHasPreKeys: false - }, - keys: initInMemoryKeyStore() - } -} - export const configureSuccessfulPairing = ( stanza: BinaryNode, { advSecretKey, signedIdentityKey, signalIdentities }: Pick