mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
implement encrypting app patches
This commit is contained in:
@@ -1,18 +1,20 @@
|
|||||||
import { decodeSyncdPatch, encodeSyncdPatch } from "../Utils/chat-utils";
|
import { encodeSyncdPatch, decodePatches, extractSyncdPatches } from "../Utils/chat-utils";
|
||||||
import { SocketConfig, WAPresence, PresenceData, Chat, ChatModification, WAMediaUpload, ChatMutation } from "../Types";
|
import { SocketConfig, WAPresence, PresenceData, Chat, ChatModification, WAMediaUpload, ChatMutation, WAPatchName, LTHashState } from "../Types";
|
||||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, S_WHATSAPP_NET } from "../WABinary";
|
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, S_WHATSAPP_NET } from "../WABinary";
|
||||||
import { makeSocket } from "./socket";
|
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { toNumber } from "../Utils/generics";
|
import { generateProfilePicture, toNumber } from "../Utils";
|
||||||
import { generateProfilePicture } from "../Utils";
|
import { randomBytes } from "crypto";
|
||||||
|
import { makeMessagesRecvSocket } from "./messages-recv";
|
||||||
|
|
||||||
export const makeChatsSocket = (config: SocketConfig) => {
|
export const makeChatsSocket = (config: SocketConfig) => {
|
||||||
const { logger } = config
|
const { logger } = config
|
||||||
const sock = makeSocket(config)
|
const sock = makeMessagesRecvSocket(config)
|
||||||
const {
|
const {
|
||||||
ev,
|
ev,
|
||||||
ws,
|
ws,
|
||||||
authState,
|
authState,
|
||||||
|
processMessage,
|
||||||
|
relayMessage,
|
||||||
generateMessageTag,
|
generateMessageTag,
|
||||||
sendNode,
|
sendNode,
|
||||||
query
|
query
|
||||||
@@ -25,7 +27,6 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET,
|
||||||
type: 'get',
|
type: 'get',
|
||||||
xmlns: 'usync',
|
xmlns: 'usync',
|
||||||
|
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -188,30 +189,38 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionSync = async() => {
|
const collectionSync = async(collections: { name: WAPatchName, version: number }[]) => {
|
||||||
const COLLECTIONS = ['critical_block', 'critical_unblock_low', 'regular_low', 'regular_high']
|
const result = await query({
|
||||||
await sendNode({
|
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET,
|
||||||
xmlns: 'w:sync:app:state',
|
xmlns: 'w:sync:app:state',
|
||||||
type: 'set',
|
type: 'set'
|
||||||
id: generateMessageTag(),
|
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'sync',
|
tag: 'sync',
|
||||||
attrs: { },
|
attrs: { },
|
||||||
content: COLLECTIONS.map(
|
content: collections.map(
|
||||||
name => ({
|
({ name, version }) => ({
|
||||||
tag: 'collection',
|
tag: 'collection',
|
||||||
attrs: { name, version: '0', return_snapshot: 'true' }
|
attrs: { name, version: version.toString(), return_snapshot: 'true' }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
logger.info('synced collection')
|
const syncNode = getBinaryNodeChild(result, 'sync')
|
||||||
|
const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
|
||||||
|
return collectionNodes.reduce(
|
||||||
|
(dict, node) => {
|
||||||
|
const snapshotNode = getBinaryNodeChild(node, 'snapshot')
|
||||||
|
if(snapshotNode) {
|
||||||
|
dict[node.attrs.name] = snapshotNode.content as Uint8Array
|
||||||
|
}
|
||||||
|
return dict
|
||||||
|
}, { } as { [P in WAPatchName]: Uint8Array }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const profilePictureUrl = async(jid: string) => {
|
const profilePictureUrl = async(jid: string) => {
|
||||||
@@ -311,18 +320,31 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
} else {
|
} else {
|
||||||
logger.warn({ action, id }, 'unprocessable update')
|
logger.warn({ action, id }, 'unprocessable update')
|
||||||
}
|
}
|
||||||
updates.push(update)
|
if(Object.keys(update).length > 1) {
|
||||||
|
updates.push(update)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ev.emit('chats.update', updates)
|
ev.emit('chats.update', updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchChat = async(
|
const appPatch = async(
|
||||||
jid: string,
|
syncAction: proto.ISyncActionValue,
|
||||||
modification: ChatModification
|
index: [string, string],
|
||||||
|
name: WAPatchName,
|
||||||
|
operation: proto.SyncdMutation.SyncdMutationSyncdOperation.SET,
|
||||||
) => {
|
) => {
|
||||||
const patch = await encodeSyncdPatch(modification, { remoteJid: jid }, authState)
|
await resyncState(name, false)
|
||||||
const type = 'regular_high'
|
const { patch, state } = await encodeSyncdPatch(
|
||||||
const ver = authState.creds.appStateVersion![type] || 0
|
syncAction,
|
||||||
|
index,
|
||||||
|
name,
|
||||||
|
operation,
|
||||||
|
authState,
|
||||||
|
)
|
||||||
|
const initial = await authState.keys.getAppStateSyncVersion(name)
|
||||||
|
// temp: verify it was encoded correctly
|
||||||
|
const result = await decodePatches({ syncds: [{ ...patch, version: { version: state.version }, }], name }, initial, authState)
|
||||||
|
|
||||||
const node: BinaryNode = {
|
const node: BinaryNode = {
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -332,29 +354,35 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'patch',
|
tag: 'sync',
|
||||||
attrs: {
|
attrs: { },
|
||||||
name: type,
|
content: [
|
||||||
version: (ver+1).toString(),
|
{
|
||||||
return_snapshot: 'false'
|
tag: 'collection',
|
||||||
},
|
attrs: {
|
||||||
content: proto.SyncdPatch.encode(patch).finish()
|
name,
|
||||||
|
version: (state.version-1).toString(),
|
||||||
|
return_snapshot: 'false'
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
tag: 'patch',
|
||||||
|
attrs: { },
|
||||||
|
content: proto.SyncdPatch.encode(patch).finish()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
await query(node)
|
await query(node)
|
||||||
|
|
||||||
authState.creds.appStateVersion![type] += 1
|
await authState.keys.setAppStateSyncVersion(name, state)
|
||||||
ev.emit('auth-state.update', authState)
|
ev.emit('auth-state.update', authState)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resyncState = async(name: 'regular_high' | 'regular_low' = 'regular_high') => {
|
const fetchAppState = async(name: WAPatchName, fromVersion: number) => {
|
||||||
authState.creds.appStateVersion = authState.creds.appStateVersion || {
|
|
||||||
regular_high: 0,
|
|
||||||
regular_low: 0,
|
|
||||||
critical_unblock_low: 0,
|
|
||||||
critical_block: 0
|
|
||||||
}
|
|
||||||
const result = await query({
|
const result = await query({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -371,7 +399,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
tag: 'collection',
|
tag: 'collection',
|
||||||
attrs: {
|
attrs: {
|
||||||
name,
|
name,
|
||||||
version: authState.creds.appStateVersion[name].toString(),
|
version: fromVersion.toString(),
|
||||||
return_snapshot: 'false'
|
return_snapshot: 'false'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,30 +407,24 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
const syncNode = getBinaryNodeChild(result, 'sync')
|
return result
|
||||||
const collectionNode = getBinaryNodeChild(syncNode, 'collection')
|
}
|
||||||
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
|
|
||||||
|
const resyncState = async(name: WAPatchName, fromScratch: boolean) => {
|
||||||
|
let state: LTHashState = fromScratch ? undefined : await authState.keys.getAppStateSyncVersion(name)
|
||||||
|
if(!state) state = { version: 0, hash: Buffer.alloc(128), mutations: [] }
|
||||||
|
|
||||||
|
logger.info(`resyncing ${name} from v${state.version}`)
|
||||||
|
|
||||||
|
const result = await fetchAppState(name, state.version)
|
||||||
|
const decoded = extractSyncdPatches(result) // extract from binary node
|
||||||
|
const { newMutations, state: newState } = await decodePatches(decoded, state, authState, true)
|
||||||
|
|
||||||
|
await authState.keys.setAppStateSyncVersion(name, newState)
|
||||||
|
|
||||||
|
logger.info(`synced ${name} to v${newState.version}`)
|
||||||
|
processSyncActions(newMutations)
|
||||||
|
|
||||||
const patches = getBinaryNodeChildren(patchesNode, 'patch')
|
|
||||||
const successfulMutations: ChatMutation[] = []
|
|
||||||
for(const { content } of patches) {
|
|
||||||
if(content) {
|
|
||||||
const syncd = proto.SyncdPatch.decode(content! as Uint8Array)
|
|
||||||
const version = toNumber(syncd.version!.version!)
|
|
||||||
if(version) {
|
|
||||||
authState.creds.appStateVersion[name] = Math.max(version, authState.creds.appStateVersion[name])
|
|
||||||
}
|
|
||||||
const { mutations, failures } = await decodeSyncdPatch(syncd, authState)
|
|
||||||
if(failures.length) {
|
|
||||||
logger.info(
|
|
||||||
{ failures: failures.map(f => ({ trace: f.stack, data: f.data })) },
|
|
||||||
'failed to decode'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
successfulMutations.push(...mutations)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processSyncActions(successfulMutations)
|
|
||||||
ev.emit('auth-state.update', authState)
|
ev.emit('auth-state.update', authState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,7 +434,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
ws.on('CB:notification,type:server_sync', (node: BinaryNode) => {
|
ws.on('CB:notification,type:server_sync', (node: BinaryNode) => {
|
||||||
const update = getBinaryNodeChild(node, 'collection')
|
const update = getBinaryNodeChild(node, 'collection')
|
||||||
if(update) {
|
if(update) {
|
||||||
resyncState(update.attrs.name as any)
|
resyncState(update.attrs.name as WAPatchName, false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -421,13 +443,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
sendPresenceUpdate('available')
|
sendPresenceUpdate('available')
|
||||||
fetchBlocklist()
|
fetchBlocklist()
|
||||||
fetchPrivacySettings()
|
fetchPrivacySettings()
|
||||||
//collectionSync()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...sock,
|
...sock,
|
||||||
patchChat,
|
appPatch,
|
||||||
sendPresenceUpdate,
|
sendPresenceUpdate,
|
||||||
presenceSubscribe,
|
presenceSubscribe,
|
||||||
profilePictureUrl,
|
profilePictureUrl,
|
||||||
@@ -436,6 +457,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
fetchPrivacySettings,
|
fetchPrivacySettings,
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
updateProfilePicture,
|
updateProfilePicture,
|
||||||
updateBlockStatus
|
updateBlockStatus,
|
||||||
|
resyncState,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,41 +1,10 @@
|
|||||||
import { generateMessageID } from "../Utils";
|
import { generateMessageID } from "../Utils";
|
||||||
import { SocketConfig, GroupMetadata, ParticipantAction } from "../Types";
|
import { SocketConfig, GroupMetadata, ParticipantAction } from "../Types";
|
||||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidDecode, jidEncode } from "../WABinary";
|
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidEncode } from "../WABinary";
|
||||||
import { makeChatsSocket } from "./chats";
|
import { makeSocket } from "./socket";
|
||||||
|
|
||||||
const extractGroupMetadata = (result: BinaryNode) => {
|
|
||||||
const group = getBinaryNodeChild(result, 'group')
|
|
||||||
const descChild = getBinaryNodeChild(group, 'description')
|
|
||||||
let desc: string | undefined
|
|
||||||
let descId: string | undefined
|
|
||||||
if(descChild) {
|
|
||||||
desc = getBinaryNodeChild(descChild, 'body')?.content as string
|
|
||||||
descId = descChild.attrs.id
|
|
||||||
}
|
|
||||||
const groupId = group.attrs.id.includes('@') ? group.attrs.id : jidEncode(group.attrs.id, 'g.us')
|
|
||||||
const metadata: GroupMetadata = {
|
|
||||||
id: groupId,
|
|
||||||
subject: group.attrs.subject,
|
|
||||||
creation: +group.attrs.creation,
|
|
||||||
owner: group.attrs.creator,
|
|
||||||
desc,
|
|
||||||
descId,
|
|
||||||
restrict: !!getBinaryNodeChild(result, 'locked'),
|
|
||||||
announce: !!getBinaryNodeChild(result, 'announcement'),
|
|
||||||
participants: getBinaryNodeChildren(group, 'participant').map(
|
|
||||||
({ attrs }) => {
|
|
||||||
return {
|
|
||||||
id: attrs.jid,
|
|
||||||
admin: attrs.type || null as any,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeGroupsSocket = (config: SocketConfig) => {
|
export const makeGroupsSocket = (config: SocketConfig) => {
|
||||||
const sock = makeChatsSocket(config)
|
const sock = makeSocket(config)
|
||||||
const { query } = sock
|
const { query } = sock
|
||||||
|
|
||||||
const groupQuery = async(jid: string, type: 'get' | 'set', content: BinaryNode[]) => (
|
const groupQuery = async(jid: string, type: 'get' | 'set', content: BinaryNode[]) => (
|
||||||
@@ -146,4 +115,36 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
await groupQuery(jid, 'set', [ { tag: setting, attrs: { } } ])
|
await groupQuery(jid, 'set', [ { tag: setting, attrs: { } } ])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const extractGroupMetadata = (result: BinaryNode) => {
|
||||||
|
const group = getBinaryNodeChild(result, 'group')
|
||||||
|
const descChild = getBinaryNodeChild(group, 'description')
|
||||||
|
let desc: string | undefined
|
||||||
|
let descId: string | undefined
|
||||||
|
if(descChild) {
|
||||||
|
desc = getBinaryNodeChild(descChild, 'body')?.content as string
|
||||||
|
descId = descChild.attrs.id
|
||||||
|
}
|
||||||
|
const groupId = group.attrs.id.includes('@') ? group.attrs.id : jidEncode(group.attrs.id, 'g.us')
|
||||||
|
const metadata: GroupMetadata = {
|
||||||
|
id: groupId,
|
||||||
|
subject: group.attrs.subject,
|
||||||
|
creation: +group.attrs.creation,
|
||||||
|
owner: group.attrs.creator,
|
||||||
|
desc,
|
||||||
|
descId,
|
||||||
|
restrict: !!getBinaryNodeChild(result, 'locked'),
|
||||||
|
announce: !!getBinaryNodeChild(result, 'announcement'),
|
||||||
|
participants: getBinaryNodeChildren(group, 'participant').map(
|
||||||
|
({ attrs }) => {
|
||||||
|
return {
|
||||||
|
id: attrs.jid,
|
||||||
|
admin: attrs.type || null as any,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return metadata
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SocketConfig } from '../Types'
|
import { SocketConfig } from '../Types'
|
||||||
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
||||||
import { makeMessagesSocket as _makeSocket } from './messages-send'
|
import { makeChatsSocket as _makeSocket } from './chats'
|
||||||
|
|
||||||
// export the last socket layer
|
// export the last socket layer
|
||||||
const makeWASocket = (config: Partial<SocketConfig>) => (
|
const makeWASocket = (config: Partial<SocketConfig>) => (
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
|
|
||||||
import { makeGroupsSocket } from "./groups"
|
|
||||||
import { SocketConfig, WAMessageStubType, ParticipantAction, Chat, GroupMetadata } from "../Types"
|
import { SocketConfig, WAMessageStubType, ParticipantAction, Chat, GroupMetadata } from "../Types"
|
||||||
import { decodeMessageStanza, encodeBigEndian, toNumber } from "../Utils"
|
import { decodeMessageStanza, encodeBigEndian, toNumber } from "../Utils"
|
||||||
import { BinaryNode, jidDecode, jidEncode, isJidStatusBroadcast, areJidsSameUser, getBinaryNodeChildren, jidNormalizedUser } from '../WABinary'
|
import { BinaryNode, jidDecode, jidEncode, isJidStatusBroadcast, areJidsSameUser, getBinaryNodeChildren, jidNormalizedUser } from '../WABinary'
|
||||||
import { downloadIfHistory } from '../Utils/history'
|
import { downloadHistory } from '../Utils/history'
|
||||||
import { proto } from "../../WAProto"
|
import { proto } from "../../WAProto"
|
||||||
import { generateSignalPubKey, xmppPreKey, xmppSignedPreKey } from "../Utils/signal"
|
import { generateSignalPubKey, xmppPreKey, xmppSignedPreKey } from "../Utils/signal"
|
||||||
import { KEY_BUNDLE_TYPE } from "../Defaults"
|
import { KEY_BUNDLE_TYPE } from "../Defaults"
|
||||||
|
import { makeMessagesSocket } from "./messages-send"
|
||||||
|
|
||||||
export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||||
const { logger } = config
|
const { logger } = config
|
||||||
const sock = makeGroupsSocket(config)
|
const sock = makeMessagesSocket(config)
|
||||||
const {
|
const {
|
||||||
ev,
|
ev,
|
||||||
authState,
|
authState,
|
||||||
ws,
|
ws,
|
||||||
assertingPreKeys,
|
assertingPreKeys,
|
||||||
sendNode,
|
sendNode,
|
||||||
|
relayMessage,
|
||||||
} = sock
|
} = sock
|
||||||
|
|
||||||
const sendMessageAck = async({ attrs }: BinaryNode) => {
|
const sendMessageAck = async({ attrs }: BinaryNode) => {
|
||||||
@@ -103,11 +104,45 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
const protocolMsg = message.message?.protocolMessage
|
const protocolMsg = message.message?.protocolMessage
|
||||||
if(protocolMsg) {
|
if(protocolMsg) {
|
||||||
switch(protocolMsg.type) {
|
switch(protocolMsg.type) {
|
||||||
|
case proto.ProtocolMessage.ProtocolMessageType.HISTORY_SYNC_NOTIFICATION:
|
||||||
|
const history = await downloadHistory(protocolMsg!.historySyncNotification)
|
||||||
|
processHistoryMessage(history)
|
||||||
|
break
|
||||||
|
case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_REQUEST:
|
||||||
|
const keys = await Promise.all(
|
||||||
|
protocolMsg.appStateSyncKeyRequest!.keyIds!.map(
|
||||||
|
async id => {
|
||||||
|
const keyId = Buffer.from(id.keyId!).toString('base64')
|
||||||
|
const keyData = await authState.keys.getAppStateSyncKey(keyId)
|
||||||
|
logger.info({ keyId }, 'received key request')
|
||||||
|
return {
|
||||||
|
keyId: id,
|
||||||
|
keyData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const msg: proto.IMessage = {
|
||||||
|
protocolMessage: {
|
||||||
|
type: proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE,
|
||||||
|
appStateSyncKeyShare: {
|
||||||
|
keys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await relayMessage(message.key.remoteJid!, msg, { })
|
||||||
|
logger.info({ with: message.key.remoteJid! }, 'shared key')
|
||||||
|
break
|
||||||
case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE:
|
case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE:
|
||||||
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')
|
||||||
await authState.keys.setAppStateSyncKey(str, keyData)
|
await authState.keys.setAppStateSyncKey(str, keyData)
|
||||||
|
|
||||||
|
authState.creds.myAppStateKeyId = str
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.emit('auth-state.update', authState)
|
ev.emit('auth-state.update', authState)
|
||||||
break
|
break
|
||||||
case proto.ProtocolMessage.ProtocolMessageType.REVOKE:
|
case proto.ProtocolMessage.ProtocolMessageType.REVOKE:
|
||||||
@@ -303,14 +338,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
logger.debug({ msgId: dec.msgId }, 'send message receipt')
|
logger.debug({ msgId: dec.msgId }, 'send message receipt')
|
||||||
|
|
||||||
const possibleHistory = downloadIfHistory(msg)
|
const message = msg.deviceSentMessage?.message || msg
|
||||||
if(possibleHistory) {
|
|
||||||
const history = await possibleHistory
|
|
||||||
logger.info({ msgId: dec.msgId, type: history.syncType }, 'recv history')
|
|
||||||
|
|
||||||
processHistoryMessage(history)
|
|
||||||
} else {
|
|
||||||
const message = msg.deviceSentMessage?.message || msg
|
|
||||||
fullMessages.push({
|
fullMessages.push({
|
||||||
key: {
|
key: {
|
||||||
remoteJid: dec.chatId,
|
remoteJid: dec.chatId,
|
||||||
@@ -323,7 +351,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
messageTimestamp: dec.timestamp,
|
messageTimestamp: dec.timestamp,
|
||||||
pushName: dec.pushname
|
pushName: dec.pushname
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(dec.successes.length) {
|
if(dec.successes.length) {
|
||||||
@@ -434,7 +461,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
ev.emit('auth-state.update', authState)
|
ev.emit('auth-state.update', authState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await processMessage(msg, chat)
|
await processMessage(msg, chat)
|
||||||
if(!!msg.message && !msg.message!.protocolMessage) {
|
if(!!msg.message && !msg.message!.protocolMessage) {
|
||||||
chat.conversationTimestamp = toNumber(msg.messageTimestamp)
|
chat.conversationTimestamp = toNumber(msg.messageTimestamp)
|
||||||
@@ -453,5 +480,5 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return sock
|
return { ...sock, processMessage }
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
import { makeMessagesRecvSocket } from "./messages-recv"
|
import { SocketConfig, MediaConnInfo, AnyMessageContent, MiscMessageGenerationOptions, WAMediaUploadFunction, MessageRelayOptions } from "../Types"
|
||||||
import { SocketConfig, MediaConnInfo, AnyMessageContent, MiscMessageGenerationOptions, WAMediaUploadFunction } from "../Types"
|
|
||||||
import { encodeWAMessage, generateMessageID, generateWAMessage } from "../Utils"
|
import { encodeWAMessage, generateMessageID, generateWAMessage } from "../Utils"
|
||||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary'
|
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary'
|
||||||
import { proto } from "../../WAProto"
|
import { proto } from "../../WAProto"
|
||||||
@@ -8,10 +7,11 @@ import { encryptSenderKeyMsgSignalProto, encryptSignalProto, extractDeviceJids,
|
|||||||
import { WA_DEFAULT_EPHEMERAL, DEFAULT_ORIGIN, MEDIA_PATH_MAP } from "../Defaults"
|
import { WA_DEFAULT_EPHEMERAL, DEFAULT_ORIGIN, MEDIA_PATH_MAP } from "../Defaults"
|
||||||
import got from "got"
|
import got from "got"
|
||||||
import { Boom } from "@hapi/boom"
|
import { Boom } from "@hapi/boom"
|
||||||
|
import { makeGroupsSocket } from "./groups"
|
||||||
|
|
||||||
export const makeMessagesSocket = (config: SocketConfig) => {
|
export const makeMessagesSocket = (config: SocketConfig) => {
|
||||||
const { logger } = config
|
const { logger } = config
|
||||||
const sock = makeMessagesRecvSocket(config)
|
const sock = makeGroupsSocket(config)
|
||||||
const {
|
const {
|
||||||
ev,
|
ev,
|
||||||
authState,
|
authState,
|
||||||
@@ -84,10 +84,12 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getUSyncDevices = async(jids: string[], ignoreZeroDevices: boolean) => {
|
const getUSyncDevices = async(jids: string[], ignoreZeroDevices: boolean) => {
|
||||||
|
jids = Array.from(new Set(jids))
|
||||||
const users = jids.map<BinaryNode>(jid => ({
|
const users = jids.map<BinaryNode>(jid => ({
|
||||||
tag: 'user',
|
tag: 'user',
|
||||||
attrs: { jid: jidNormalizedUser(jid) }
|
attrs: { jid: jidNormalizedUser(jid) }
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const iq: BinaryNode = {
|
const iq: BinaryNode = {
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -175,7 +177,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
const relayMessage = async(jid: string, message: proto.IMessage, msgId?: string) => {
|
const relayMessage = async(
|
||||||
|
jid: string,
|
||||||
|
message: proto.IMessage,
|
||||||
|
{ messageId: msgId, cachedGroupMetadata }: MessageRelayOptions
|
||||||
|
) => {
|
||||||
const { user, server } = jidDecode(jid)
|
const { user, server } = jidDecode(jid)
|
||||||
const isGroup = server === 'g.us'
|
const isGroup = server === 'g.us'
|
||||||
msgId = msgId || generateMessageID()
|
msgId = msgId || generateMessageID()
|
||||||
@@ -188,7 +194,10 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
if(isGroup) {
|
if(isGroup) {
|
||||||
const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(destinationJid, encodedMsg, authState)
|
const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(destinationJid, encodedMsg, authState)
|
||||||
const groupData = await groupMetadata(jid)
|
|
||||||
|
let groupData = cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
||||||
|
if(!groupData) groupData = await groupMetadata(jid)
|
||||||
|
|
||||||
const participantsList = groupData.participants.map(p => p.id)
|
const participantsList = groupData.participants.map(p => p.id)
|
||||||
const devices = await getUSyncDevices(participantsList, false)
|
const devices = await getUSyncDevices(participantsList, false)
|
||||||
|
|
||||||
@@ -384,7 +393,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
upload: waUploadToServer
|
upload: waUploadToServer
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
await relayMessage(jid, fullMsg.message, fullMsg.key.id!)
|
await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id! })
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
ev.emit('messages.upsert', { messages: [fullMsg], type: 'append' })
|
ev.emit('messages.upsert', { messages: [fullMsg], type: 'append' })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Contact } from "./Contact"
|
import type { Contact } from "./Contact"
|
||||||
import type { proto } from "../../WAProto"
|
import type { proto } from "../../WAProto"
|
||||||
|
import type { WAPatchName, ChatMutation } from "./Chat"
|
||||||
|
|
||||||
export type KeyPair = { public: Uint8Array, private: Uint8Array }
|
export type KeyPair = { public: Uint8Array, private: Uint8Array }
|
||||||
export type SignedKeyPair = { keyPair: KeyPair, signature: Uint8Array, keyId: number }
|
export type SignedKeyPair = { keyPair: KeyPair, signature: Uint8Array, keyId: number }
|
||||||
@@ -13,7 +14,7 @@ export type SignalIdentity = {
|
|||||||
identifierKey: Uint8Array
|
identifierKey: Uint8Array
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CollectionType = 'regular_high' | 'regular_low' | 'critical_unblock_low' | 'critical_block'
|
export type LTHashState = { version: number, hash: Buffer, mutations: ChatMutation[] }
|
||||||
|
|
||||||
export type AuthenticationCreds = {
|
export type AuthenticationCreds = {
|
||||||
noiseKey: KeyPair
|
noiseKey: KeyPair
|
||||||
@@ -24,9 +25,6 @@ export type AuthenticationCreds = {
|
|||||||
me?: Contact
|
me?: Contact
|
||||||
account?: proto.ADVSignedDeviceIdentity
|
account?: proto.ADVSignedDeviceIdentity
|
||||||
signalIdentities?: SignalIdentity[]
|
signalIdentities?: SignalIdentity[]
|
||||||
appStateVersion?: {
|
|
||||||
[T in CollectionType]: number
|
|
||||||
}
|
|
||||||
myAppStateKeyId?: string
|
myAppStateKeyId?: string
|
||||||
firstUnuploadedPreKeyId: number
|
firstUnuploadedPreKeyId: number
|
||||||
serverHasPreKeys: boolean
|
serverHasPreKeys: boolean
|
||||||
@@ -45,6 +43,9 @@ export type SignalKeyStore = {
|
|||||||
|
|
||||||
getAppStateSyncKey: (id: string) => Awaitable<proto.IAppStateSyncKeyData>
|
getAppStateSyncKey: (id: string) => Awaitable<proto.IAppStateSyncKeyData>
|
||||||
setAppStateSyncKey: (id: string, item: proto.IAppStateSyncKeyData | null) => Awaitable<void>
|
setAppStateSyncKey: (id: string, item: proto.IAppStateSyncKeyData | null) => Awaitable<void>
|
||||||
|
|
||||||
|
getAppStateSyncVersion: (name: WAPatchName) => Awaitable<LTHashState>
|
||||||
|
setAppStateSyncVersion: (id: WAPatchName, item: LTHashState) => Awaitable<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthenticationState = {
|
export type AuthenticationState = {
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import type { proto } from "../../WAProto"
|
|||||||
/** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */
|
/** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */
|
||||||
export type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
|
export type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
|
||||||
|
|
||||||
|
export type WAPatchName = 'critical_block' | 'critical_unblock_low' | 'regular_low' | 'regular_high' | 'regular'
|
||||||
|
|
||||||
export interface PresenceData {
|
export interface PresenceData {
|
||||||
lastKnownPresence: WAPresence
|
lastKnownPresence: WAPresence
|
||||||
lastSeen?: number
|
lastSeen?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChatMutation = { action: proto.ISyncActionValue, index: [string, string] }
|
export type ChatMutation = { action: proto.ISyncActionValue, index: [string, string], indexMac: Uint8Array, valueMac: Uint8Array, operation: number }
|
||||||
|
|
||||||
export type Chat = Omit<proto.IConversation, 'messages'> & {
|
export type Chat = Omit<proto.IConversation, 'messages'> & {
|
||||||
/** unix timestamp of date when mute ends, if applicable */
|
/** unix timestamp of date when mute ends, if applicable */
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ReadStream } from "fs"
|
import type { ReadStream } from "fs"
|
||||||
import type { Logger } from "pino"
|
import type { Logger } from "pino"
|
||||||
import type { URL } from "url"
|
import type { URL } from "url"
|
||||||
|
import type { GroupMetadata } from "./GroupMetadata"
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
|
|
||||||
// export the WAMessage Prototypes
|
// export the WAMessage Prototypes
|
||||||
@@ -103,6 +104,13 @@ export type AnyMessageContent = AnyRegularMessageContent | {
|
|||||||
} | {
|
} | {
|
||||||
disappearingMessagesInChat: boolean | number
|
disappearingMessagesInChat: boolean | number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MessageRelayOptions = {
|
||||||
|
messageId?: string
|
||||||
|
cachedGroupMetadata?: (jid: string) => Promise<GroupMetadata | undefined>
|
||||||
|
//cachedDevices?: (jid: string) => Promise<string[] | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
export type MiscMessageGenerationOptions = {
|
export type MiscMessageGenerationOptions = {
|
||||||
/** Force message id */
|
/** Force message id */
|
||||||
messageId?: string
|
messageId?: string
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import { aesDecrypt, hmacSign, aesEncrypt, hkdf } from "./crypto"
|
import { aesDecrypt, hmacSign, aesEncrypt, hkdf } from "./crypto"
|
||||||
import { AuthenticationState, ChatModification, ChatMutation } from "../Types"
|
import { AuthenticationState, ChatModification, ChatMutation, WAPatchName, LTHashState } from "../Types"
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { LT_HASH_ANTI_TAMPERING } from '../WABinary/LTHash'
|
import { LT_HASH_ANTI_TAMPERING } from '../WABinary/LTHash'
|
||||||
|
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren } from '../WABinary'
|
||||||
|
import { toNumber } from './generics'
|
||||||
|
|
||||||
type SyncdType = 'regular_high' | 'regular_low'
|
export const mutationKeys = (keydata: Uint8Array) => {
|
||||||
|
|
||||||
const mutationKeys = (keydata: Uint8Array) => {
|
|
||||||
const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
||||||
return {
|
return {
|
||||||
indexKey: expanded.slice(0, 32),
|
indexKey: expanded.slice(0, 32),
|
||||||
@@ -48,22 +48,40 @@ const to64BitNetworkOrder = function(e) {
|
|||||||
return Buffer.from(t)
|
return Buffer.from(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateSnapshotMac = (version: number, indexMac: Uint8Array, valueMac: Uint8Array, type: SyncdType, key: Buffer) => {
|
type Mac = { indexMac: Uint8Array, valueMac: Uint8Array, operation: proto.SyncdMutation.SyncdMutationSyncdOperation }
|
||||||
|
|
||||||
const ltHash = () => {
|
const computeLtHash = (initial: Uint8Array, macs: Mac[], getPrevSetValueMac: (index: Uint8Array, internalIndex: number) => Uint8Array) => {
|
||||||
const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(new Uint8Array(128).buffer, [ new Uint8Array(valueMac).buffer, new Uint8Array(indexMac).buffer ], [])
|
const addBuffs: ArrayBuffer[] = []
|
||||||
const buff = Buffer.from(result)
|
const subBuffs: ArrayBuffer[] = []
|
||||||
console.log(buff.toString('hex'))
|
for(let i = 0; i < macs.length;i++) {
|
||||||
return buff
|
const { indexMac, valueMac, operation } = macs[i]
|
||||||
|
const subBuff = getPrevSetValueMac(indexMac, i)
|
||||||
|
if(operation === proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE) {
|
||||||
|
if(!subBuff) {
|
||||||
|
throw new Boom('')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addBuffs.push(new Uint8Array(valueMac).buffer)
|
||||||
|
}
|
||||||
|
if(subBuff) {
|
||||||
|
subBuffs.push(new Uint8Array(subBuff).buffer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const total = Buffer.concat([
|
|
||||||
ltHash(),
|
const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(new Uint8Array(initial).buffer, addBuffs, subBuffs)
|
||||||
to64BitNetworkOrder(version),
|
const buff = Buffer.from(result)
|
||||||
Buffer.from(type, 'utf-8')
|
return buff
|
||||||
])
|
|
||||||
return hmacSign(total, key)
|
|
||||||
}
|
}
|
||||||
const generatePatchMac = (snapshotMac: Uint8Array, valueMacs: Uint8Array[], version: number, type: SyncdType, key: Buffer) => {
|
|
||||||
|
export const generateSnapshotMac = (lthash: Uint8Array, version: number, name: WAPatchName, key: Buffer) => {
|
||||||
|
const total = Buffer.concat([
|
||||||
|
lthash,
|
||||||
|
to64BitNetworkOrder(version),
|
||||||
|
Buffer.from(name, 'utf-8')
|
||||||
|
])
|
||||||
|
return hmacSign(total, key, 'sha256')
|
||||||
|
}
|
||||||
|
const generatePatchMac = (snapshotMac: Uint8Array, valueMacs: Uint8Array[], version: number, type: WAPatchName, key: Buffer) => {
|
||||||
const total = Buffer.concat([
|
const total = Buffer.concat([
|
||||||
snapshotMac,
|
snapshotMac,
|
||||||
...valueMacs,
|
...valueMacs,
|
||||||
@@ -73,76 +91,83 @@ const generatePatchMac = (snapshotMac: Uint8Array, valueMacs: Uint8Array[], vers
|
|||||||
return hmacSign(total, key)
|
return hmacSign(total, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const encodeSyncdPatch = async(action: ChatModification, lastMessageKey: proto.IMessageKey, { creds: { appStateVersion, myAppStateKeyId }, keys }: AuthenticationState) => {
|
export const encodeSyncdPatch = async(
|
||||||
let syncAction: proto.ISyncActionValue = { }
|
syncAction: proto.ISyncActionValue,
|
||||||
if('archive' in action) {
|
index: [string, string],
|
||||||
syncAction.archiveChatAction = {
|
type: WAPatchName,
|
||||||
archived: action.archive,
|
operation: proto.SyncdMutation.SyncdMutationSyncdOperation,
|
||||||
messageRange: {
|
{ creds: { myAppStateKeyId }, keys }: AuthenticationState
|
||||||
messages: [
|
) => {
|
||||||
{ key: lastMessageKey }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if('mute' in action) {
|
|
||||||
const value = typeof action.mute === 'number' ? true : false
|
|
||||||
syncAction.muteAction = {
|
|
||||||
muted: value,
|
|
||||||
muteEndTimestamp: typeof action.mute === 'number' ? action.mute : undefined
|
|
||||||
}
|
|
||||||
} else if('delete' in action) {
|
|
||||||
syncAction.deleteChatAction = { }
|
|
||||||
} else if('markRead' in action) {
|
|
||||||
syncAction.markChatAsReadAction = {
|
|
||||||
read: action.markRead
|
|
||||||
}
|
|
||||||
} else if('pin' in action) {
|
|
||||||
throw new Boom('Pin not supported on multi-device yet', { statusCode: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const encoded = proto.SyncActionValue.encode(syncAction).finish()
|
|
||||||
const key = !!myAppStateKeyId ? await keys.getAppStateSyncKey(myAppStateKeyId) : undefined
|
const key = !!myAppStateKeyId ? await keys.getAppStateSyncKey(myAppStateKeyId) : undefined
|
||||||
if(!key) {
|
if(!key) {
|
||||||
throw new Boom(`myAppStateKey not present`, { statusCode: 404 })
|
throw new Boom(`myAppStateKey not present`, { statusCode: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
|
const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
|
||||||
|
|
||||||
const index = JSON.stringify([Object.keys(action)[0], lastMessageKey.remoteJid])
|
const state = { ...await keys.getAppStateSyncVersion(type) }
|
||||||
|
|
||||||
|
const indexBuffer = Buffer.from(JSON.stringify(index))
|
||||||
|
const encoded = proto.SyncActionData.encode({
|
||||||
|
index: indexBuffer,
|
||||||
|
value: syncAction,
|
||||||
|
padding: new Uint8Array(0),
|
||||||
|
version: 2
|
||||||
|
}).finish()
|
||||||
|
|
||||||
const keyValue = mutationKeys(key!.keyData!)
|
const keyValue = mutationKeys(key!.keyData!)
|
||||||
|
|
||||||
const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
|
const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
|
||||||
const macValue = generateMac(1, encValue, encKeyId, keyValue.valueMacKey)
|
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
|
||||||
const indexMacValue = hmacSign(Buffer.from(index), keyValue.indexKey)
|
const indexMac = hmacSign(indexBuffer, keyValue.indexKey)
|
||||||
|
|
||||||
const type = 'regular_high'
|
state.hash = computeLtHash(
|
||||||
const v = appStateVersion[type]+1
|
state.hash,
|
||||||
|
[ { indexMac, valueMac, operation } ],
|
||||||
|
(index) => [...state.mutations].reverse().find(m => Buffer.compare(m.indexMac, index) === 0)?.valueMac
|
||||||
|
)
|
||||||
|
state.version += 1
|
||||||
|
|
||||||
const snapshotMac = generateSnapshotMac(v, indexMacValue, macValue, type, keyValue.snapshotMacKey)
|
const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey)
|
||||||
|
|
||||||
const patch: proto.ISyncdPatch = {
|
const patch: proto.ISyncdPatch = {
|
||||||
patchMac: generatePatchMac(snapshotMac, [macValue], v, type, keyValue.patchMacKey),
|
patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey),
|
||||||
snapshotMac: snapshotMac,
|
snapshotMac: snapshotMac,
|
||||||
keyId: { id: encKeyId },
|
keyId: { id: encKeyId },
|
||||||
mutations: [
|
mutations: [
|
||||||
{
|
{
|
||||||
operation: 1,
|
operation: operation,
|
||||||
record: {
|
record: {
|
||||||
index: {
|
index: {
|
||||||
blob: indexMacValue
|
blob: indexMac
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
blob: Buffer.concat([ encValue, macValue ])
|
blob: Buffer.concat([ encValue, valueMac ])
|
||||||
},
|
},
|
||||||
keyId: { id: encKeyId }
|
keyId: { id: encKeyId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
return patch
|
|
||||||
|
state.mutations = [
|
||||||
|
...state.mutations,
|
||||||
|
{
|
||||||
|
action: syncAction,
|
||||||
|
index,
|
||||||
|
valueMac,
|
||||||
|
indexMac,
|
||||||
|
operation
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return { patch, state }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decodeSyncdPatch = async(msg: proto.ISyncdPatch, {keys}: AuthenticationState) => {
|
export const decodeSyncdPatch = async(
|
||||||
|
msg: proto.ISyncdPatch,
|
||||||
|
name: WAPatchName,
|
||||||
|
{keys}: AuthenticationState,
|
||||||
|
validateMacs: boolean = true
|
||||||
|
) => {
|
||||||
const keyCache: { [_: string]: ReturnType<typeof mutationKeys> } = { }
|
const keyCache: { [_: string]: ReturnType<typeof mutationKeys> } = { }
|
||||||
const getKey = async(keyId: Uint8Array) => {
|
const getKey = async(keyId: Uint8Array) => {
|
||||||
const base64Key = Buffer.from(keyId!).toString('base64')
|
const base64Key = Buffer.from(keyId!).toString('base64')
|
||||||
@@ -160,42 +185,144 @@ export const decodeSyncdPatch = async(msg: proto.ISyncdPatch, {keys}: Authentica
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mutations: ChatMutation[] = []
|
const mutations: ChatMutation[] = []
|
||||||
const failures: Boom[] = []
|
|
||||||
|
|
||||||
/*const mainKey = getKey(msg.keyId!.id)
|
if(validateMacs) {
|
||||||
const mutation = msg.mutations![0]!.record
|
const mainKey = await getKey(msg.keyId!.id)
|
||||||
|
const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32))
|
||||||
const patchMac = generatePatchMac(msg.snapshotMac, [ mutation.value!.blob!.slice(-32) ], toNumber(msg.version!.version), 'regular_low', mainKey.patchMacKey)
|
|
||||||
console.log(patchMac)
|
const patchMac = generatePatchMac(msg.snapshotMac, mutationmacs, toNumber(msg.version!.version), name, mainKey.patchMacKey)
|
||||||
console.log(msg.patchMac)*/
|
if(Buffer.compare(patchMac, msg.patchMac) !== 0) {
|
||||||
|
throw new Boom('Invalid patch mac')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// indexKey used to HMAC sign record.index.blob
|
// indexKey used to HMAC sign record.index.blob
|
||||||
// valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
|
// valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
|
||||||
// the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId
|
// the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId
|
||||||
for(const { operation, record } of msg.mutations!) {
|
for(const { operation, record } of msg.mutations!) {
|
||||||
try {
|
const key = await getKey(record.keyId!.id!)
|
||||||
const key = await getKey(record.keyId!.id!)
|
const content = Buffer.from(record.value!.blob!)
|
||||||
const content = Buffer.from(record.value!.blob!)
|
const encContent = content.slice(0, -32)
|
||||||
const encContent = content.slice(0, -32)
|
const ogValueMac = content.slice(-32)
|
||||||
|
if(validateMacs) {
|
||||||
const contentHmac = generateMac(operation, encContent, record.keyId!.id!, key.valueMacKey)
|
const contentHmac = generateMac(operation, encContent, record.keyId!.id!, key.valueMacKey)
|
||||||
if(Buffer.compare(contentHmac, content.slice(-32)) !== 0) {
|
if(Buffer.compare(contentHmac, ogValueMac) !== 0) {
|
||||||
throw new Boom('HMAC content verification failed')
|
throw new Boom('HMAC content verification failed')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = aesDecrypt(encContent, key.valueEncryptionKey)
|
const result = aesDecrypt(encContent, key.valueEncryptionKey)
|
||||||
const syncAction = proto.SyncActionData.decode(result)
|
const syncAction = proto.SyncActionData.decode(result)
|
||||||
|
|
||||||
|
if(validateMacs) {
|
||||||
const hmac = hmacSign(syncAction.index, key.indexKey)
|
const hmac = hmacSign(syncAction.index, key.indexKey)
|
||||||
if(Buffer.compare(hmac, record.index!.blob) !== 0) {
|
if(Buffer.compare(hmac, record.index!.blob) !== 0) {
|
||||||
throw new Boom('HMAC index verification failed')
|
throw new Boom('HMAC index verification failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexStr = Buffer.from(syncAction.index).toString()
|
|
||||||
mutations.push({ action: syncAction.value!, index: JSON.parse(indexStr) })
|
|
||||||
} catch(error) {
|
|
||||||
failures.push(new Boom(error, { data: { operation, record } }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const indexStr = Buffer.from(syncAction.index).toString()
|
||||||
|
mutations.push({
|
||||||
|
action: syncAction.value!,
|
||||||
|
index: JSON.parse(indexStr),
|
||||||
|
indexMac: record.index!.blob!,
|
||||||
|
valueMac: ogValueMac,
|
||||||
|
operation: operation
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mutations, failures }
|
return { mutations }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractSyncdPatches = (result: BinaryNode) => {
|
||||||
|
const syncNode = getBinaryNodeChild(result, 'sync')
|
||||||
|
const collectionNode = getBinaryNodeChild(syncNode, 'collection')
|
||||||
|
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
|
||||||
|
|
||||||
|
const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
|
||||||
|
const syncds: proto.ISyncdPatch[] = []
|
||||||
|
const name = collectionNode.attrs.name as WAPatchName
|
||||||
|
for(let { content } of patches) {
|
||||||
|
if(content) {
|
||||||
|
const syncd = proto.SyncdPatch.decode(content! as Uint8Array)
|
||||||
|
if(!syncd.version) {
|
||||||
|
syncd.version = { version: +collectionNode.attrs.version+1 }
|
||||||
|
}
|
||||||
|
syncds.push(syncd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { syncds, name }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decodePatches = async(
|
||||||
|
{ syncds, name }: ReturnType<typeof extractSyncdPatches>,
|
||||||
|
initial: LTHashState,
|
||||||
|
auth: AuthenticationState,
|
||||||
|
validateMacs: boolean = true
|
||||||
|
) => {
|
||||||
|
const successfulMutations: ChatMutation[] = []
|
||||||
|
|
||||||
|
let current = initial.hash
|
||||||
|
let currentVersion = initial.version
|
||||||
|
for(const syncd of syncds) {
|
||||||
|
const { mutations, version, keyId, snapshotMac } = syncd
|
||||||
|
const macs = mutations.map(
|
||||||
|
m => ({
|
||||||
|
operation: m.operation!,
|
||||||
|
indexMac: m.record.index!.blob!,
|
||||||
|
valueMac: m.record.value!.blob!.slice(-32)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
currentVersion = toNumber(version.version!)
|
||||||
|
|
||||||
|
current = computeLtHash(current, macs, (index, maxIndex) => {
|
||||||
|
let value: Uint8Array
|
||||||
|
for(const item of initial.mutations) {
|
||||||
|
if(Buffer.compare(item.indexMac, index) === 0) {
|
||||||
|
value = item.valueMac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(const { version, mutations } of syncds) {
|
||||||
|
const versionNum = toNumber(version.version!)
|
||||||
|
const mutationIdx = mutations.findIndex(m => {
|
||||||
|
return Buffer.compare(m.record!.index!.blob, index) === 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if(mutationIdx >= 0 && (versionNum < currentVersion || mutationIdx < maxIndex)) {
|
||||||
|
value = mutations[mutationIdx].record!.value!.blob!.slice(-32)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(versionNum >= currentVersion) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
|
||||||
|
if(validateMacs) {
|
||||||
|
const base64Key = Buffer.from(keyId!.id!).toString('base64')
|
||||||
|
const keyEnc = await auth.keys.getAppStateSyncKey(base64Key)
|
||||||
|
if(!keyEnc) {
|
||||||
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 500 })
|
||||||
|
}
|
||||||
|
const result = mutationKeys(keyEnc.keyData!)
|
||||||
|
const computedSnapshotMac = generateSnapshotMac(current, currentVersion, name, result.snapshotMacKey)
|
||||||
|
if(Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
|
||||||
|
throw new Boom(`failed to verify LTHash at ${currentVersion}`, { statusCode: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodeResult = await decodeSyncdPatch(syncd, name, auth!, validateMacs)
|
||||||
|
console.log(currentVersion, decodeResult.mutations)
|
||||||
|
successfulMutations.push(...decodeResult.mutations)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
newMutations: successfulMutations,
|
||||||
|
state: {
|
||||||
|
hash: current,
|
||||||
|
version: currentVersion,
|
||||||
|
mutations: [...initial.mutations, ...successfulMutations]
|
||||||
|
} as LTHashState
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,6 @@ import { inflate } from "zlib";
|
|||||||
|
|
||||||
const inflatePromise = promisify(inflate)
|
const inflatePromise = promisify(inflate)
|
||||||
|
|
||||||
export const downloadIfHistory = (message: proto.IMessage) => {
|
|
||||||
if(message.protocolMessage?.historySyncNotification) {
|
|
||||||
return downloadHistory(message.protocolMessage!.historySyncNotification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const downloadHistory = async(msg: proto.IHistorySyncNotification) => {
|
export const downloadHistory = async(msg: proto.IHistorySyncNotification) => {
|
||||||
const stream = await downloadContentFromMessage(msg, 'history')
|
const stream = await downloadContentFromMessage(msg, 'history')
|
||||||
let buffer = Buffer.from([])
|
let buffer = Buffer.from([])
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import type { AuthenticationState, SocketConfig, SignalKeyStore, AuthenticationCreds, KeyPair } from "../Types"
|
import type { AuthenticationState, SocketConfig, SignalKeyStore, AuthenticationCreds, KeyPair, LTHashState } from "../Types"
|
||||||
import { Curve, hmacSign, signedKeyPair } from './crypto'
|
import { Curve, hmacSign, signedKeyPair } from './crypto'
|
||||||
import { encodeInt, generateRegistrationId } from './generics'
|
import { encodeInt, generateRegistrationId } from './generics'
|
||||||
import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary } from '../WABinary'
|
import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary } from '../WABinary'
|
||||||
@@ -83,22 +83,25 @@ export const generateRegistrationNode = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const initInMemoryKeyStore = (
|
export const initInMemoryKeyStore = (
|
||||||
{ preKeys, sessions, senderKeys, appStateSyncKeys }: {
|
{ preKeys, sessions, senderKeys, appStateSyncKeys, appStateVersions }: {
|
||||||
preKeys?: { [k: number]: KeyPair },
|
preKeys?: { [k: number]: KeyPair },
|
||||||
sessions?: { [k: string]: any },
|
sessions?: { [k: string]: any },
|
||||||
senderKeys?: { [k: string]: any }
|
senderKeys?: { [k: string]: any }
|
||||||
appStateSyncKeys?: { [k: string]: proto.IAppStateSyncKeyData }
|
appStateSyncKeys?: { [k: string]: proto.IAppStateSyncKeyData },
|
||||||
|
appStateVersions?: { [k: string]: LTHashState },
|
||||||
} = { },
|
} = { },
|
||||||
) => {
|
) => {
|
||||||
preKeys = preKeys || { }
|
preKeys = preKeys || { }
|
||||||
sessions = sessions || { }
|
sessions = sessions || { }
|
||||||
senderKeys = senderKeys || { }
|
senderKeys = senderKeys || { }
|
||||||
appStateSyncKeys = appStateSyncKeys || { }
|
appStateSyncKeys = appStateSyncKeys || { }
|
||||||
|
appStateVersions = appStateVersions || { }
|
||||||
return {
|
return {
|
||||||
preKeys,
|
preKeys,
|
||||||
sessions,
|
sessions,
|
||||||
senderKeys,
|
senderKeys,
|
||||||
appStateSyncKeys,
|
appStateSyncKeys,
|
||||||
|
appStateVersions,
|
||||||
getPreKey: keyId => preKeys[keyId],
|
getPreKey: keyId => preKeys[keyId],
|
||||||
setPreKey: (keyId, pair) => {
|
setPreKey: (keyId, pair) => {
|
||||||
if(pair) preKeys[keyId] = pair
|
if(pair) preKeys[keyId] = pair
|
||||||
@@ -125,6 +128,16 @@ export const initInMemoryKeyStore = (
|
|||||||
setAppStateSyncKey: (id, item) => {
|
setAppStateSyncKey: (id, item) => {
|
||||||
if(item) appStateSyncKeys[id] = item
|
if(item) appStateSyncKeys[id] = item
|
||||||
else delete appStateSyncKeys[id]
|
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
|
} as SignalKeyStore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ class d {
|
|||||||
}
|
}
|
||||||
_addSingle(e, t) {
|
_addSingle(e, t) {
|
||||||
var r = this;
|
var r = this;
|
||||||
const n = new Uint8Array(hkdf(t, o, { info: r.salt })).buffer;
|
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer;
|
||||||
return r.performPointwiseWithOverflow(e, n, ((e,t)=>e + t))
|
return r.performPointwiseWithOverflow(e, n, ((e,t)=>e + t))
|
||||||
}
|
}
|
||||||
_subtractSingle(e, t) {
|
_subtractSingle(e, t) {
|
||||||
var r = this;
|
var r = this;
|
||||||
|
|
||||||
const n = new Uint8Array(hkdf(t, o, { info: r.salt })).buffer;
|
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer;
|
||||||
return r.performPointwiseWithOverflow(e, n, ((e,t)=>e - t))
|
return r.performPointwiseWithOverflow(e, n, ((e,t)=>e - t))
|
||||||
}
|
}
|
||||||
performPointwiseWithOverflow(e, t, r) {
|
performPointwiseWithOverflow(e, t, r) {
|
||||||
|
|||||||
Reference in New Issue
Block a user