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,12 +1,12 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
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 { LT_HASH_ANTI_TAMPERING } from '../WABinary/LTHash'
|
||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren } from '../WABinary'
|
||||
import { toNumber } from './generics'
|
||||
|
||||
type SyncdType = 'regular_high' | 'regular_low'
|
||||
|
||||
const mutationKeys = (keydata: Uint8Array) => {
|
||||
export const mutationKeys = (keydata: Uint8Array) => {
|
||||
const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
||||
return {
|
||||
indexKey: expanded.slice(0, 32),
|
||||
@@ -48,22 +48,40 @@ const to64BitNetworkOrder = function(e) {
|
||||
return Buffer.from(t)
|
||||
}
|
||||
|
||||
const generateSnapshotMac = (version: number, indexMac: Uint8Array, valueMac: Uint8Array, type: SyncdType, key: Buffer) => {
|
||||
|
||||
const ltHash = () => {
|
||||
const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(new Uint8Array(128).buffer, [ new Uint8Array(valueMac).buffer, new Uint8Array(indexMac).buffer ], [])
|
||||
const buff = Buffer.from(result)
|
||||
console.log(buff.toString('hex'))
|
||||
return buff
|
||||
type Mac = { indexMac: Uint8Array, valueMac: Uint8Array, operation: proto.SyncdMutation.SyncdMutationSyncdOperation }
|
||||
|
||||
const computeLtHash = (initial: Uint8Array, macs: Mac[], getPrevSetValueMac: (index: Uint8Array, internalIndex: number) => Uint8Array) => {
|
||||
const addBuffs: ArrayBuffer[] = []
|
||||
const subBuffs: ArrayBuffer[] = []
|
||||
for(let i = 0; i < macs.length;i++) {
|
||||
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(),
|
||||
to64BitNetworkOrder(version),
|
||||
Buffer.from(type, 'utf-8')
|
||||
])
|
||||
return hmacSign(total, key)
|
||||
|
||||
const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(new Uint8Array(initial).buffer, addBuffs, subBuffs)
|
||||
const buff = Buffer.from(result)
|
||||
return buff
|
||||
}
|
||||
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([
|
||||
snapshotMac,
|
||||
...valueMacs,
|
||||
@@ -73,76 +91,83 @@ const generatePatchMac = (snapshotMac: Uint8Array, valueMacs: Uint8Array[], vers
|
||||
return hmacSign(total, key)
|
||||
}
|
||||
|
||||
export const encodeSyncdPatch = async(action: ChatModification, lastMessageKey: proto.IMessageKey, { creds: { appStateVersion, myAppStateKeyId }, keys }: AuthenticationState) => {
|
||||
let syncAction: proto.ISyncActionValue = { }
|
||||
if('archive' in action) {
|
||||
syncAction.archiveChatAction = {
|
||||
archived: action.archive,
|
||||
messageRange: {
|
||||
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()
|
||||
export const encodeSyncdPatch = async(
|
||||
syncAction: proto.ISyncActionValue,
|
||||
index: [string, string],
|
||||
type: WAPatchName,
|
||||
operation: proto.SyncdMutation.SyncdMutationSyncdOperation,
|
||||
{ creds: { myAppStateKeyId }, keys }: AuthenticationState
|
||||
) => {
|
||||
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 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 encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
|
||||
const macValue = generateMac(1, encValue, encKeyId, keyValue.valueMacKey)
|
||||
const indexMacValue = hmacSign(Buffer.from(index), keyValue.indexKey)
|
||||
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
|
||||
const indexMac = hmacSign(indexBuffer, keyValue.indexKey)
|
||||
|
||||
const type = 'regular_high'
|
||||
const v = appStateVersion[type]+1
|
||||
state.hash = computeLtHash(
|
||||
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 = {
|
||||
patchMac: generatePatchMac(snapshotMac, [macValue], v, type, keyValue.patchMacKey),
|
||||
patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey),
|
||||
snapshotMac: snapshotMac,
|
||||
keyId: { id: encKeyId },
|
||||
mutations: [
|
||||
{
|
||||
operation: 1,
|
||||
operation: operation,
|
||||
record: {
|
||||
index: {
|
||||
blob: indexMacValue
|
||||
blob: indexMac
|
||||
},
|
||||
value: {
|
||||
blob: Buffer.concat([ encValue, macValue ])
|
||||
blob: Buffer.concat([ encValue, valueMac ])
|
||||
},
|
||||
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 getKey = async(keyId: Uint8Array) => {
|
||||
const base64Key = Buffer.from(keyId!).toString('base64')
|
||||
@@ -160,42 +185,144 @@ export const decodeSyncdPatch = async(msg: proto.ISyncdPatch, {keys}: Authentica
|
||||
}
|
||||
|
||||
const mutations: ChatMutation[] = []
|
||||
const failures: Boom[] = []
|
||||
|
||||
/*const mainKey = getKey(msg.keyId!.id)
|
||||
const mutation = msg.mutations![0]!.record
|
||||
|
||||
const patchMac = generatePatchMac(msg.snapshotMac, [ mutation.value!.blob!.slice(-32) ], toNumber(msg.version!.version), 'regular_low', mainKey.patchMacKey)
|
||||
console.log(patchMac)
|
||||
console.log(msg.patchMac)*/
|
||||
if(validateMacs) {
|
||||
const mainKey = await getKey(msg.keyId!.id)
|
||||
const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32))
|
||||
|
||||
const patchMac = generatePatchMac(msg.snapshotMac, mutationmacs, toNumber(msg.version!.version), name, mainKey.patchMacKey)
|
||||
if(Buffer.compare(patchMac, msg.patchMac) !== 0) {
|
||||
throw new Boom('Invalid patch mac')
|
||||
}
|
||||
}
|
||||
|
||||
// indexKey used to HMAC sign record.index.blob
|
||||
// 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
|
||||
for(const { operation, record } of msg.mutations!) {
|
||||
try {
|
||||
const key = await getKey(record.keyId!.id!)
|
||||
const content = Buffer.from(record.value!.blob!)
|
||||
const encContent = content.slice(0, -32)
|
||||
const key = await getKey(record.keyId!.id!)
|
||||
const content = Buffer.from(record.value!.blob!)
|
||||
const encContent = content.slice(0, -32)
|
||||
const ogValueMac = content.slice(-32)
|
||||
if(validateMacs) {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
const result = aesDecrypt(encContent, key.valueEncryptionKey)
|
||||
const syncAction = proto.SyncActionData.decode(result)
|
||||
const result = aesDecrypt(encContent, key.valueEncryptionKey)
|
||||
const syncAction = proto.SyncActionData.decode(result)
|
||||
|
||||
if(validateMacs) {
|
||||
const hmac = hmacSign(syncAction.index, key.indexKey)
|
||||
if(Buffer.compare(hmac, record.index!.blob) !== 0) {
|
||||
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)
|
||||
|
||||
export const downloadIfHistory = (message: proto.IMessage) => {
|
||||
if(message.protocolMessage?.historySyncNotification) {
|
||||
return downloadHistory(message.protocolMessage!.historySyncNotification)
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadHistory = async(msg: proto.IHistorySyncNotification) => {
|
||||
const stream = await downloadContentFromMessage(msg, 'history')
|
||||
let buffer = Buffer.from([])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { randomBytes } from 'crypto'
|
||||
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 { encodeInt, generateRegistrationId } from './generics'
|
||||
import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary } from '../WABinary'
|
||||
@@ -83,22 +83,25 @@ export const generateRegistrationNode = (
|
||||
}
|
||||
|
||||
export const initInMemoryKeyStore = (
|
||||
{ preKeys, sessions, senderKeys, appStateSyncKeys }: {
|
||||
{ preKeys, sessions, senderKeys, appStateSyncKeys, appStateVersions }: {
|
||||
preKeys?: { [k: number]: KeyPair },
|
||||
sessions?: { [k: string]: any },
|
||||
senderKeys?: { [k: string]: any }
|
||||
appStateSyncKeys?: { [k: string]: proto.IAppStateSyncKeyData }
|
||||
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
|
||||
@@ -125,6 +128,16 @@ export const initInMemoryKeyStore = (
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user