diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index a3c4b25..8052a5b 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -1,5 +1,5 @@ import { decodeSyncdPatch, encodeSyncdPatch } from "../Utils/chat-utils"; -import { SocketConfig, WAPresence, PresenceData, Chat, ChatModification, WAMediaUpload } from "../Types"; +import { SocketConfig, WAPresence, PresenceData, Chat, ChatModification, WAMediaUpload, ChatMutation } from "../Types"; import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, S_WHATSAPP_NET } from "../WABinary"; import { makeSocket } from "./socket"; import { proto } from '../../WAProto' @@ -278,7 +278,7 @@ export const makeChatsSocket = (config: SocketConfig) => { } } - const processSyncActions = (actions: { action: proto.ISyncActionValue, index: [string, string] }[]) => { + const processSyncActions = (actions: ChatMutation[]) => { const updates: Partial[] = [] for(const { action, index: [_, id] } of actions) { const update: Partial = { id } @@ -309,7 +309,7 @@ export const makeChatsSocket = (config: SocketConfig) => { jid: string, modification: ChatModification ) => { - const patch = encodeSyncdPatch(modification, { remoteJid: jid }, authState) + const patch = await encodeSyncdPatch(modification, { remoteJid: jid }, authState) const type = 'regular_high' const ver = authState.creds.appStateVersion![type] || 0 const node: BinaryNode = { @@ -373,24 +373,24 @@ export const makeChatsSocket = (config: SocketConfig) => { const patchesNode = getBinaryNodeChild(collectionNode, 'patches') const patches = getBinaryNodeChildren(patchesNode, 'patch') - const successfulMutations = patches.flatMap(({ content }) => { + 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 } = decodeSyncdPatch(syncd, authState) + 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' ) } - return mutations + successfulMutations.push(...mutations) } - return [] - }) + } processSyncActions(successfulMutations) ev.emit('auth-state.update', authState) } diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 33c68da..095a098 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -99,16 +99,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { }) } - const processMessage = (message: proto.IWebMessageInfo, chatUpdate: Partial) => { + const processMessage = async(message: proto.IWebMessageInfo, chatUpdate: Partial) => { const protocolMsg = message.message?.protocolMessage if(protocolMsg) { switch(protocolMsg.type) { case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE: - const newKeys = JSON.parse(JSON.stringify(protocolMsg.appStateSyncKeyShare!.keys)) - authState.creds.appStateSyncKeys = [ - ...(authState.creds.appStateSyncKeys || []), - ...newKeys - ] + for(const { keyData, keyId } of protocolMsg.appStateSyncKeyShare!.keys || []) { + const str = Buffer.from(keyId.keyId!).toString('base64') + await authState.keys.setAppStateSyncKey(str, keyData) + } ev.emit('auth-state.update', authState) break case proto.ProtocolMessage.ProtocolMessageType.REVOKE: @@ -417,10 +416,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } }) - ev.on('messages.upsert', ({ messages }) => { + ev.on('messages.upsert', async({ messages }) => { const chat: Partial = { id: messages[0].key.remoteJid } for(const msg of messages) { - processMessage(msg, chat) + await processMessage(msg, chat) if(!!msg.message && !msg.message!.protocolMessage) { chat.conversationTimestamp = toNumber(msg.messageTimestamp) if(!msg.key.fromMe) { diff --git a/src/Types/Auth.ts b/src/Types/Auth.ts index 5ff963e..eeb7c97 100644 --- a/src/Types/Auth.ts +++ b/src/Types/Auth.ts @@ -24,11 +24,10 @@ export type AuthenticationCreds = { me?: Contact account?: proto.ADVSignedDeviceIdentity signalIdentities?: SignalIdentity[] - appStateSyncKeys?: proto.IAppStateSyncKey[] appStateVersion?: { [T in CollectionType]: number } - + myAppStateKeyId?: string firstUnuploadedPreKeyId: number serverHasPreKeys: boolean nextPreKeyId: number @@ -43,6 +42,9 @@ export type SignalKeyStore = { getSenderKey: (id: string) => Awaitable setSenderKey: (id: string, item: any | null) => Awaitable + + getAppStateSyncKey: (id: string) => Awaitable + setAppStateSyncKey: (id: string, item: proto.IAppStateSyncKeyData | null) => Awaitable } export type AuthenticationState = { diff --git a/src/Types/Chat.ts b/src/Types/Chat.ts index 3fbad52..c6cd774 100644 --- a/src/Types/Chat.ts +++ b/src/Types/Chat.ts @@ -8,6 +8,8 @@ export interface PresenceData { lastSeen?: number } +export type ChatMutation = { action: proto.ISyncActionValue, index: [string, string] } + export type Chat = Omit & { /** unix timestamp of date when mute ends, if applicable */ mute?: number | null diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index 35c02b6..0ed8a28 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -1,13 +1,13 @@ import { Boom } from '@hapi/boom' import { aesDecrypt, hmacSign, aesEncrypt, hkdf } from "./crypto" -import { AuthenticationState, ChatModification } from "../Types" +import { AuthenticationState, ChatModification, ChatMutation } from "../Types" import { proto } from '../../WAProto' import { LT_HASH_ANTI_TAMPERING } from '../WABinary/LTHash' type SyncdType = 'regular_high' | 'regular_low' -const mutationKeys = (keydata: string) => { - const expanded = hkdf(Buffer.from(keydata, 'base64'), 160, { info: 'WhatsApp Mutation Keys' }) +const mutationKeys = (keydata: Uint8Array) => { + const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' }) return { indexKey: expanded.slice(0, 32), valueEncryptionKey: expanded.slice(32, 64), @@ -73,7 +73,7 @@ const generatePatchMac = (snapshotMac: Uint8Array, valueMacs: Uint8Array[], vers return hmacSign(total, key) } -export const encodeSyncdPatch = (action: ChatModification, lastMessageKey: proto.IMessageKey, { creds: { appStateSyncKeys: [key], appStateVersion } }: AuthenticationState) => { +export const encodeSyncdPatch = async(action: ChatModification, lastMessageKey: proto.IMessageKey, { creds: { appStateVersion, myAppStateKeyId }, keys }: AuthenticationState) => { let syncAction: proto.ISyncActionValue = { } if('archive' in action) { syncAction.archiveChatAction = { @@ -101,12 +101,18 @@ export const encodeSyncdPatch = (action: ChatModification, lastMessageKey: proto } const encoded = proto.SyncActionValue.encode(syncAction).finish() + const key = !!myAppStateKeyId ? await keys.getAppStateSyncKey(myAppStateKeyId) : undefined + if(!key) { + throw new Boom(`myAppStateKey not present`, { statusCode: 404 }) + } + + const encKeyId = Buffer.from(myAppStateKeyId, 'base64') const index = JSON.stringify([Object.keys(action)[0], lastMessageKey.remoteJid]) - const keyValue = mutationKeys(key.keyData!.keyData! as any) + const keyValue = mutationKeys(key!.keyData!) const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey) - const macValue = generateMac(1, encValue, key.keyId!.keyId, keyValue.valueMacKey) + const macValue = generateMac(1, encValue, encKeyId, keyValue.valueMacKey) const indexMacValue = hmacSign(Buffer.from(index), keyValue.indexKey) const type = 'regular_high' @@ -117,7 +123,7 @@ export const encodeSyncdPatch = (action: ChatModification, lastMessageKey: proto const patch: proto.ISyncdPatch = { patchMac: generatePatchMac(snapshotMac, [macValue], v, type, keyValue.patchMacKey), snapshotMac: snapshotMac, - keyId: { id: key.keyId.keyId }, + keyId: { id: encKeyId }, mutations: [ { operation: 1, @@ -128,7 +134,7 @@ export const encodeSyncdPatch = (action: ChatModification, lastMessageKey: proto value: { blob: Buffer.concat([ encValue, macValue ]) }, - keyId: { id: key.keyId.keyId } + keyId: { id: encKeyId } } } ] @@ -136,27 +142,24 @@ export const encodeSyncdPatch = (action: ChatModification, lastMessageKey: proto return patch } -export const decodeSyncdPatch = (msg: proto.ISyncdPatch, {creds}: AuthenticationState) => { +export const decodeSyncdPatch = async(msg: proto.ISyncdPatch, {keys}: AuthenticationState) => { const keyCache: { [_: string]: ReturnType } = { } - const getKey = (keyId: Uint8Array) => { + const getKey = async(keyId: Uint8Array) => { const base64Key = Buffer.from(keyId!).toString('base64') - let key = keyCache[base64Key] if(!key) { - const keyEnc = creds.appStateSyncKeys?.find(k => ( - (k.keyId!.keyId as any) === base64Key - )) + const keyEnc = await keys.getAppStateSyncKey(base64Key) if(!keyEnc) { throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 500, data: msg }) } - const result = mutationKeys(keyEnc.keyData!.keyData as any) + const result = mutationKeys(keyEnc.keyData!) keyCache[base64Key] = result key = result } return key } - const mutations: { action: proto.ISyncActionValue, index: [string, string] }[] = [] + const mutations: ChatMutation[] = [] const failures: Boom[] = [] /*const mainKey = getKey(msg.keyId!.id) @@ -171,7 +174,7 @@ export const decodeSyncdPatch = (msg: proto.ISyncdPatch, {creds}: Authentication // 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!) { try { - const key = getKey(record.keyId!.id!) + const key = await getKey(record.keyId!.id!) const content = Buffer.from(record.value!.blob!) const encContent = content.slice(0, -32) const contentHmac = generateMac(operation, encContent, record.keyId!.id!, key.valueMacKey) diff --git a/src/Utils/crypto.ts b/src/Utils/crypto.ts index 201ae7e..02e8f08 100644 --- a/src/Utils/crypto.ts +++ b/src/Utils/crypto.ts @@ -70,7 +70,7 @@ export function sha256(buffer: Buffer) { } // HKDF key expansion // from: https://github.com/benadida/node-hkdf -export function hkdf(buffer: Buffer, expandedLength: number, { info, salt }: { salt?: Buffer, info?: string }) { +export function hkdf(buffer: Uint8Array, expandedLength: number, { info, salt }: { salt?: Buffer, info?: string }) { const hashAlg = 'sha256' const hashLength = 32 salt = salt || Buffer.alloc(hashLength) diff --git a/src/Utils/validate-connection.ts b/src/Utils/validate-connection.ts index 02446e1..b327d31 100644 --- a/src/Utils/validate-connection.ts +++ b/src/Utils/validate-connection.ts @@ -83,19 +83,22 @@ export const generateRegistrationNode = ( } export const initInMemoryKeyStore = ( - { preKeys, sessions, senderKeys }: { + { preKeys, sessions, senderKeys, appStateSyncKeys }: { preKeys?: { [k: number]: KeyPair }, sessions?: { [k: string]: any }, senderKeys?: { [k: string]: any } + appStateSyncKeys?: { [k: string]: proto.IAppStateSyncKeyData } } = { }, ) => { preKeys = preKeys || { } sessions = sessions || { } senderKeys = senderKeys || { } + appStateSyncKeys = appStateSyncKeys || { } return { preKeys, sessions, senderKeys, + appStateSyncKeys, getPreKey: keyId => preKeys[keyId], setPreKey: (keyId, pair) => { if(pair) preKeys[keyId] = pair @@ -112,6 +115,16 @@ export const initInMemoryKeyStore = ( 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] } } as SignalKeyStore } diff --git a/yarn.lock b/yarn.lock index e4c9a2a..42d1dd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,11 +2,6 @@ # yarn lockfile v1 -"@adiwajshing/keyed-db@^0.2.4": - version "0.2.4" - resolved "https://registry.yarnpkg.com/@adiwajshing/keyed-db/-/keyed-db-0.2.4.tgz#2a09e88fce20b2672deb60a7750c5fe3ab0dfd99" - integrity sha512-yprSnAtj80/VKuDqRcFFLDYltoNV8tChNwFfIgcf6PGD4sjzWIBgs08pRuTqGH5mk5wgL6PBRSsMCZqtZwzFEw== - "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"