mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
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
This commit is contained in:
@@ -1,41 +1,16 @@
|
|||||||
import { readFileSync, writeFileSync } from "fs"
|
|
||||||
import P from "pino"
|
import P from "pino"
|
||||||
import { Boom } from "@hapi/boom"
|
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 { state, saveState } = useSingleFileAuthState('./auth_info_multi.json')
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start a connection
|
// start a connection
|
||||||
const startSock = () => {
|
const startSock = () => {
|
||||||
|
|
||||||
const sock = makeWASocket({
|
const sock = makeWASocket({
|
||||||
logger: P({ level: 'trace' }),
|
logger: P({ level: 'trace' }),
|
||||||
printQRInTerminal: true,
|
printQRInTerminal: true,
|
||||||
auth: loadState()
|
auth: state
|
||||||
})
|
})
|
||||||
|
|
||||||
const sendMessageWTyping = async(msg: AnyMessageContent, jid: string) => {
|
const sendMessageWTyping = async(msg: AnyMessageContent, jid: string) => {
|
||||||
@@ -80,9 +55,8 @@ const startSock = () => {
|
|||||||
|
|
||||||
console.log('connection update', update)
|
console.log('connection update', update)
|
||||||
})
|
})
|
||||||
// listen for when the auth state is updated
|
// listen for when the auth credentials is updated
|
||||||
// it is imperative you save this data, it affects the signing keys you need to have conversations
|
sock.ev.on('creds.update', saveState)
|
||||||
sock.ev.on('auth-state.update', () => saveState(sock.authState))
|
|
||||||
|
|
||||||
return sock
|
return sock
|
||||||
}
|
}
|
||||||
|
|||||||
51
README.md
51
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.
|
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
|
``` ts
|
||||||
import makeWASocket, { BufferJSON } from '@adiwajshing/baileys-md'
|
import makeWASocket, { BufferJSON, useSingleFileAuthState } from '@adiwajshing/baileys-md'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
|
||||||
// will initialize a default in-memory auth session
|
// utility function to help save the auth state in a single file
|
||||||
const conn = makeSocket()
|
// 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
|
// this will be called as soon as the credentials are updated
|
||||||
conn.ev.on ('auth-state.update', () => {
|
conn.ev.on ('creds.update', saveState)
|
||||||
// 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)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, to restore a session:
|
**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.
|
||||||
``` 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.
|
|
||||||
|
|
||||||
## Listening to Connection Updates
|
## Listening to Connection Updates
|
||||||
|
|
||||||
@@ -196,8 +169,8 @@ The events are typed up in a type map, as mentioned here:
|
|||||||
export type BaileysEventMap = {
|
export type BaileysEventMap = {
|
||||||
/** connection state has been updated -- WS closed, opened, connecting etc. */
|
/** connection state has been updated -- WS closed, opened, connecting etc. */
|
||||||
'connection.update': Partial<ConnectionState>
|
'connection.update': Partial<ConnectionState>
|
||||||
/** auth state updated -- some pre keys, or identity keys etc. */
|
/** auth credentials updated -- some pre key state, device ID etc. */
|
||||||
'auth-state.update': AuthenticationState
|
'creds.update': Partial<AuthenticationCreds>
|
||||||
/** set chats (history sync), messages are reverse chronologically sorted */
|
/** set chats (history sync), messages are reverse chronologically sorted */
|
||||||
'chats.set': { chats: Chat[], messages: WAMessage[] }
|
'chats.set': { chats: Chat[], messages: WAMessage[] }
|
||||||
/** upsert chats */
|
/** upsert chats */
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
|||||||
keepAliveIntervalMs: 25_000,
|
keepAliveIntervalMs: 25_000,
|
||||||
logger: P().child({ class: 'baileys' }),
|
logger: P().child({ class: 'baileys' }),
|
||||||
printQRInTerminal: false,
|
printQRInTerminal: false,
|
||||||
emitOwnEvents: true
|
emitOwnEvents: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MEDIA_PATH_MAP: { [T in MediaType]: string } = {
|
export const MEDIA_PATH_MAP: { [T in MediaType]: string } = {
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
processSyncActions(newMutations)
|
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 sendPresenceUpdate = async(type: WAPresence, toJid?: string) => {
|
||||||
|
const me = authState.creds.me!
|
||||||
if(type === 'available' || type === 'unavailable') {
|
if(type === 'available' || type === 'unavailable') {
|
||||||
await sendNode({
|
await sendNode({
|
||||||
tag: 'presence',
|
tag: 'presence',
|
||||||
attrs: {
|
attrs: {
|
||||||
name: authState.creds.me!.name,
|
name: me!.name,
|
||||||
type
|
type
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -257,7 +257,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
await sendNode({
|
await sendNode({
|
||||||
tag: 'chatstate',
|
tag: 'chatstate',
|
||||||
attrs: {
|
attrs: {
|
||||||
from: authState.creds.me!.id!,
|
from: me!.id!,
|
||||||
to: toJid,
|
to: toJid,
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
@@ -344,8 +344,11 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
name: action.contactAction!.fullName
|
name: action.contactAction!.fullName
|
||||||
}
|
}
|
||||||
} else if(action?.pushNameSetting) {
|
} else if(action?.pushNameSetting) {
|
||||||
authState.creds.me!.name = action?.pushNameSetting?.name!
|
const me = {
|
||||||
ev.emit('auth-state.update', authState)
|
...authState.creds.me!,
|
||||||
|
name: action?.pushNameSetting?.name!
|
||||||
|
}
|
||||||
|
ev.emit('creds.update', { me })
|
||||||
} else {
|
} else {
|
||||||
logger.warn({ action, id }, 'unprocessable update')
|
logger.warn({ action, id }, 'unprocessable update')
|
||||||
}
|
}
|
||||||
@@ -420,7 +423,6 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
await query(node)
|
await query(node)
|
||||||
|
|
||||||
await authState.keys.setAppStateSyncVersion(name, state)
|
await authState.keys.setAppStateSyncVersion(name, state)
|
||||||
ev.emit('auth-state.update', authState)
|
|
||||||
|
|
||||||
if(config.emitOwnEvents) {
|
if(config.emitOwnEvents) {
|
||||||
processSyncActions(result.newMutations)
|
processSyncActions(result.newMutations)
|
||||||
|
|||||||
@@ -158,15 +158,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
logger.info({ with: message.key.remoteJid! }, 'shared key')
|
logger.info({ with: message.key.remoteJid! }, 'shared key')
|
||||||
break
|
break
|
||||||
case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE:
|
case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE:
|
||||||
|
let newAppStateSyncKeyId = ''
|
||||||
for(const { keyData, keyId } of protocolMsg.appStateSyncKeyShare!.keys || []) {
|
for(const { keyData, keyId } of protocolMsg.appStateSyncKeyShare!.keys || []) {
|
||||||
const str = Buffer.from(keyId.keyId!).toString('base64')
|
const str = Buffer.from(keyId.keyId!).toString('base64')
|
||||||
logger.info({ str }, 'injecting new app state sync key')
|
logger.info({ str }, 'injecting new app state sync key')
|
||||||
await authState.keys.setAppStateSyncKey(str, keyData)
|
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()
|
resyncMainAppState()
|
||||||
break
|
break
|
||||||
@@ -359,7 +360,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
// if there were some successful decryptions
|
// if there were some successful decryptions
|
||||||
if(dec.successes.length) {
|
if(dec.successes.length) {
|
||||||
ev.emit('auth-state.update', authState)
|
|
||||||
// send message receipt
|
// send message receipt
|
||||||
let recpAttrs: { [_: string]: any }
|
let recpAttrs: { [_: string]: any }
|
||||||
if(isMe) {
|
if(isMe) {
|
||||||
@@ -506,8 +506,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
contactNameUpdates[jid] = msg.pushName
|
contactNameUpdates[jid] = msg.pushName
|
||||||
// update our pushname too
|
// update our pushname too
|
||||||
if(msg.key.fromMe && authState.creds.me?.name !== msg.pushName) {
|
if(msg.key.fromMe && authState.creds.me?.name !== msg.pushName) {
|
||||||
authState.creds.me!.name = msg.pushName
|
ev.emit('creds.update', { me: { ...authState.creds.me!, name: msg.pushName! } })
|
||||||
ev.emit('auth-state.update', authState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
const destinationJid = jidEncode(user, isGroup ? 'g.us' : 's.whatsapp.net')
|
const destinationJid = jidEncode(user, isGroup ? 'g.us' : 's.whatsapp.net')
|
||||||
|
|
||||||
if(isGroup) {
|
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
|
let groupData = cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
||||||
if(!groupData) groupData = await groupMetadata(jid)
|
if(!groupData) groupData = await groupMetadata(jid)
|
||||||
@@ -375,7 +375,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
await sendNode(stanza)
|
await sendNode(stanza)
|
||||||
|
|
||||||
ev.emit('auth-state.update', authState)
|
|
||||||
return msgId
|
return msgId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { promisify } from "util"
|
|||||||
import WebSocket from "ws"
|
import WebSocket from "ws"
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { DisconnectReason, SocketConfig, BaileysEventEmitter, ConnectionState } from "../Types"
|
import { DisconnectReason, SocketConfig, BaileysEventEmitter, ConnectionState, AuthenticationCreds } from "../Types"
|
||||||
import { Curve, initAuthState, generateRegistrationNode, configureSuccessfulPairing, generateLoginNode, encodeBigEndian, promiseTimeout, generateOrGetPreKeys, xmppSignedPreKey, xmppPreKey, getPreKeys, makeNoiseHandler } from "../Utils"
|
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 { DEFAULT_ORIGIN, DEF_TAG_PREFIX, DEF_CALLBACK_PREFIX, KEY_BUNDLE_TYPE } from "../Defaults"
|
||||||
import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, S_WHATSAPP_NET, getBinaryNodeChild } from '../WABinary'
|
import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, S_WHATSAPP_NET, getBinaryNodeChild } from '../WABinary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects to WA servers and performs:
|
* Connects to WA servers and performs:
|
||||||
* - simple queries (no retry mechanism, wait for connection establishment)
|
* - simple queries (no retry mechanism, wait for connection establishment)
|
||||||
@@ -39,13 +40,22 @@ export const makeSocket = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
ws.setMaxListeners(0)
|
ws.setMaxListeners(0)
|
||||||
|
const ev = new EventEmitter() as BaileysEventEmitter
|
||||||
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
||||||
const ephemeralKeyPair = Curve.generateKeyPair()
|
const ephemeralKeyPair = Curve.generateKeyPair()
|
||||||
/** WA noise protocol wrapper */
|
/** WA noise protocol wrapper */
|
||||||
const noise = makeNoiseHandler(ephemeralKeyPair)
|
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 { creds } = authState
|
||||||
const ev = new EventEmitter() as BaileysEventEmitter
|
|
||||||
|
|
||||||
let lastDateRecv: Date
|
let lastDateRecv: Date
|
||||||
let epoch = 0
|
let epoch = 0
|
||||||
@@ -174,11 +184,16 @@ export const makeSocket = ({
|
|||||||
}
|
}
|
||||||
/** get some pre-keys and do something with them */
|
/** get some pre-keys and do something with them */
|
||||||
const assertingPreKeys = async(range: number, execute: (keys: { [_: number]: any }) => Promise<void>) => {
|
const assertingPreKeys = async(range: number, execute: (keys: { [_: number]: any }) => Promise<void>) => {
|
||||||
const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(authState, range)
|
const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(authState.creds, range)
|
||||||
|
|
||||||
creds.serverHasPreKeys = true
|
const update: Partial<AuthenticationCreds> = {
|
||||||
creds.nextPreKeyId = Math.max(lastPreKeyId+1, creds.nextPreKeyId)
|
nextPreKeyId: Math.max(lastPreKeyId+1, creds.nextPreKeyId),
|
||||||
creds.firstUnuploadedPreKeyId = Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId+1)
|
firstUnuploadedPreKeyId: Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId+1)
|
||||||
|
}
|
||||||
|
if(!creds.serverHasPreKeys) {
|
||||||
|
update.serverHasPreKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.keys(newPreKeys).map(k => authState.keys.setPreKey(+k, newPreKeys[+k]))
|
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])
|
const preKeys = await getPreKeys(authState.keys, preKeysRange[0], preKeysRange[0] + preKeysRange[1])
|
||||||
await execute(preKeys)
|
await execute(preKeys)
|
||||||
|
|
||||||
ev.emit('auth-state.update', authState)
|
ev.emit('creds.update', update)
|
||||||
}
|
}
|
||||||
/** generates and uploads a set of pre-keys */
|
/** generates and uploads a set of pre-keys */
|
||||||
const uploadPreKeys = async() => {
|
const uploadPreKeys = async() => {
|
||||||
@@ -464,10 +479,10 @@ export const makeSocket = ({
|
|||||||
throw new Boom('Authentication failed', { statusCode: +(value.attrs.code || 500) })
|
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 })
|
ev.emit('connection.update', { isNewLogin: true, qr: undefined })
|
||||||
|
|
||||||
end(new Boom('Restart Required', { statusCode: DisconnectReason.restartRequired }))
|
end(new Boom('Restart Required', { statusCode: DisconnectReason.restartRequired }))
|
||||||
@@ -512,6 +527,8 @@ export const makeSocket = ({
|
|||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined })
|
ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined })
|
||||||
})
|
})
|
||||||
|
// update credentials when required
|
||||||
|
ev.on('creds.update', update => Object.assign(creds, update))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ws,
|
ws,
|
||||||
|
|||||||
@@ -16,12 +16,16 @@ export type SignalIdentity = {
|
|||||||
|
|
||||||
export type LTHashState = { version: number, hash: Buffer, mutations: ChatMutation[] }
|
export type LTHashState = { version: number, hash: Buffer, mutations: ChatMutation[] }
|
||||||
|
|
||||||
export type AuthenticationCreds = {
|
export type SignalCreds = {
|
||||||
noiseKey: KeyPair
|
readonly signedIdentityKey: KeyPair
|
||||||
signedIdentityKey: KeyPair
|
readonly signedPreKey: SignedKeyPair
|
||||||
signedPreKey: SignedKeyPair
|
readonly registrationId: number
|
||||||
registrationId: number
|
}
|
||||||
advSecretKey: string
|
|
||||||
|
export type AuthenticationCreds = SignalCreds & {
|
||||||
|
readonly noiseKey: KeyPair
|
||||||
|
readonly advSecretKey: string
|
||||||
|
|
||||||
me?: Contact
|
me?: Contact
|
||||||
account?: proto.IADVSignedDeviceIdentity
|
account?: proto.IADVSignedDeviceIdentity
|
||||||
signalIdentities?: SignalIdentity[]
|
signalIdentities?: SignalIdentity[]
|
||||||
@@ -49,6 +53,11 @@ export type SignalKeyStore = {
|
|||||||
setAppStateSyncVersion: (id: WAPatchName, item: LTHashState) => Awaitable<void>
|
setAppStateSyncVersion: (id: WAPatchName, item: LTHashState) => Awaitable<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SignalAuthState = {
|
||||||
|
creds: SignalCreds
|
||||||
|
keys: SignalKeyStore
|
||||||
|
}
|
||||||
|
|
||||||
export type AuthenticationState = {
|
export type AuthenticationState = {
|
||||||
creds: AuthenticationCreds
|
creds: AuthenticationCreds
|
||||||
keys: SignalKeyStore
|
keys: SignalKeyStore
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { Logger } from "pino"
|
|||||||
import type { URL } from "url"
|
import type { URL } from "url"
|
||||||
import type NodeCache from 'node-cache'
|
import type NodeCache from 'node-cache'
|
||||||
|
|
||||||
import { AuthenticationState } from './Auth'
|
import { AuthenticationState, AuthenticationCreds } from './Auth'
|
||||||
import { Chat, PresenceData } from './Chat'
|
import { Chat, PresenceData } from './Chat'
|
||||||
import { Contact } from './Contact'
|
import { Contact } from './Contact'
|
||||||
import { ConnectionState } from './State'
|
import { ConnectionState } from './State'
|
||||||
@@ -96,8 +96,8 @@ export type CurveKeyPair = { private: Uint8Array; public: Uint8Array }
|
|||||||
export type BaileysEventMap = {
|
export type BaileysEventMap = {
|
||||||
/** connection state has been updated -- WS closed, opened, connecting etc. */
|
/** connection state has been updated -- WS closed, opened, connecting etc. */
|
||||||
'connection.update': Partial<ConnectionState>
|
'connection.update': Partial<ConnectionState>
|
||||||
/** auth state updated -- some pre keys, or identity keys etc. */
|
/** credentials updated -- some metadata, keys or something */
|
||||||
'auth-state.update': AuthenticationState
|
'creds.update': Partial<AuthenticationCreds>
|
||||||
/** set chats (history sync), messages are reverse chronologically sorted */
|
/** set chats (history sync), messages are reverse chronologically sorted */
|
||||||
'chats.set': { chats: Chat[], messages: WAMessage[] }
|
'chats.set': { chats: Chat[], messages: WAMessage[] }
|
||||||
/** upsert chats */
|
/** upsert chats */
|
||||||
|
|||||||
133
src/Utils/auth-utils.ts
Normal file
133
src/Utils/auth-utils.ts
Normal file
@@ -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 }
|
||||||
|
}
|
||||||
@@ -8,4 +8,5 @@ export * from './signal'
|
|||||||
export * from './noise-handler'
|
export * from './noise-handler'
|
||||||
export * from './history'
|
export * from './history'
|
||||||
export * from './chat-utils'
|
export * from './chat-utils'
|
||||||
export * from './lt-hash'
|
export * from './lt-hash'
|
||||||
|
export * from './auth-utils'
|
||||||
@@ -2,7 +2,7 @@ import * as libsignal from 'libsignal'
|
|||||||
import { encodeBigEndian } from "./generics"
|
import { encodeBigEndian } from "./generics"
|
||||||
import { Curve } from "./crypto"
|
import { Curve } from "./crypto"
|
||||||
import { SenderKeyDistributionMessage, GroupSessionBuilder, SenderKeyRecord, SenderKeyName, GroupCipher } from '../../WASignalGroup'
|
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 { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildUInt, jidDecode, JidWithDevice } from "../WABinary"
|
||||||
import { proto } from "../../WAProto"
|
import { proto } from "../../WAProto"
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export const getPreKeys = async({ getPreKey }: SignalKeyStore, min: number, limi
|
|||||||
return dict
|
return dict
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateOrGetPreKeys = ({ creds }: AuthenticationState, range: number) => {
|
export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number) => {
|
||||||
const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId
|
const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId
|
||||||
const remaining = range - avaliable
|
const remaining = range - avaliable
|
||||||
const lastPreKeyId = creds.nextPreKeyId + remaining - 1
|
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 => {
|
loadSession: async id => {
|
||||||
const sess = await keys.getSession(id)
|
const sess = await keys.getSession(id)
|
||||||
if(sess) {
|
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 senderName = jidToSignalSenderKeyName(group, user)
|
||||||
const cipher = new GroupCipher(signalStorage(auth), senderName)
|
const cipher = new GroupCipher(signalStorage(auth), senderName)
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ export const decryptGroupSignalProto = (group: string, user: string, msg: Buffer
|
|||||||
export const processSenderKeyMessage = async(
|
export const processSenderKeyMessage = async(
|
||||||
authorJid: string,
|
authorJid: string,
|
||||||
item: proto.ISenderKeyDistributionMessage,
|
item: proto.ISenderKeyDistributionMessage,
|
||||||
auth: AuthenticationState
|
auth: SignalAuthState
|
||||||
) => {
|
) => {
|
||||||
const builder = new GroupSessionBuilder(signalStorage(auth))
|
const builder = new GroupSessionBuilder(signalStorage(auth))
|
||||||
const senderName = jidToSignalSenderKeyName(item.groupId, authorJid)
|
const senderName = jidToSignalSenderKeyName(item.groupId, authorJid)
|
||||||
@@ -156,7 +156,7 @@ export const processSenderKeyMessage = async(
|
|||||||
await builder.process(senderName, senderMsg)
|
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 addr = jidToSignalProtocolAddress(user)
|
||||||
const session = new libsignal.SessionCipher(signalStorage(auth), addr)
|
const session = new libsignal.SessionCipher(signalStorage(auth), addr)
|
||||||
let result: Buffer
|
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 addr = jidToSignalProtocolAddress(user)
|
||||||
const cipher = new libsignal.SessionCipher(signalStorage(auth), addr)
|
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 storage = signalStorage(auth)
|
||||||
const senderName = jidToSignalSenderKeyName(group, auth.creds.me!.id)
|
const senderName = jidToSignalSenderKeyName(group, meId)
|
||||||
const builder = new GroupSessionBuilder(storage)
|
const builder = new GroupSessionBuilder(storage)
|
||||||
|
|
||||||
const senderKey = await auth.keys.getSenderKey(senderName)
|
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) => (
|
const extractKey = (key: BinaryNode) => (
|
||||||
key ? ({
|
key ? ({
|
||||||
keyId: getBinaryNodeChildUInt(key, 'id', 3),
|
keyId: getBinaryNodeChildUInt(key, 'id', 3),
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import { randomBytes } from 'crypto'
|
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import type { AuthenticationState, SocketConfig, SignalKeyStore, AuthenticationCreds, KeyPair, LTHashState } from "../Types"
|
import type { SocketConfig, AuthenticationCreds, SignalCreds } from "../Types"
|
||||||
import { Curve, hmacSign, signedKeyPair } from './crypto'
|
import { Curve, hmacSign } from './crypto'
|
||||||
import { encodeInt, generateRegistrationId } from './generics'
|
import { encodeInt } from './generics'
|
||||||
import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary, getAllBinaryNodeChildren } from '../WABinary'
|
import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary, getAllBinaryNodeChildren } from '../WABinary'
|
||||||
import { createSignalIdentity } from './signal'
|
import { createSignalIdentity } from './signal'
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@ export const generateLoginNode = (userJid: string, config: Pick<SocketConfig, 'v
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const generateRegistrationNode = (
|
export const generateRegistrationNode = (
|
||||||
{ registrationId, signedPreKey, signedIdentityKey }: Pick<AuthenticationCreds, 'registrationId' | 'signedPreKey' | 'signedIdentityKey'>,
|
{ registrationId, signedPreKey, signedIdentityKey }: SignalCreds,
|
||||||
config: Pick<SocketConfig, 'version' | 'browser'>
|
config: Pick<SocketConfig, 'version' | 'browser'>
|
||||||
) => {
|
) => {
|
||||||
const appVersionBuf = new Uint8Array(Buffer.from(ENCODED_VERSION, "base64"));
|
const appVersionBuf = new Uint8Array(Buffer.from(ENCODED_VERSION, "base64"));
|
||||||
@@ -82,84 +81,6 @@ export const generateRegistrationNode = (
|
|||||||
return proto.ClientPayload.encode(registerPayload).finish()
|
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 = (
|
export const configureSuccessfulPairing = (
|
||||||
stanza: BinaryNode,
|
stanza: BinaryNode,
|
||||||
{ advSecretKey, signedIdentityKey, signalIdentities }: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'>
|
{ advSecretKey, signedIdentityKey, signalIdentities }: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'>
|
||||||
|
|||||||
Reference in New Issue
Block a user