mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
chore: add linting
This commit is contained in:
@@ -2,9 +2,9 @@ import { Boom } from '@hapi/boom'
|
||||
import { randomBytes } from 'crypto'
|
||||
import type { Logger } from 'pino'
|
||||
import { proto } from '../../WAProto'
|
||||
import type { AuthenticationCreds, AuthenticationState, SignalDataTypeMap, SignalDataSet, SignalKeyStore, SignalKeyStoreWithTransaction } from "../Types"
|
||||
import type { AuthenticationCreds, AuthenticationState, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction } from '../Types'
|
||||
import { Curve, signedKeyPair } from './crypto'
|
||||
import { generateRegistrationId, BufferJSON } from './generics'
|
||||
import { BufferJSON, generateRegistrationId } from './generics'
|
||||
|
||||
const KEY_MAP: { [T in keyof SignalDataTypeMap]: string } = {
|
||||
'pre-key': 'preKeys',
|
||||
@@ -46,6 +46,7 @@ export const addTransactionCapability = (state: SignalKeyStore, logger: Logger):
|
||||
if(value) {
|
||||
dict[id] = value
|
||||
}
|
||||
|
||||
return dict
|
||||
}, { }
|
||||
)
|
||||
@@ -55,7 +56,7 @@ export const addTransactionCapability = (state: SignalKeyStore, logger: Logger):
|
||||
},
|
||||
set: data => {
|
||||
if(inTransaction) {
|
||||
logger.trace({ types: Object.keys(data) }, `caching in transaction`)
|
||||
logger.trace({ types: Object.keys(data) }, 'caching in transaction')
|
||||
for(const key in data) {
|
||||
transactionCache[key] = transactionCache[key] || { }
|
||||
Object.assign(transactionCache[key], data[key])
|
||||
@@ -69,7 +70,7 @@ export const addTransactionCapability = (state: SignalKeyStore, logger: Logger):
|
||||
},
|
||||
isInTransaction: () => inTransaction,
|
||||
prefetch: (type, ids) => {
|
||||
logger.trace({ type, ids }, `prefetching`)
|
||||
logger.trace({ type, ids }, 'prefetching')
|
||||
return prefetch(type, ids)
|
||||
},
|
||||
transaction: async(work) => {
|
||||
@@ -128,17 +129,17 @@ export const useSingleFileAuthState = (filename: string, logger?: Logger): { sta
|
||||
)
|
||||
}
|
||||
|
||||
if(existsSync(filename)) {
|
||||
const result = JSON.parse(
|
||||
readFileSync(filename, { encoding: 'utf-8' }),
|
||||
BufferJSON.reviver
|
||||
)
|
||||
if(existsSync(filename)) {
|
||||
const result = JSON.parse(
|
||||
readFileSync(filename, { encoding: 'utf-8' }),
|
||||
BufferJSON.reviver
|
||||
)
|
||||
creds = result.creds
|
||||
keys = result.keys
|
||||
} else {
|
||||
creds = initAuthCreds()
|
||||
keys = { }
|
||||
}
|
||||
} else {
|
||||
creds = initAuthCreds()
|
||||
keys = { }
|
||||
}
|
||||
|
||||
return {
|
||||
state: {
|
||||
@@ -153,8 +154,10 @@ export const useSingleFileAuthState = (filename: string, logger?: Logger): { sta
|
||||
if(type === 'app-state-sync-key') {
|
||||
value = proto.AppStateSyncKeyData.fromObject(value)
|
||||
}
|
||||
|
||||
dict[id] = value
|
||||
}
|
||||
|
||||
return dict
|
||||
}, { }
|
||||
)
|
||||
@@ -165,6 +168,7 @@ export const useSingleFileAuthState = (filename: string, logger?: Logger): { sta
|
||||
keys[key] = keys[key] || { }
|
||||
Object.assign(keys[key], data[_key])
|
||||
}
|
||||
|
||||
saveState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,523 +1,541 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { aesDecrypt, hmacSign, aesEncrypt, hkdf } from "./crypto"
|
||||
import { WAPatchCreate, ChatMutation, WAPatchName, LTHashState, ChatModification, LastMessageList } from "../Types"
|
||||
import { proto } from '../../WAProto'
|
||||
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
|
||||
import { ChatModification, ChatMutation, LastMessageList, LTHashState, WAPatchCreate, WAPatchName } from '../Types'
|
||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren } from '../WABinary'
|
||||
import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from './crypto'
|
||||
import { toNumber } from './generics'
|
||||
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
|
||||
import { downloadContentFromMessage, } from './messages-media'
|
||||
|
||||
type FetchAppStateSyncKey = (keyId: string) => Promise<proto.IAppStateSyncKeyData> | proto.IAppStateSyncKeyData
|
||||
|
||||
const mutationKeys = (keydata: Uint8Array) => {
|
||||
const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
||||
return {
|
||||
indexKey: expanded.slice(0, 32),
|
||||
valueEncryptionKey: expanded.slice(32, 64),
|
||||
valueMacKey: expanded.slice(64, 96),
|
||||
snapshotMacKey: expanded.slice(96, 128),
|
||||
patchMacKey: expanded.slice(128, 160)
|
||||
}
|
||||
const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
||||
return {
|
||||
indexKey: expanded.slice(0, 32),
|
||||
valueEncryptionKey: expanded.slice(32, 64),
|
||||
valueMacKey: expanded.slice(64, 96),
|
||||
snapshotMacKey: expanded.slice(96, 128),
|
||||
patchMacKey: expanded.slice(128, 160)
|
||||
}
|
||||
}
|
||||
|
||||
const generateMac = (operation: proto.SyncdMutation.SyncdMutationSyncdOperation, data: Buffer, keyId: Uint8Array | string, key: Buffer) => {
|
||||
const getKeyData = () => {
|
||||
let r: number
|
||||
switch (operation) {
|
||||
case proto.SyncdMutation.SyncdMutationSyncdOperation.SET:
|
||||
r = 0x01
|
||||
break
|
||||
case proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE:
|
||||
r = 0x02
|
||||
break
|
||||
}
|
||||
const buff = Buffer.from([r])
|
||||
return Buffer.concat([ buff, Buffer.from(keyId as any, 'base64') ])
|
||||
}
|
||||
const keyData = getKeyData()
|
||||
const getKeyData = () => {
|
||||
let r: number
|
||||
switch (operation) {
|
||||
case proto.SyncdMutation.SyncdMutationSyncdOperation.SET:
|
||||
r = 0x01
|
||||
break
|
||||
case proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE:
|
||||
r = 0x02
|
||||
break
|
||||
}
|
||||
|
||||
const last = Buffer.alloc(8) // 8 bytes
|
||||
last.set([ keyData.length ], last.length-1)
|
||||
const buff = Buffer.from([r])
|
||||
return Buffer.concat([ buff, Buffer.from(keyId as any, 'base64') ])
|
||||
}
|
||||
|
||||
const total = Buffer.concat([ keyData, data, last ])
|
||||
const hmac = hmacSign(total, key, 'sha512')
|
||||
const keyData = getKeyData()
|
||||
|
||||
return hmac.slice(0, 32)
|
||||
const last = Buffer.alloc(8) // 8 bytes
|
||||
last.set([ keyData.length ], last.length-1)
|
||||
|
||||
const total = Buffer.concat([ keyData, data, last ])
|
||||
const hmac = hmacSign(total, key, 'sha512')
|
||||
|
||||
return hmac.slice(0, 32)
|
||||
}
|
||||
|
||||
const to64BitNetworkOrder = function(e) {
|
||||
const t = new ArrayBuffer(8)
|
||||
new DataView(t).setUint32(4, e, !1)
|
||||
return Buffer.from(t)
|
||||
const to64BitNetworkOrder = (e: number) => {
|
||||
const t = new ArrayBuffer(8)
|
||||
new DataView(t).setUint32(4, e, !1)
|
||||
return Buffer.from(t)
|
||||
}
|
||||
|
||||
type Mac = { indexMac: Uint8Array, valueMac: Uint8Array, operation: proto.SyncdMutation.SyncdMutationSyncdOperation }
|
||||
|
||||
const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' | 'indexValueMap'>) => {
|
||||
indexValueMap = { ...indexValueMap }
|
||||
const addBuffs: ArrayBuffer[] = []
|
||||
const subBuffs: ArrayBuffer[] = []
|
||||
indexValueMap = { ...indexValueMap }
|
||||
const addBuffs: ArrayBuffer[] = []
|
||||
const subBuffs: ArrayBuffer[] = []
|
||||
|
||||
return {
|
||||
mix: ({ indexMac, valueMac, operation }: Mac) => {
|
||||
const indexMacBase64 = Buffer.from(indexMac).toString('base64')
|
||||
const prevOp = indexValueMap[indexMacBase64]
|
||||
if(operation === proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE) {
|
||||
if(!prevOp) {
|
||||
throw new Boom('tried remove, but no previous op', { data: { indexMac, valueMac } })
|
||||
}
|
||||
// remove from index value mac, since this mutation is erased
|
||||
delete indexValueMap[indexMacBase64]
|
||||
} else {
|
||||
addBuffs.push(new Uint8Array(valueMac).buffer)
|
||||
// add this index into the history map
|
||||
indexValueMap[indexMacBase64] = { valueMac }
|
||||
}
|
||||
if(prevOp) {
|
||||
subBuffs.push(new Uint8Array(prevOp.valueMac).buffer)
|
||||
}
|
||||
},
|
||||
finish: () => {
|
||||
const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(new Uint8Array(hash).buffer, addBuffs, subBuffs)
|
||||
const buffer = Buffer.from(result)
|
||||
return {
|
||||
mix: ({ indexMac, valueMac, operation }: Mac) => {
|
||||
const indexMacBase64 = Buffer.from(indexMac).toString('base64')
|
||||
const prevOp = indexValueMap[indexMacBase64]
|
||||
if(operation === proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE) {
|
||||
if(!prevOp) {
|
||||
throw new Boom('tried remove, but no previous op', { data: { indexMac, valueMac } })
|
||||
}
|
||||
|
||||
return {
|
||||
hash: buffer,
|
||||
indexValueMap
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove from index value mac, since this mutation is erased
|
||||
delete indexValueMap[indexMacBase64]
|
||||
} else {
|
||||
addBuffs.push(new Uint8Array(valueMac).buffer)
|
||||
// add this index into the history map
|
||||
indexValueMap[indexMacBase64] = { valueMac }
|
||||
}
|
||||
|
||||
if(prevOp) {
|
||||
subBuffs.push(new Uint8Array(prevOp.valueMac).buffer)
|
||||
}
|
||||
},
|
||||
finish: () => {
|
||||
const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(new Uint8Array(hash).buffer, addBuffs, subBuffs)
|
||||
const buffer = Buffer.from(result)
|
||||
|
||||
return {
|
||||
hash: buffer,
|
||||
indexValueMap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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,
|
||||
to64BitNetworkOrder(version),
|
||||
Buffer.from(type, 'utf-8')
|
||||
])
|
||||
return hmacSign(total, key)
|
||||
const total = Buffer.concat([
|
||||
snapshotMac,
|
||||
...valueMacs,
|
||||
to64BitNetworkOrder(version),
|
||||
Buffer.from(type, 'utf-8')
|
||||
])
|
||||
return hmacSign(total, key)
|
||||
}
|
||||
|
||||
export const newLTHashState = (): LTHashState => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} })
|
||||
|
||||
export const encodeSyncdPatch = async(
|
||||
{ type, index, syncAction, apiVersion, operation }: WAPatchCreate,
|
||||
myAppStateKeyId: string,
|
||||
state: LTHashState,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey
|
||||
{ type, index, syncAction, apiVersion, operation }: WAPatchCreate,
|
||||
myAppStateKeyId: string,
|
||||
state: LTHashState,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey
|
||||
) => {
|
||||
const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
|
||||
if(!key) {
|
||||
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 })
|
||||
}
|
||||
const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
|
||||
const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
|
||||
if(!key) {
|
||||
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 })
|
||||
}
|
||||
|
||||
state = { ...state, indexValueMap: { ...state.indexValueMap } }
|
||||
const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
|
||||
|
||||
const indexBuffer = Buffer.from(JSON.stringify(index))
|
||||
const dataProto = proto.SyncActionData.fromObject({
|
||||
index: indexBuffer,
|
||||
value: syncAction,
|
||||
padding: new Uint8Array(0),
|
||||
version: apiVersion
|
||||
})
|
||||
const encoded = proto.SyncActionData.encode(dataProto).finish()
|
||||
state = { ...state, indexValueMap: { ...state.indexValueMap } }
|
||||
|
||||
const keyValue = mutationKeys(key!.keyData!)
|
||||
const indexBuffer = Buffer.from(JSON.stringify(index))
|
||||
const dataProto = proto.SyncActionData.fromObject({
|
||||
index: indexBuffer,
|
||||
value: syncAction,
|
||||
padding: new Uint8Array(0),
|
||||
version: apiVersion
|
||||
})
|
||||
const encoded = proto.SyncActionData.encode(dataProto).finish()
|
||||
|
||||
const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
|
||||
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
|
||||
const indexMac = hmacSign(indexBuffer, keyValue.indexKey)
|
||||
const keyValue = mutationKeys(key!.keyData!)
|
||||
|
||||
// update LT hash
|
||||
const generator = makeLtHashGenerator(state)
|
||||
generator.mix({ indexMac, valueMac, operation })
|
||||
Object.assign(state, generator.finish())
|
||||
const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
|
||||
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
|
||||
const indexMac = hmacSign(indexBuffer, keyValue.indexKey)
|
||||
|
||||
state.version += 1
|
||||
// update LT hash
|
||||
const generator = makeLtHashGenerator(state)
|
||||
generator.mix({ indexMac, valueMac, operation })
|
||||
Object.assign(state, generator.finish())
|
||||
|
||||
const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey)
|
||||
state.version += 1
|
||||
|
||||
const patch: proto.ISyncdPatch = {
|
||||
patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey),
|
||||
snapshotMac: snapshotMac,
|
||||
keyId: { id: encKeyId },
|
||||
mutations: [
|
||||
{
|
||||
operation: operation,
|
||||
record: {
|
||||
index: {
|
||||
blob: indexMac
|
||||
},
|
||||
value: {
|
||||
blob: Buffer.concat([ encValue, valueMac ])
|
||||
},
|
||||
keyId: { id: encKeyId }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey)
|
||||
|
||||
const base64Index = indexMac.toString('base64')
|
||||
state.indexValueMap[base64Index] = { valueMac }
|
||||
const patch: proto.ISyncdPatch = {
|
||||
patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey),
|
||||
snapshotMac: snapshotMac,
|
||||
keyId: { id: encKeyId },
|
||||
mutations: [
|
||||
{
|
||||
operation: operation,
|
||||
record: {
|
||||
index: {
|
||||
blob: indexMac
|
||||
},
|
||||
value: {
|
||||
blob: Buffer.concat([ encValue, valueMac ])
|
||||
},
|
||||
keyId: { id: encKeyId }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return { patch, state }
|
||||
const base64Index = indexMac.toString('base64')
|
||||
state.indexValueMap[base64Index] = { valueMac }
|
||||
|
||||
return { patch, state }
|
||||
}
|
||||
|
||||
export const decodeSyncdMutations = async(
|
||||
msgMutations: (proto.ISyncdMutation | proto.ISyncdRecord)[],
|
||||
initialState: LTHashState,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||
validateMacs: boolean
|
||||
msgMutations: (proto.ISyncdMutation | proto.ISyncdRecord)[],
|
||||
initialState: LTHashState,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||
validateMacs: boolean
|
||||
) => {
|
||||
const keyCache: { [_: string]: ReturnType<typeof mutationKeys> } = { }
|
||||
const getKey = async(keyId: Uint8Array) => {
|
||||
const base64Key = Buffer.from(keyId!).toString('base64')
|
||||
let key = keyCache[base64Key]
|
||||
if(!key) {
|
||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||
if(!keyEnc) {
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404, data: { msgMutations } })
|
||||
}
|
||||
const result = mutationKeys(keyEnc.keyData!)
|
||||
keyCache[base64Key] = result
|
||||
key = result
|
||||
}
|
||||
return key
|
||||
}
|
||||
const keyCache: { [_: string]: ReturnType<typeof mutationKeys> } = { }
|
||||
const getKey = async(keyId: Uint8Array) => {
|
||||
const base64Key = Buffer.from(keyId!).toString('base64')
|
||||
let key = keyCache[base64Key]
|
||||
if(!key) {
|
||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||
if(!keyEnc) {
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404, data: { msgMutations } })
|
||||
}
|
||||
|
||||
const ltGenerator = makeLtHashGenerator(initialState)
|
||||
const result = mutationKeys(keyEnc.keyData!)
|
||||
keyCache[base64Key] = result
|
||||
key = result
|
||||
}
|
||||
|
||||
const mutations: ChatMutation[] = []
|
||||
// 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 msgMutation of msgMutations!) {
|
||||
// if it's a syncdmutation, get the operation property
|
||||
// otherwise, if it's only a record -- it'll be a SET mutation
|
||||
const operation = 'operation' in msgMutation ? msgMutation.operation : proto.SyncdMutation.SyncdMutationSyncdOperation.SET
|
||||
const record = ('record' in msgMutation && !!msgMutation.record) ? msgMutation.record : msgMutation as proto.ISyncdRecord
|
||||
return key
|
||||
}
|
||||
|
||||
const ltGenerator = makeLtHashGenerator(initialState)
|
||||
|
||||
const mutations: ChatMutation[] = []
|
||||
// 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 msgMutation of msgMutations!) {
|
||||
// if it's a syncdmutation, get the operation property
|
||||
// otherwise, if it's only a record -- it'll be a SET mutation
|
||||
const operation = 'operation' in msgMutation ? msgMutation.operation : proto.SyncdMutation.SyncdMutationSyncdOperation.SET
|
||||
const record = ('record' in msgMutation && !!msgMutation.record) ? msgMutation.record : msgMutation as proto.ISyncdRecord
|
||||
|
||||
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, ogValueMac) !== 0) {
|
||||
throw new Boom('HMAC content verification failed')
|
||||
}
|
||||
}
|
||||
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, 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')
|
||||
}
|
||||
}
|
||||
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({
|
||||
syncAction,
|
||||
index: JSON.parse(indexStr),
|
||||
})
|
||||
ltGenerator.mix({
|
||||
indexMac: record.index!.blob!,
|
||||
valueMac: ogValueMac,
|
||||
operation: operation
|
||||
})
|
||||
}
|
||||
const indexStr = Buffer.from(syncAction.index).toString()
|
||||
mutations.push({
|
||||
syncAction,
|
||||
index: JSON.parse(indexStr),
|
||||
})
|
||||
ltGenerator.mix({
|
||||
indexMac: record.index!.blob!,
|
||||
valueMac: ogValueMac,
|
||||
operation: operation
|
||||
})
|
||||
}
|
||||
|
||||
return { mutations, ...ltGenerator.finish() }
|
||||
return { mutations, ...ltGenerator.finish() }
|
||||
}
|
||||
|
||||
export const decodeSyncdPatch = async(
|
||||
msg: proto.ISyncdPatch,
|
||||
name: WAPatchName,
|
||||
initialState: LTHashState,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||
validateMacs: boolean
|
||||
msg: proto.ISyncdPatch,
|
||||
name: WAPatchName,
|
||||
initialState: LTHashState,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||
validateMacs: boolean
|
||||
) => {
|
||||
if(validateMacs) {
|
||||
const base64Key = Buffer.from(msg.keyId!.id).toString('base64')
|
||||
const mainKeyObj = await getAppStateSyncKey(base64Key)
|
||||
const mainKey = mutationKeys(mainKeyObj.keyData!)
|
||||
const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32))
|
||||
if(validateMacs) {
|
||||
const base64Key = Buffer.from(msg.keyId!.id).toString('base64')
|
||||
const mainKeyObj = await getAppStateSyncKey(base64Key)
|
||||
const mainKey = mutationKeys(mainKeyObj.keyData!)
|
||||
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')
|
||||
}
|
||||
}
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
const result = await decodeSyncdMutations(msg!.mutations!, initialState, getAppStateSyncKey, validateMacs)
|
||||
return result
|
||||
const result = await decodeSyncdMutations(msg!.mutations!, initialState, getAppStateSyncKey, validateMacs)
|
||||
return result
|
||||
}
|
||||
|
||||
export const extractSyncdPatches = async(result: BinaryNode) => {
|
||||
const syncNode = getBinaryNodeChild(result, 'sync')
|
||||
const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
|
||||
const syncNode = getBinaryNodeChild(result, 'sync')
|
||||
const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
|
||||
|
||||
const final = { } as { [T in WAPatchName]: { patches: proto.ISyncdPatch[], hasMorePatches: boolean, snapshot?: proto.ISyncdSnapshot } }
|
||||
await Promise.all(
|
||||
collectionNodes.map(
|
||||
async collectionNode => {
|
||||
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
|
||||
const final = { } as { [T in WAPatchName]: { patches: proto.ISyncdPatch[], hasMorePatches: boolean, snapshot?: proto.ISyncdSnapshot } }
|
||||
await Promise.all(
|
||||
collectionNodes.map(
|
||||
async collectionNode => {
|
||||
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
|
||||
|
||||
const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
|
||||
const snapshotNode = getBinaryNodeChild(collectionNode, 'snapshot')
|
||||
const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
|
||||
const snapshotNode = getBinaryNodeChild(collectionNode, 'snapshot')
|
||||
|
||||
const syncds: proto.ISyncdPatch[] = []
|
||||
const name = collectionNode.attrs.name as WAPatchName
|
||||
const syncds: proto.ISyncdPatch[] = []
|
||||
const name = collectionNode.attrs.name as WAPatchName
|
||||
|
||||
const hasMorePatches = collectionNode.attrs.has_more_patches == 'true'
|
||||
const hasMorePatches = collectionNode.attrs.has_more_patches === 'true'
|
||||
|
||||
let snapshot: proto.ISyncdSnapshot | undefined = undefined
|
||||
if(snapshotNode && !!snapshotNode.content) {
|
||||
if(!Buffer.isBuffer(snapshotNode)) {
|
||||
snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
|
||||
}
|
||||
const blobRef = proto.ExternalBlobReference.decode(
|
||||
let snapshot: proto.ISyncdSnapshot | undefined = undefined
|
||||
if(snapshotNode && !!snapshotNode.content) {
|
||||
if(!Buffer.isBuffer(snapshotNode)) {
|
||||
snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
|
||||
}
|
||||
|
||||
const blobRef = proto.ExternalBlobReference.decode(
|
||||
snapshotNode.content! as Buffer
|
||||
)
|
||||
const data = await downloadExternalBlob(blobRef)
|
||||
snapshot = proto.SyncdSnapshot.decode(data)
|
||||
}
|
||||
)
|
||||
const data = await downloadExternalBlob(blobRef)
|
||||
snapshot = proto.SyncdSnapshot.decode(data)
|
||||
}
|
||||
|
||||
for(let { content } of patches) {
|
||||
if(content) {
|
||||
if(!Buffer.isBuffer(content)) {
|
||||
content = Buffer.from(Object.values(content))
|
||||
}
|
||||
const syncd = proto.SyncdPatch.decode(content! as Uint8Array)
|
||||
if(!syncd.version) {
|
||||
syncd.version = { version: +collectionNode.attrs.version+1 }
|
||||
}
|
||||
syncds.push(syncd)
|
||||
}
|
||||
}
|
||||
for(let { content } of patches) {
|
||||
if(content) {
|
||||
if(!Buffer.isBuffer(content)) {
|
||||
content = Buffer.from(Object.values(content))
|
||||
}
|
||||
|
||||
const syncd = proto.SyncdPatch.decode(content! as Uint8Array)
|
||||
if(!syncd.version) {
|
||||
syncd.version = { version: +collectionNode.attrs.version+1 }
|
||||
}
|
||||
|
||||
syncds.push(syncd)
|
||||
}
|
||||
}
|
||||
|
||||
final[name] = { patches: syncds, hasMorePatches, snapshot }
|
||||
}
|
||||
)
|
||||
)
|
||||
final[name] = { patches: syncds, hasMorePatches, snapshot }
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return final
|
||||
return final
|
||||
}
|
||||
|
||||
|
||||
export const downloadExternalBlob = async(blob: proto.IExternalBlobReference) => {
|
||||
const stream = await downloadContentFromMessage(blob, 'md-app-state')
|
||||
const stream = await downloadContentFromMessage(blob, 'md-app-state')
|
||||
let buffer = Buffer.from([])
|
||||
for await(const chunk of stream) {
|
||||
for await (const chunk of stream) {
|
||||
buffer = Buffer.concat([buffer, chunk])
|
||||
}
|
||||
return buffer
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
export const downloadExternalPatch = async(blob: proto.IExternalBlobReference) => {
|
||||
const buffer = await downloadExternalBlob(blob)
|
||||
const buffer = await downloadExternalBlob(blob)
|
||||
const syncData = proto.SyncdMutations.decode(buffer)
|
||||
return syncData
|
||||
}
|
||||
|
||||
export const decodeSyncdSnapshot = async(
|
||||
name: WAPatchName,
|
||||
snapshot: proto.ISyncdSnapshot,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||
minimumVersionNumber: number | undefined,
|
||||
validateMacs: boolean = true
|
||||
name: WAPatchName,
|
||||
snapshot: proto.ISyncdSnapshot,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||
minimumVersionNumber: number | undefined,
|
||||
validateMacs: boolean = true
|
||||
) => {
|
||||
const newState = newLTHashState()
|
||||
newState.version = toNumber(snapshot.version!.version!)
|
||||
const newState = newLTHashState()
|
||||
newState.version = toNumber(snapshot.version!.version!)
|
||||
|
||||
let { hash, indexValueMap, mutations } = await decodeSyncdMutations(snapshot.records!, newState, getAppStateSyncKey, validateMacs)
|
||||
newState.hash = hash
|
||||
newState.indexValueMap = indexValueMap
|
||||
const { hash, indexValueMap, mutations } = await decodeSyncdMutations(snapshot.records!, newState, getAppStateSyncKey, validateMacs)
|
||||
newState.hash = hash
|
||||
newState.indexValueMap = indexValueMap
|
||||
|
||||
if(validateMacs) {
|
||||
const base64Key = Buffer.from(snapshot.keyId!.id!).toString('base64')
|
||||
const keyEnc = await 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(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||
if(Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
|
||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`, { statusCode: 500 })
|
||||
}
|
||||
}
|
||||
if(validateMacs) {
|
||||
const base64Key = Buffer.from(snapshot.keyId!.id!).toString('base64')
|
||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||
if(!keyEnc) {
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 500 })
|
||||
}
|
||||
|
||||
const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber
|
||||
if(!areMutationsRequired) {
|
||||
mutations = []
|
||||
}
|
||||
const result = mutationKeys(keyEnc.keyData!)
|
||||
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||
if(Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
|
||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`, { statusCode: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
mutations
|
||||
}
|
||||
const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber
|
||||
if(!areMutationsRequired) {
|
||||
// clear array
|
||||
mutations.splice(0, mutations.length)
|
||||
}
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
mutations
|
||||
}
|
||||
}
|
||||
|
||||
export const decodePatches = async(
|
||||
name: WAPatchName,
|
||||
syncds: proto.ISyncdPatch[],
|
||||
initial: LTHashState,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||
minimumVersionNumber?: number,
|
||||
validateMacs: boolean = true
|
||||
name: WAPatchName,
|
||||
syncds: proto.ISyncdPatch[],
|
||||
initial: LTHashState,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||
minimumVersionNumber?: number,
|
||||
validateMacs: boolean = true
|
||||
) => {
|
||||
const successfulMutations: ChatMutation[] = []
|
||||
const successfulMutations: ChatMutation[] = []
|
||||
|
||||
const newState: LTHashState = {
|
||||
...initial,
|
||||
indexValueMap: { ...initial.indexValueMap }
|
||||
}
|
||||
const newState: LTHashState = {
|
||||
...initial,
|
||||
indexValueMap: { ...initial.indexValueMap }
|
||||
}
|
||||
|
||||
for(const syncd of syncds) {
|
||||
const { version, keyId, snapshotMac } = syncd
|
||||
if(syncd.externalMutations) {
|
||||
const ref = await downloadExternalPatch(syncd.externalMutations)
|
||||
syncd.mutations.push(...ref.mutations)
|
||||
}
|
||||
for(const syncd of syncds) {
|
||||
const { version, keyId, snapshotMac } = syncd
|
||||
if(syncd.externalMutations) {
|
||||
const ref = await downloadExternalPatch(syncd.externalMutations)
|
||||
syncd.mutations.push(...ref.mutations)
|
||||
}
|
||||
|
||||
const patchVersion = toNumber(version.version!)
|
||||
const patchVersion = toNumber(version.version!)
|
||||
|
||||
newState.version = patchVersion
|
||||
newState.version = patchVersion
|
||||
|
||||
const decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, validateMacs)
|
||||
const decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, validateMacs)
|
||||
|
||||
newState.hash = decodeResult.hash
|
||||
newState.indexValueMap = decodeResult.indexValueMap
|
||||
if(typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber) {
|
||||
successfulMutations.push(...decodeResult.mutations)
|
||||
}
|
||||
newState.hash = decodeResult.hash
|
||||
newState.indexValueMap = decodeResult.indexValueMap
|
||||
if(typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber) {
|
||||
successfulMutations.push(...decodeResult.mutations)
|
||||
}
|
||||
|
||||
if(validateMacs) {
|
||||
const base64Key = Buffer.from(keyId!.id!).toString('base64')
|
||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||
if(!keyEnc) {
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
||||
}
|
||||
const result = mutationKeys(keyEnc.keyData!)
|
||||
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||
if(Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
|
||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
newMutations: successfulMutations,
|
||||
state: newState
|
||||
}
|
||||
if(validateMacs) {
|
||||
const base64Key = Buffer.from(keyId!.id!).toString('base64')
|
||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||
if(!keyEnc) {
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
||||
}
|
||||
|
||||
const result = mutationKeys(keyEnc.keyData!)
|
||||
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||
if(Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
|
||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
newMutations: successfulMutations,
|
||||
state: newState
|
||||
}
|
||||
}
|
||||
|
||||
export const chatModificationToAppPatch = (
|
||||
mod: ChatModification,
|
||||
jid: string
|
||||
mod: ChatModification,
|
||||
jid: string
|
||||
) => {
|
||||
const OP = proto.SyncdMutation.SyncdMutationSyncdOperation
|
||||
const getMessageRange = (lastMessages: LastMessageList) => {
|
||||
if(!lastMessages?.length) {
|
||||
throw new Boom('Expected last message to be not from me', { statusCode: 400 })
|
||||
}
|
||||
const lastMsg = lastMessages[lastMessages.length-1]
|
||||
if(lastMsg.key.fromMe) {
|
||||
throw new Boom('Expected last message in array to be not from me', { statusCode: 400 })
|
||||
}
|
||||
const messageRange: proto.ISyncActionMessageRange = {
|
||||
lastMessageTimestamp: lastMsg?.messageTimestamp,
|
||||
messages: lastMessages
|
||||
}
|
||||
return messageRange
|
||||
}
|
||||
let patch: WAPatchCreate
|
||||
if('mute' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
muteAction: {
|
||||
muted: !!mod.mute,
|
||||
muteEndTimestamp: mod.mute || undefined
|
||||
}
|
||||
},
|
||||
index: ['mute', jid],
|
||||
type: 'regular_high',
|
||||
apiVersion: 2,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('archive' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
archiveChatAction: {
|
||||
archived: !!mod.archive,
|
||||
messageRange: getMessageRange(mod.lastMessages)
|
||||
}
|
||||
},
|
||||
index: ['archive', jid],
|
||||
type: 'regular_low',
|
||||
apiVersion: 3,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('markRead' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
markChatAsReadAction: {
|
||||
read: mod.markRead,
|
||||
messageRange: getMessageRange(mod.lastMessages)
|
||||
}
|
||||
},
|
||||
index: ['markChatAsRead', jid],
|
||||
type: 'regular_low',
|
||||
apiVersion: 3,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('clear' in mod) {
|
||||
if(mod.clear === 'all') {
|
||||
throw new Boom('not supported')
|
||||
} else {
|
||||
const key = mod.clear.messages[0]
|
||||
patch = {
|
||||
syncAction: {
|
||||
deleteMessageForMeAction: {
|
||||
deleteMedia: false
|
||||
}
|
||||
},
|
||||
index: ['deleteMessageForMe', jid, key.id, key.fromMe ? '1' : '0', '0'],
|
||||
type: 'regular_high',
|
||||
apiVersion: 3,
|
||||
operation: OP.SET
|
||||
}
|
||||
}
|
||||
} else if('pin' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
pinAction: {
|
||||
pinned: !!mod.pin
|
||||
}
|
||||
},
|
||||
index: ['pin_v1', jid],
|
||||
type: 'regular_low',
|
||||
apiVersion: 5,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else {
|
||||
throw new Boom('not supported')
|
||||
}
|
||||
const OP = proto.SyncdMutation.SyncdMutationSyncdOperation
|
||||
const getMessageRange = (lastMessages: LastMessageList) => {
|
||||
if(!lastMessages?.length) {
|
||||
throw new Boom('Expected last message to be not from me', { statusCode: 400 })
|
||||
}
|
||||
|
||||
patch.syncAction.timestamp = Date.now()
|
||||
const lastMsg = lastMessages[lastMessages.length-1]
|
||||
if(lastMsg.key.fromMe) {
|
||||
throw new Boom('Expected last message in array to be not from me', { statusCode: 400 })
|
||||
}
|
||||
|
||||
return patch
|
||||
const messageRange: proto.ISyncActionMessageRange = {
|
||||
lastMessageTimestamp: lastMsg?.messageTimestamp,
|
||||
messages: lastMessages
|
||||
}
|
||||
return messageRange
|
||||
}
|
||||
|
||||
let patch: WAPatchCreate
|
||||
if('mute' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
muteAction: {
|
||||
muted: !!mod.mute,
|
||||
muteEndTimestamp: mod.mute || undefined
|
||||
}
|
||||
},
|
||||
index: ['mute', jid],
|
||||
type: 'regular_high',
|
||||
apiVersion: 2,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('archive' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
archiveChatAction: {
|
||||
archived: !!mod.archive,
|
||||
messageRange: getMessageRange(mod.lastMessages)
|
||||
}
|
||||
},
|
||||
index: ['archive', jid],
|
||||
type: 'regular_low',
|
||||
apiVersion: 3,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('markRead' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
markChatAsReadAction: {
|
||||
read: mod.markRead,
|
||||
messageRange: getMessageRange(mod.lastMessages)
|
||||
}
|
||||
},
|
||||
index: ['markChatAsRead', jid],
|
||||
type: 'regular_low',
|
||||
apiVersion: 3,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('clear' in mod) {
|
||||
if(mod.clear === 'all') {
|
||||
throw new Boom('not supported')
|
||||
} else {
|
||||
const key = mod.clear.messages[0]
|
||||
patch = {
|
||||
syncAction: {
|
||||
deleteMessageForMeAction: {
|
||||
deleteMedia: false
|
||||
}
|
||||
},
|
||||
index: ['deleteMessageForMe', jid, key.id, key.fromMe ? '1' : '0', '0'],
|
||||
type: 'regular_high',
|
||||
apiVersion: 3,
|
||||
operation: OP.SET
|
||||
}
|
||||
}
|
||||
} else if('pin' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
pinAction: {
|
||||
pinned: !!mod.pin
|
||||
}
|
||||
},
|
||||
index: ['pin_v1', jid],
|
||||
type: 'regular_low',
|
||||
apiVersion: 5,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else {
|
||||
throw new Boom('not supported')
|
||||
}
|
||||
|
||||
patch.syncAction.timestamp = Date.now()
|
||||
|
||||
return patch
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as curveJs from 'curve25519-js'
|
||||
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto'
|
||||
import * as curveJs from 'curve25519-js'
|
||||
import { KeyPair } from '../Types'
|
||||
|
||||
export const Curve = {
|
||||
@@ -23,70 +23,77 @@ export const Curve = {
|
||||
}
|
||||
|
||||
export const signedKeyPair = (keyPair: KeyPair, keyId: number) => {
|
||||
const signKeys = Curve.generateKeyPair()
|
||||
const pubKey = new Uint8Array(33)
|
||||
pubKey.set([5], 0)
|
||||
pubKey.set(signKeys.public, 1)
|
||||
const signKeys = Curve.generateKeyPair()
|
||||
const pubKey = new Uint8Array(33)
|
||||
pubKey.set([5], 0)
|
||||
pubKey.set(signKeys.public, 1)
|
||||
|
||||
const signature = Curve.sign(keyPair.private, pubKey)
|
||||
const signature = Curve.sign(keyPair.private, pubKey)
|
||||
|
||||
return { keyPair: signKeys, signature, keyId }
|
||||
return { keyPair: signKeys, signature, keyId }
|
||||
}
|
||||
|
||||
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
|
||||
export function aesDecrypt(buffer: Buffer, key: Buffer) {
|
||||
return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16))
|
||||
return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16))
|
||||
}
|
||||
|
||||
/** decrypt AES 256 CBC */
|
||||
export function aesDecryptWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
|
||||
const aes = createDecipheriv('aes-256-cbc', key, IV)
|
||||
return Buffer.concat([aes.update(buffer), aes.final()])
|
||||
const aes = createDecipheriv('aes-256-cbc', key, IV)
|
||||
return Buffer.concat([aes.update(buffer), aes.final()])
|
||||
}
|
||||
|
||||
// encrypt AES 256 CBC; where a random IV is prefixed to the buffer
|
||||
export function aesEncrypt(buffer: Buffer | Uint8Array, key: Buffer) {
|
||||
const IV = randomBytes(16)
|
||||
const aes = createCipheriv('aes-256-cbc', key, IV)
|
||||
return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer
|
||||
const IV = randomBytes(16)
|
||||
const aes = createCipheriv('aes-256-cbc', key, IV)
|
||||
return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer
|
||||
}
|
||||
|
||||
// encrypt AES 256 CBC with a given IV
|
||||
export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
|
||||
const aes = createCipheriv('aes-256-cbc', key, IV)
|
||||
return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer
|
||||
const aes = createCipheriv('aes-256-cbc', key, IV)
|
||||
return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer
|
||||
}
|
||||
|
||||
// sign HMAC using SHA 256
|
||||
export function hmacSign(buffer: Buffer | Uint8Array, key: Buffer | Uint8Array, variant: 'sha256' | 'sha512' = 'sha256') {
|
||||
return createHmac(variant, key).update(buffer).digest()
|
||||
return createHmac(variant, key).update(buffer).digest()
|
||||
}
|
||||
|
||||
export function sha256(buffer: Buffer) {
|
||||
return createHash('sha256').update(buffer).digest()
|
||||
return createHash('sha256').update(buffer).digest()
|
||||
}
|
||||
|
||||
// HKDF key expansion
|
||||
// from: https://github.com/benadida/node-hkdf
|
||||
export function hkdf(buffer: Uint8Array, expandedLength: number, { info, salt }: { salt?: Buffer, info?: string }) {
|
||||
const hashAlg = 'sha256'
|
||||
const hashLength = 32
|
||||
salt = salt || Buffer.alloc(hashLength)
|
||||
// now we compute the PRK
|
||||
const prk = createHmac(hashAlg, salt).update(buffer).digest()
|
||||
const hashAlg = 'sha256'
|
||||
const hashLength = 32
|
||||
salt = salt || Buffer.alloc(hashLength)
|
||||
// now we compute the PRK
|
||||
const prk = createHmac(hashAlg, salt).update(buffer).digest()
|
||||
|
||||
let prev = Buffer.from([])
|
||||
const buffers = []
|
||||
const num_blocks = Math.ceil(expandedLength / hashLength)
|
||||
let prev = Buffer.from([])
|
||||
const buffers = []
|
||||
const num_blocks = Math.ceil(expandedLength / hashLength)
|
||||
|
||||
const infoBuff = Buffer.from(info || [])
|
||||
const infoBuff = Buffer.from(info || [])
|
||||
|
||||
for (var i=0; i<num_blocks; i++) {
|
||||
const hmac = createHmac(hashAlg, prk)
|
||||
// XXX is there a more optimal way to build up buffers?
|
||||
const input = Buffer.concat([
|
||||
prev,
|
||||
infoBuff,
|
||||
Buffer.from(String.fromCharCode(i + 1))
|
||||
]);
|
||||
hmac.update(input)
|
||||
for(var i=0; i<num_blocks; i++) {
|
||||
const hmac = createHmac(hashAlg, prk)
|
||||
// XXX is there a more optimal way to build up buffers?
|
||||
const input = Buffer.concat([
|
||||
prev,
|
||||
infoBuff,
|
||||
Buffer.from(String.fromCharCode(i + 1))
|
||||
])
|
||||
hmac.update(input)
|
||||
|
||||
prev = hmac.digest()
|
||||
buffers.push(prev)
|
||||
}
|
||||
return Buffer.concat(buffers, expandedLength)
|
||||
prev = hmac.digest()
|
||||
buffers.push(prev)
|
||||
}
|
||||
|
||||
return Buffer.concat(buffers, expandedLength)
|
||||
}
|
||||
@@ -1,114 +1,128 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { unpadRandomMax16 } from "./generics"
|
||||
import { WAMessageKey, AuthenticationState } from '../Types'
|
||||
import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidStatusBroadcast, isJidUser, jidNormalizedUser } from '../WABinary'
|
||||
import { decryptGroupSignalProto, decryptSignalProto, processSenderKeyMessage } from './signal'
|
||||
import { proto } from '../../WAProto'
|
||||
import { AuthenticationState, WAMessageKey } from '../Types'
|
||||
import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidStatusBroadcast, isJidUser } from '../WABinary'
|
||||
import { unpadRandomMax16 } from './generics'
|
||||
import { decryptGroupSignalProto, decryptSignalProto, processSenderKeyMessage } from './signal'
|
||||
|
||||
type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'direct_peer_status' | 'other_status'
|
||||
|
||||
export const decodeMessageStanza = async(stanza: BinaryNode, auth: AuthenticationState) => {
|
||||
//const deviceIdentity = (stanza.content as BinaryNodeM[])?.find(m => m.tag === 'device-identity')
|
||||
//const deviceIdentityBytes = deviceIdentity ? deviceIdentity.content as Buffer : undefined
|
||||
//const deviceIdentity = (stanza.content as BinaryNodeM[])?.find(m => m.tag === 'device-identity')
|
||||
//const deviceIdentityBytes = deviceIdentity ? deviceIdentity.content as Buffer : undefined
|
||||
|
||||
let msgType: MessageType
|
||||
let chatId: string
|
||||
let author: string
|
||||
let msgType: MessageType
|
||||
let chatId: string
|
||||
let author: string
|
||||
|
||||
const msgId: string = stanza.attrs.id
|
||||
const from: string = stanza.attrs.from
|
||||
const participant: string | undefined = stanza.attrs.participant
|
||||
const recipient: string | undefined = stanza.attrs.recipient
|
||||
const msgId: string = stanza.attrs.id
|
||||
const from: string = stanza.attrs.from
|
||||
const participant: string | undefined = stanza.attrs.participant
|
||||
const recipient: string | undefined = stanza.attrs.recipient
|
||||
|
||||
const isMe = (jid: string) => areJidsSameUser(jid, auth.creds.me!.id)
|
||||
const isMe = (jid: string) => areJidsSameUser(jid, auth.creds.me!.id)
|
||||
|
||||
if(isJidUser(from)) {
|
||||
if(recipient) {
|
||||
if(!isMe(from)) {
|
||||
throw new Boom('')
|
||||
}
|
||||
chatId = recipient
|
||||
} else {
|
||||
chatId = from
|
||||
}
|
||||
msgType = 'chat'
|
||||
author = from
|
||||
} else if(isJidGroup(from)) {
|
||||
if(!participant) {
|
||||
throw new Boom('No participant in group message')
|
||||
}
|
||||
msgType = 'group'
|
||||
author = participant
|
||||
chatId = from
|
||||
} else if(isJidBroadcast(from)) {
|
||||
if(!participant) {
|
||||
throw new Boom('No participant in group message')
|
||||
}
|
||||
const isParticipantMe = isMe(participant)
|
||||
if(isJidStatusBroadcast(from)) {
|
||||
msgType = isParticipantMe ? 'direct_peer_status' : 'other_status'
|
||||
} else {
|
||||
msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast'
|
||||
}
|
||||
chatId = from
|
||||
author = participant
|
||||
}
|
||||
if(isJidUser(from)) {
|
||||
if(recipient) {
|
||||
if(!isMe(from)) {
|
||||
throw new Boom('')
|
||||
}
|
||||
|
||||
const sender = msgType === 'chat' ? author : chatId
|
||||
chatId = recipient
|
||||
} else {
|
||||
chatId = from
|
||||
}
|
||||
|
||||
const fromMe = isMe(stanza.attrs.participant || stanza.attrs.from)
|
||||
const pushname = stanza.attrs.notify
|
||||
msgType = 'chat'
|
||||
author = from
|
||||
} else if(isJidGroup(from)) {
|
||||
if(!participant) {
|
||||
throw new Boom('No participant in group message')
|
||||
}
|
||||
|
||||
msgType = 'group'
|
||||
author = participant
|
||||
chatId = from
|
||||
} else if(isJidBroadcast(from)) {
|
||||
if(!participant) {
|
||||
throw new Boom('No participant in group message')
|
||||
}
|
||||
|
||||
const isParticipantMe = isMe(participant)
|
||||
if(isJidStatusBroadcast(from)) {
|
||||
msgType = isParticipantMe ? 'direct_peer_status' : 'other_status'
|
||||
} else {
|
||||
msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast'
|
||||
}
|
||||
|
||||
chatId = from
|
||||
author = participant
|
||||
}
|
||||
|
||||
const sender = msgType === 'chat' ? author : chatId
|
||||
|
||||
const fromMe = isMe(stanza.attrs.participant || stanza.attrs.from)
|
||||
const pushname = stanza.attrs.notify
|
||||
|
||||
const key: WAMessageKey = {
|
||||
remoteJid: chatId,
|
||||
fromMe,
|
||||
id: msgId,
|
||||
participant
|
||||
}
|
||||
const key: WAMessageKey = {
|
||||
remoteJid: chatId,
|
||||
fromMe,
|
||||
id: msgId,
|
||||
participant
|
||||
}
|
||||
|
||||
const fullMessage: proto.IWebMessageInfo = {
|
||||
key,
|
||||
messageTimestamp: +stanza.attrs.t,
|
||||
pushName: pushname
|
||||
}
|
||||
const fullMessage: proto.IWebMessageInfo = {
|
||||
key,
|
||||
messageTimestamp: +stanza.attrs.t,
|
||||
pushName: pushname
|
||||
}
|
||||
|
||||
if(key.fromMe) {
|
||||
fullMessage.status = proto.WebMessageInfo.WebMessageInfoStatus.SERVER_ACK
|
||||
}
|
||||
if(key.fromMe) {
|
||||
fullMessage.status = proto.WebMessageInfo.WebMessageInfoStatus.SERVER_ACK
|
||||
}
|
||||
|
||||
if(Array.isArray(stanza.content)) {
|
||||
for(const { tag, attrs, content } of stanza.content) {
|
||||
if(tag !== 'enc') continue
|
||||
if(!(content instanceof Uint8Array)) continue
|
||||
if(Array.isArray(stanza.content)) {
|
||||
for(const { tag, attrs, content } of stanza.content) {
|
||||
if(tag !== 'enc') {
|
||||
continue
|
||||
}
|
||||
|
||||
let msgBuffer: Buffer
|
||||
if(!(content instanceof Uint8Array)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const e2eType = attrs.type
|
||||
switch(e2eType) {
|
||||
case 'skmsg':
|
||||
msgBuffer = await decryptGroupSignalProto(sender, author, content, auth)
|
||||
break
|
||||
case 'pkmsg':
|
||||
case 'msg':
|
||||
const user = isJidUser(sender) ? sender : author
|
||||
msgBuffer = await decryptSignalProto(user, e2eType, content as Buffer, auth)
|
||||
break
|
||||
}
|
||||
let msg: proto.IMessage = proto.Message.decode(unpadRandomMax16(msgBuffer))
|
||||
msg = msg.deviceSentMessage?.message || msg
|
||||
if(msg.senderKeyDistributionMessage) {
|
||||
await processSenderKeyMessage(author, msg.senderKeyDistributionMessage, auth)
|
||||
}
|
||||
let msgBuffer: Buffer
|
||||
|
||||
if(fullMessage.message) Object.assign(fullMessage.message, msg)
|
||||
else fullMessage.message = msg
|
||||
} catch(error) {
|
||||
fullMessage.messageStubType = proto.WebMessageInfo.WebMessageInfoStubType.CIPHERTEXT
|
||||
fullMessage.messageStubParameters = [error.message]
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
const e2eType = attrs.type
|
||||
switch (e2eType) {
|
||||
case 'skmsg':
|
||||
msgBuffer = await decryptGroupSignalProto(sender, author, content, auth)
|
||||
break
|
||||
case 'pkmsg':
|
||||
case 'msg':
|
||||
const user = isJidUser(sender) ? sender : author
|
||||
msgBuffer = await decryptSignalProto(user, e2eType, content as Buffer, auth)
|
||||
break
|
||||
}
|
||||
|
||||
return fullMessage
|
||||
let msg: proto.IMessage = proto.Message.decode(unpadRandomMax16(msgBuffer))
|
||||
msg = msg.deviceSentMessage?.message || msg
|
||||
if(msg.senderKeyDistributionMessage) {
|
||||
await processSenderKeyMessage(author, msg.senderKeyDistributionMessage, auth)
|
||||
}
|
||||
|
||||
if(fullMessage.message) {
|
||||
Object.assign(fullMessage.message, msg)
|
||||
} else {
|
||||
fullMessage.message = msg
|
||||
}
|
||||
} catch(error) {
|
||||
fullMessage.messageStubType = proto.WebMessageInfo.WebMessageInfoStubType.CIPHERTEXT
|
||||
fullMessage.messageStubParameters = [error.message]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fullMessage
|
||||
}
|
||||
@@ -2,217 +2,233 @@ import { Boom } from '@hapi/boom'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { platform, release } from 'os'
|
||||
import { Logger } from 'pino'
|
||||
import { ConnectionState } from '..'
|
||||
import { proto } from '../../WAProto'
|
||||
import { CommonBaileysEventEmitter, DisconnectReason } from '../Types'
|
||||
import { Binary } from '../WABinary'
|
||||
import { ConnectionState } from '..'
|
||||
|
||||
const PLATFORM_MAP = {
|
||||
'aix': 'AIX',
|
||||
'darwin': 'Mac OS',
|
||||
'win32': 'Windows',
|
||||
'android': 'Android'
|
||||
'aix': 'AIX',
|
||||
'darwin': 'Mac OS',
|
||||
'win32': 'Windows',
|
||||
'android': 'Android'
|
||||
}
|
||||
|
||||
export const Browsers = {
|
||||
ubuntu: browser => ['Ubuntu', browser, '18.04'] as [string, string, string],
|
||||
macOS: browser => ['Mac OS', browser, '10.15.3'] as [string, string, string],
|
||||
baileys: browser => ['Baileys', browser, '4.0.0'] as [string, string, string],
|
||||
/** The appropriate browser based on your OS & release */
|
||||
appropriate: browser => [ PLATFORM_MAP[platform()] || 'Ubuntu', browser, release() ] as [string, string, string]
|
||||
ubuntu: browser => ['Ubuntu', browser, '18.04'] as [string, string, string],
|
||||
macOS: browser => ['Mac OS', browser, '10.15.3'] as [string, string, string],
|
||||
baileys: browser => ['Baileys', browser, '4.0.0'] as [string, string, string],
|
||||
/** The appropriate browser based on your OS & release */
|
||||
appropriate: browser => [ PLATFORM_MAP[platform()] || 'Ubuntu', browser, release() ] as [string, string, string]
|
||||
}
|
||||
|
||||
export const BufferJSON = {
|
||||
replacer: (k, value: any) => {
|
||||
if(Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') {
|
||||
return { type: 'Buffer', data: Buffer.from(value?.data || value).toString('base64') }
|
||||
}
|
||||
return value
|
||||
},
|
||||
reviver: (_, value: any) => {
|
||||
if(typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) {
|
||||
const val = value.data || value.value
|
||||
return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val)
|
||||
}
|
||||
return value
|
||||
}
|
||||
replacer: (k, value: any) => {
|
||||
if(Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') {
|
||||
return { type: 'Buffer', data: Buffer.from(value?.data || value).toString('base64') }
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
reviver: (_, value: any) => {
|
||||
if(typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) {
|
||||
const val = value.data || value.value
|
||||
return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const writeRandomPadMax16 = function(e: Binary) {
|
||||
function r(e: Binary, t: number) {
|
||||
for (var r = 0; r < t; r++)
|
||||
e.writeUint8(t)
|
||||
}
|
||||
export const writeRandomPadMax16 = (e: Binary) => {
|
||||
function r(e: Binary, t: number) {
|
||||
for(var r = 0; r < t; r++) {
|
||||
e.writeUint8(t)
|
||||
}
|
||||
}
|
||||
|
||||
var t = randomBytes(1)
|
||||
r(e, 1 + (15 & t[0]))
|
||||
return e
|
||||
var t = randomBytes(1)
|
||||
r(e, 1 + (15 & t[0]))
|
||||
return e
|
||||
}
|
||||
|
||||
export const unpadRandomMax16 = (e: Uint8Array | Buffer) => {
|
||||
const t = new Uint8Array(e);
|
||||
if (0 === t.length) {
|
||||
throw new Error('unpadPkcs7 given empty bytes');
|
||||
}
|
||||
const t = new Uint8Array(e)
|
||||
if(0 === t.length) {
|
||||
throw new Error('unpadPkcs7 given empty bytes')
|
||||
}
|
||||
|
||||
var r = t[t.length - 1];
|
||||
if (r > t.length) {
|
||||
throw new Error(`unpad given ${t.length} bytes, but pad is ${r}`);
|
||||
}
|
||||
var r = t[t.length - 1]
|
||||
if(r > t.length) {
|
||||
throw new Error(`unpad given ${t.length} bytes, but pad is ${r}`)
|
||||
}
|
||||
|
||||
return new Uint8Array(t.buffer, t.byteOffset, t.length - r);
|
||||
return new Uint8Array(t.buffer, t.byteOffset, t.length - r)
|
||||
}
|
||||
|
||||
export const encodeWAMessage = (message: proto.IMessage) => (
|
||||
Buffer.from(
|
||||
writeRandomPadMax16(
|
||||
new Binary(proto.Message.encode(message).finish())
|
||||
).readByteArray()
|
||||
)
|
||||
Buffer.from(
|
||||
writeRandomPadMax16(
|
||||
new Binary(proto.Message.encode(message).finish())
|
||||
).readByteArray()
|
||||
)
|
||||
)
|
||||
|
||||
export const generateRegistrationId = () => (
|
||||
Uint16Array.from(randomBytes(2))[0] & 0x3fff
|
||||
Uint16Array.from(randomBytes(2))[0] & 0x3fff
|
||||
)
|
||||
|
||||
export const encodeInt = (e: number, t: number) => {
|
||||
for (var r = t, a = new Uint8Array(e), i = e - 1; i >= 0; i--) {
|
||||
a[i] = 255 & r
|
||||
r >>>= 8
|
||||
}
|
||||
return a
|
||||
for(var r = t, a = new Uint8Array(e), i = e - 1; i >= 0; i--) {
|
||||
a[i] = 255 & r
|
||||
r >>>= 8
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
export const encodeBigEndian = (e: number, t=4) => {
|
||||
let r = e;
|
||||
let a = new Uint8Array(t);
|
||||
for (let i = t - 1; i >= 0; i--) {
|
||||
a[i] = 255 & r
|
||||
r >>>= 8
|
||||
}
|
||||
return a
|
||||
let r = e
|
||||
const a = new Uint8Array(t)
|
||||
for(let i = t - 1; i >= 0; i--) {
|
||||
a[i] = 255 & r
|
||||
r >>>= 8
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
export const toNumber = (t: Long | number) => ((typeof t === 'object' && 'toNumber' in t) ? t.toNumber() : t)
|
||||
|
||||
export function shallowChanges <T> (old: T, current: T, {lookForDeletedKeys}: {lookForDeletedKeys: boolean}): Partial<T> {
|
||||
let changes: Partial<T> = {}
|
||||
for (let key in current) {
|
||||
if (old[key] !== current[key]) {
|
||||
changes[key] = current[key] || null
|
||||
}
|
||||
}
|
||||
if (lookForDeletedKeys) {
|
||||
for (let key in old) {
|
||||
if (!changes[key] && old[key] !== current[key]) {
|
||||
changes[key] = current[key] || null
|
||||
}
|
||||
}
|
||||
}
|
||||
return changes
|
||||
export function shallowChanges <T>(old: T, current: T, { lookForDeletedKeys }: {lookForDeletedKeys: boolean}): Partial<T> {
|
||||
const changes: Partial<T> = {}
|
||||
for(const key in current) {
|
||||
if(old[key] !== current[key]) {
|
||||
changes[key] = current[key] || null
|
||||
}
|
||||
}
|
||||
|
||||
if(lookForDeletedKeys) {
|
||||
for(const key in old) {
|
||||
if(!changes[key] && old[key] !== current[key]) {
|
||||
changes[key] = current[key] || null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
/** unix timestamp of a date in seconds */
|
||||
export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime()/1000)
|
||||
|
||||
export type DebouncedTimeout = ReturnType<typeof debouncedTimeout>
|
||||
|
||||
export const debouncedTimeout = (intervalMs: number = 1000, task: () => void = undefined) => {
|
||||
let timeout: NodeJS.Timeout
|
||||
return {
|
||||
start: (newIntervalMs?: number, newTask?: () => void) => {
|
||||
task = newTask || task
|
||||
intervalMs = newIntervalMs || intervalMs
|
||||
timeout && clearTimeout(timeout)
|
||||
timeout = setTimeout(task, intervalMs)
|
||||
},
|
||||
cancel: () => {
|
||||
timeout && clearTimeout(timeout)
|
||||
timeout = undefined
|
||||
},
|
||||
setTask: (newTask: () => void) => task = newTask,
|
||||
setInterval: (newInterval: number) => intervalMs = newInterval
|
||||
}
|
||||
let timeout: NodeJS.Timeout
|
||||
return {
|
||||
start: (newIntervalMs?: number, newTask?: () => void) => {
|
||||
task = newTask || task
|
||||
intervalMs = newIntervalMs || intervalMs
|
||||
timeout && clearTimeout(timeout)
|
||||
timeout = setTimeout(task, intervalMs)
|
||||
},
|
||||
cancel: () => {
|
||||
timeout && clearTimeout(timeout)
|
||||
timeout = undefined
|
||||
},
|
||||
setTask: (newTask: () => void) => task = newTask,
|
||||
setInterval: (newInterval: number) => intervalMs = newInterval
|
||||
}
|
||||
}
|
||||
|
||||
export const delay = (ms: number) => delayCancellable (ms).delay
|
||||
export const delayCancellable = (ms: number) => {
|
||||
const stack = new Error().stack
|
||||
let timeout: NodeJS.Timeout
|
||||
let reject: (error) => void
|
||||
const delay: Promise<void> = new Promise((resolve, _reject) => {
|
||||
timeout = setTimeout(resolve, ms)
|
||||
reject = _reject
|
||||
})
|
||||
const cancel = () => {
|
||||
clearTimeout (timeout)
|
||||
reject(
|
||||
new Boom('Cancelled', {
|
||||
statusCode: 500,
|
||||
data: {
|
||||
stack
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return { delay, cancel }
|
||||
const stack = new Error().stack
|
||||
let timeout: NodeJS.Timeout
|
||||
let reject: (error) => void
|
||||
const delay: Promise<void> = new Promise((resolve, _reject) => {
|
||||
timeout = setTimeout(resolve, ms)
|
||||
reject = _reject
|
||||
})
|
||||
const cancel = () => {
|
||||
clearTimeout (timeout)
|
||||
reject(
|
||||
new Boom('Cancelled', {
|
||||
statusCode: 500,
|
||||
data: {
|
||||
stack
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return { delay, cancel }
|
||||
}
|
||||
|
||||
export async function promiseTimeout<T>(ms: number, promise: (resolve: (v?: T)=>void, reject: (error) => void) => void) {
|
||||
if (!ms) return new Promise (promise)
|
||||
const stack = new Error().stack
|
||||
// Create a promise that rejects in <ms> milliseconds
|
||||
let {delay, cancel} = delayCancellable (ms)
|
||||
const p = new Promise ((resolve, reject) => {
|
||||
delay
|
||||
.then(() => reject(
|
||||
new Boom('Timed Out', {
|
||||
statusCode: DisconnectReason.timedOut,
|
||||
data: {
|
||||
stack
|
||||
}
|
||||
})
|
||||
))
|
||||
.catch (err => reject(err))
|
||||
if(!ms) {
|
||||
return new Promise (promise)
|
||||
}
|
||||
|
||||
const stack = new Error().stack
|
||||
// Create a promise that rejects in <ms> milliseconds
|
||||
const { delay, cancel } = delayCancellable (ms)
|
||||
const p = new Promise ((resolve, reject) => {
|
||||
delay
|
||||
.then(() => reject(
|
||||
new Boom('Timed Out', {
|
||||
statusCode: DisconnectReason.timedOut,
|
||||
data: {
|
||||
stack
|
||||
}
|
||||
})
|
||||
))
|
||||
.catch (err => reject(err))
|
||||
|
||||
promise (resolve, reject)
|
||||
})
|
||||
.finally (cancel)
|
||||
return p as Promise<T>
|
||||
promise (resolve, reject)
|
||||
})
|
||||
.finally (cancel)
|
||||
return p as Promise<T>
|
||||
}
|
||||
|
||||
// generate a random ID to attach to a message
|
||||
export const generateMessageID = () => 'BAE5' + randomBytes(6).toString('hex').toUpperCase()
|
||||
|
||||
export const bindWaitForConnectionUpdate = (ev: CommonBaileysEventEmitter<any>) => (
|
||||
async(check: (u: Partial<ConnectionState>) => boolean, timeoutMs?: number) => {
|
||||
let listener: (item: Partial<ConnectionState>) => void
|
||||
await (
|
||||
promiseTimeout(
|
||||
timeoutMs,
|
||||
(resolve, reject) => {
|
||||
listener = (update) => {
|
||||
if(check(update)) {
|
||||
resolve()
|
||||
} else if(update.connection == 'close') {
|
||||
async(check: (u: Partial<ConnectionState>) => boolean, timeoutMs?: number) => {
|
||||
let listener: (item: Partial<ConnectionState>) => void
|
||||
await (
|
||||
promiseTimeout(
|
||||
timeoutMs,
|
||||
(resolve, reject) => {
|
||||
listener = (update) => {
|
||||
if(check(update)) {
|
||||
resolve()
|
||||
} else if(update.connection === 'close') {
|
||||
reject(update.lastDisconnect?.error || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }))
|
||||
}
|
||||
}
|
||||
ev.on('connection.update', listener)
|
||||
}
|
||||
)
|
||||
.finally(() => (
|
||||
ev.off('connection.update', listener)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
ev.on('connection.update', listener)
|
||||
}
|
||||
)
|
||||
.finally(() => (
|
||||
ev.off('connection.update', listener)
|
||||
))
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const printQRIfNecessaryListener = (ev: CommonBaileysEventEmitter<any>, logger: Logger) => {
|
||||
ev.on('connection.update', async({ qr }) => {
|
||||
if(qr) {
|
||||
const QR = await import('qrcode-terminal')
|
||||
.catch(err => {
|
||||
logger.error('QR code terminal not added as dependency')
|
||||
})
|
||||
QR?.generate(qr, { small: true })
|
||||
}
|
||||
})
|
||||
ev.on('connection.update', async({ qr }) => {
|
||||
if(qr) {
|
||||
const QR = await import('qrcode-terminal')
|
||||
.catch(err => {
|
||||
logger.error('QR code terminal not added as dependency')
|
||||
})
|
||||
QR?.generate(qr, { small: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import { downloadContentFromMessage } from "./messages-media"
|
||||
import { proto } from "../../WAProto"
|
||||
import { promisify } from 'util'
|
||||
import { inflate } from "zlib"
|
||||
import { Chat, Contact } from "../Types"
|
||||
import { inflate } from 'zlib'
|
||||
import { proto } from '../../WAProto'
|
||||
import { Chat, Contact } from '../Types'
|
||||
import { downloadContentFromMessage } from './messages-media'
|
||||
|
||||
const inflatePromise = promisify(inflate)
|
||||
|
||||
export const downloadHistory = async(msg: proto.IHistorySyncNotification) => {
|
||||
const stream = await downloadContentFromMessage(msg, 'history')
|
||||
let buffer = Buffer.from([])
|
||||
for await(const chunk of stream) {
|
||||
for await (const chunk of stream) {
|
||||
buffer = Buffer.concat([buffer, chunk])
|
||||
}
|
||||
|
||||
// decompress buffer
|
||||
buffer = await inflatePromise(buffer)
|
||||
|
||||
@@ -24,45 +25,47 @@ export const processHistoryMessage = (item: proto.IHistorySync, historyCache: Se
|
||||
const messages: proto.IWebMessageInfo[] = []
|
||||
const contacts: Contact[] = []
|
||||
const chats: Chat[] = []
|
||||
switch(item.syncType) {
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_BOOTSTRAP:
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.RECENT:
|
||||
for(const chat of item.conversations) {
|
||||
const contactId = `c:${chat.id}`
|
||||
if(chat.name && !historyCache.has(contactId)) {
|
||||
contacts.push({
|
||||
id: chat.id,
|
||||
name: chat.name
|
||||
})
|
||||
historyCache.add(contactId)
|
||||
}
|
||||
switch (item.syncType) {
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_BOOTSTRAP:
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.RECENT:
|
||||
for(const chat of item.conversations) {
|
||||
const contactId = `c:${chat.id}`
|
||||
if(chat.name && !historyCache.has(contactId)) {
|
||||
contacts.push({
|
||||
id: chat.id,
|
||||
name: chat.name
|
||||
})
|
||||
historyCache.add(contactId)
|
||||
}
|
||||
|
||||
for(const { message } of chat.messages || []) {
|
||||
const uqId = `${message?.key.remoteJid}:${message.key.id}`
|
||||
if(message && !historyCache.has(uqId)) {
|
||||
messages.push(message)
|
||||
historyCache.add(uqId)
|
||||
}
|
||||
}
|
||||
|
||||
delete chat.messages
|
||||
if(!historyCache.has(chat.id)) {
|
||||
chats.push(chat)
|
||||
historyCache.add(chat.id)
|
||||
for(const { message } of chat.messages || []) {
|
||||
const uqId = `${message?.key.remoteJid}:${message.key.id}`
|
||||
if(message && !historyCache.has(uqId)) {
|
||||
messages.push(message)
|
||||
historyCache.add(uqId)
|
||||
}
|
||||
}
|
||||
break
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.PUSH_NAME:
|
||||
for(const c of item.pushnames) {
|
||||
const contactId = `c:${c.id}`
|
||||
if(historyCache.has(contactId)) {
|
||||
contacts.push({ notify: c.pushname, id: c.id })
|
||||
historyCache.add(contactId)
|
||||
}
|
||||
|
||||
delete chat.messages
|
||||
if(!historyCache.has(chat.id)) {
|
||||
chats.push(chat)
|
||||
historyCache.add(chat.id)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_STATUS_V3:
|
||||
// TODO
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.PUSH_NAME:
|
||||
for(const c of item.pushnames) {
|
||||
const contactId = `c:${c.id}`
|
||||
if(historyCache.has(contactId)) {
|
||||
contacts.push({ notify: c.pushname, id: c.id })
|
||||
historyCache.add(contactId)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_STATUS_V3:
|
||||
// TODO
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +1,82 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { decodeBinaryNodeLegacy, jidNormalizedUser } from "../WABinary"
|
||||
import { aesDecrypt, hmacSign, hkdf, Curve } from "./crypto"
|
||||
import { AuthenticationCreds, Contact, CurveKeyPair, DisconnectReason, LegacyAuthenticationCreds, WATag } from '../Types'
|
||||
import { decodeBinaryNodeLegacy, jidNormalizedUser } from '../WABinary'
|
||||
import { aesDecrypt, Curve, hkdf, hmacSign } from './crypto'
|
||||
import { BufferJSON } from './generics'
|
||||
import { DisconnectReason, WATag, LegacyAuthenticationCreds, AuthenticationCreds, CurveKeyPair, Contact } from "../Types"
|
||||
|
||||
export const newLegacyAuthCreds = () => ({
|
||||
clientID: randomBytes(16).toString('base64')
|
||||
clientID: randomBytes(16).toString('base64')
|
||||
}) as LegacyAuthenticationCreds
|
||||
|
||||
export const decodeWAMessage = (
|
||||
message: Buffer | string,
|
||||
auth: { macKey: Buffer, encKey: Buffer },
|
||||
fromMe: boolean=false
|
||||
message: Buffer | string,
|
||||
auth: { macKey: Buffer, encKey: Buffer },
|
||||
fromMe: boolean=false
|
||||
) => {
|
||||
let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message
|
||||
if (commaIndex < 0) throw new Boom('invalid message', { data: message }) // if there was no comma, then this message must be not be valid
|
||||
let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message
|
||||
if(commaIndex < 0) {
|
||||
throw new Boom('invalid message', { data: message })
|
||||
} // if there was no comma, then this message must be not be valid
|
||||
|
||||
if (message[commaIndex+1] === ',') commaIndex += 1
|
||||
let data = message.slice(commaIndex+1, message.length)
|
||||
if(message[commaIndex+1] === ',') {
|
||||
commaIndex += 1
|
||||
}
|
||||
|
||||
let data = message.slice(commaIndex+1, message.length)
|
||||
|
||||
// get the message tag.
|
||||
// If a query was done, the server will respond with the same message tag we sent the query with
|
||||
const messageTag: string = message.slice(0, commaIndex).toString()
|
||||
let json: any
|
||||
let tags: WATag
|
||||
if(data.length) {
|
||||
const possiblyEnc = (data.length > 32 && data.length % 16 === 0)
|
||||
if(typeof data === 'string' || !possiblyEnc) {
|
||||
json = JSON.parse(data.toString()) // parse the JSON
|
||||
} else {
|
||||
try {
|
||||
json = JSON.parse(data.toString())
|
||||
} catch {
|
||||
const { macKey, encKey } = auth || {}
|
||||
if (!macKey || !encKey) {
|
||||
throw new Boom('recieved encrypted buffer when auth creds unavailable', { data: message, statusCode: DisconnectReason.badSession })
|
||||
}
|
||||
/*
|
||||
// get the message tag.
|
||||
// If a query was done, the server will respond with the same message tag we sent the query with
|
||||
const messageTag: string = message.slice(0, commaIndex).toString()
|
||||
let json: any
|
||||
let tags: WATag
|
||||
if(data.length) {
|
||||
const possiblyEnc = (data.length > 32 && data.length % 16 === 0)
|
||||
if(typeof data === 'string' || !possiblyEnc) {
|
||||
json = JSON.parse(data.toString()) // parse the JSON
|
||||
} else {
|
||||
try {
|
||||
json = JSON.parse(data.toString())
|
||||
} catch{
|
||||
const { macKey, encKey } = auth || {}
|
||||
if(!macKey || !encKey) {
|
||||
throw new Boom('recieved encrypted buffer when auth creds unavailable', { data: message, statusCode: DisconnectReason.badSession })
|
||||
}
|
||||
|
||||
/*
|
||||
If the data recieved was not a JSON, then it must be an encrypted message.
|
||||
Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys
|
||||
*/
|
||||
if (fromMe) {
|
||||
tags = [data[0], data[1]]
|
||||
data = data.slice(2, data.length)
|
||||
}
|
||||
if(fromMe) {
|
||||
tags = [data[0], data[1]]
|
||||
data = data.slice(2, data.length)
|
||||
}
|
||||
|
||||
const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message
|
||||
data = data.slice(32, data.length) // the actual message
|
||||
const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey
|
||||
const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message
|
||||
data = data.slice(32, data.length) // the actual message
|
||||
const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey
|
||||
|
||||
if (checksum.equals(computedChecksum)) {
|
||||
// the checksum the server sent, must match the one we computed for the message to be valid
|
||||
const decrypted = aesDecrypt(data, encKey) // decrypt using AES
|
||||
json = decodeBinaryNodeLegacy(decrypted, { index: 0 }) // decode the binary message into a JSON array
|
||||
} else {
|
||||
throw new Boom('Bad checksum', {
|
||||
data: {
|
||||
received: checksum.toString('hex'),
|
||||
computed: computedChecksum.toString('hex'),
|
||||
data: data.slice(0, 80).toString(),
|
||||
tag: messageTag,
|
||||
message: message.slice(0, 80).toString()
|
||||
},
|
||||
statusCode: DisconnectReason.badSession
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [messageTag, json, tags] as const
|
||||
if(checksum.equals(computedChecksum)) {
|
||||
// the checksum the server sent, must match the one we computed for the message to be valid
|
||||
const decrypted = aesDecrypt(data, encKey) // decrypt using AES
|
||||
json = decodeBinaryNodeLegacy(decrypted, { index: 0 }) // decode the binary message into a JSON array
|
||||
} else {
|
||||
throw new Boom('Bad checksum', {
|
||||
data: {
|
||||
received: checksum.toString('hex'),
|
||||
computed: computedChecksum.toString('hex'),
|
||||
data: data.slice(0, 80).toString(),
|
||||
tag: messageTag,
|
||||
message: message.slice(0, 80).toString()
|
||||
},
|
||||
statusCode: DisconnectReason.badSession
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [messageTag, json, tags] as const
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,104 +89,110 @@ export const validateNewConnection = (
|
||||
auth: LegacyAuthenticationCreds,
|
||||
curveKeys: CurveKeyPair
|
||||
) => {
|
||||
// set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone
|
||||
const onValidationSuccess = () => {
|
||||
// set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone
|
||||
const onValidationSuccess = () => {
|
||||
const user: Contact = {
|
||||
id: jidNormalizedUser(json.wid),
|
||||
name: json.pushname
|
||||
}
|
||||
id: jidNormalizedUser(json.wid),
|
||||
name: json.pushname
|
||||
}
|
||||
return { user, auth, phone: json.phone }
|
||||
}
|
||||
if (!json.secret) {
|
||||
}
|
||||
|
||||
if(!json.secret) {
|
||||
// if we didn't get a secret, we don't need it, we're validated
|
||||
if (json.clientToken && json.clientToken !== auth.clientToken) {
|
||||
if(json.clientToken && json.clientToken !== auth.clientToken) {
|
||||
auth = { ...auth, clientToken: json.clientToken }
|
||||
}
|
||||
if (json.serverToken && json.serverToken !== auth.serverToken) {
|
||||
|
||||
if(json.serverToken && json.serverToken !== auth.serverToken) {
|
||||
auth = { ...auth, serverToken: json.serverToken }
|
||||
}
|
||||
|
||||
return onValidationSuccess()
|
||||
}
|
||||
const secret = Buffer.from(json.secret, 'base64')
|
||||
if (secret.length !== 144) {
|
||||
}
|
||||
|
||||
const secret = Buffer.from(json.secret, 'base64')
|
||||
if(secret.length !== 144) {
|
||||
throw new Error ('incorrect secret length received: ' + secret.length)
|
||||
}
|
||||
}
|
||||
|
||||
// generate shared key from our private key & the secret shared by the server
|
||||
const sharedKey = Curve.sharedKey(curveKeys.private, secret.slice(0, 32))
|
||||
// expand the key to 80 bytes using HKDF
|
||||
const expandedKey = hkdf(sharedKey as Buffer, 80, { })
|
||||
// generate shared key from our private key & the secret shared by the server
|
||||
const sharedKey = Curve.sharedKey(curveKeys.private, secret.slice(0, 32))
|
||||
// expand the key to 80 bytes using HKDF
|
||||
const expandedKey = hkdf(sharedKey as Buffer, 80, { })
|
||||
|
||||
// perform HMAC validation.
|
||||
const hmacValidationKey = expandedKey.slice(32, 64)
|
||||
const hmacValidationMessage = Buffer.concat([secret.slice(0, 32), secret.slice(64, secret.length)])
|
||||
// perform HMAC validation.
|
||||
const hmacValidationKey = expandedKey.slice(32, 64)
|
||||
const hmacValidationMessage = Buffer.concat([secret.slice(0, 32), secret.slice(64, secret.length)])
|
||||
|
||||
const hmac = hmacSign(hmacValidationMessage, hmacValidationKey)
|
||||
const hmac = hmacSign(hmacValidationMessage, hmacValidationKey)
|
||||
|
||||
if (!hmac.equals(secret.slice(32, 64))) {
|
||||
if(!hmac.equals(secret.slice(32, 64))) {
|
||||
// if the checksums didn't match
|
||||
throw new Boom('HMAC validation failed', { statusCode: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
// computed HMAC should equal secret[32:64]
|
||||
// expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp
|
||||
// they are encrypted using key: expandedKey[0:32]
|
||||
const encryptedAESKeys = Buffer.concat([
|
||||
// computed HMAC should equal secret[32:64]
|
||||
// expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp
|
||||
// they are encrypted using key: expandedKey[0:32]
|
||||
const encryptedAESKeys = Buffer.concat([
|
||||
expandedKey.slice(64, expandedKey.length),
|
||||
secret.slice(64, secret.length),
|
||||
])
|
||||
const decryptedKeys = aesDecrypt(encryptedAESKeys, expandedKey.slice(0, 32))
|
||||
// set the credentials
|
||||
auth = {
|
||||
])
|
||||
const decryptedKeys = aesDecrypt(encryptedAESKeys, expandedKey.slice(0, 32))
|
||||
// set the credentials
|
||||
auth = {
|
||||
encKey: decryptedKeys.slice(0, 32), // first 32 bytes form the key to encrypt/decrypt messages
|
||||
macKey: decryptedKeys.slice(32, 64), // last 32 bytes from the key to sign messages
|
||||
clientToken: json.clientToken,
|
||||
serverToken: json.serverToken,
|
||||
clientID: auth.clientID,
|
||||
}
|
||||
return onValidationSuccess()
|
||||
}
|
||||
return onValidationSuccess()
|
||||
}
|
||||
|
||||
export const computeChallengeResponse = (challenge: string, auth: LegacyAuthenticationCreds) => {
|
||||
const bytes = Buffer.from(challenge, 'base64') // decode the base64 encoded challenge string
|
||||
const signed = hmacSign(bytes, auth.macKey).toString('base64') // sign the challenge string with our macKey
|
||||
return['admin', 'challenge', signed, auth.serverToken, auth.clientID] // prepare to send this signed string with the serverToken & clientID
|
||||
return ['admin', 'challenge', signed, auth.serverToken, auth.clientID] // prepare to send this signed string with the serverToken & clientID
|
||||
}
|
||||
|
||||
export const useSingleFileLegacyAuthState = (file: string) => {
|
||||
// require fs here so that in case "fs" is not available -- the app does not crash
|
||||
// require fs here so that in case "fs" is not available -- the app does not crash
|
||||
const { readFileSync, writeFileSync, existsSync } = require('fs')
|
||||
let state: LegacyAuthenticationCreds
|
||||
let state: LegacyAuthenticationCreds
|
||||
|
||||
if(existsSync(file)) {
|
||||
state = JSON.parse(
|
||||
readFileSync(file, { encoding: 'utf-8' }),
|
||||
BufferJSON.reviver
|
||||
)
|
||||
if(typeof state.encKey === 'string') {
|
||||
state.encKey = Buffer.from(state.encKey, 'base64')
|
||||
}
|
||||
if(typeof state.macKey === 'string') {
|
||||
state.macKey = Buffer.from(state.macKey, 'base64')
|
||||
}
|
||||
} else {
|
||||
state = newLegacyAuthCreds()
|
||||
}
|
||||
if(existsSync(file)) {
|
||||
state = JSON.parse(
|
||||
readFileSync(file, { encoding: 'utf-8' }),
|
||||
BufferJSON.reviver
|
||||
)
|
||||
if(typeof state.encKey === 'string') {
|
||||
state.encKey = Buffer.from(state.encKey, 'base64')
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
saveState: () => {
|
||||
const str = JSON.stringify(state, BufferJSON.replacer, 2)
|
||||
writeFileSync(file, str)
|
||||
}
|
||||
}
|
||||
if(typeof state.macKey === 'string') {
|
||||
state.macKey = Buffer.from(state.macKey, 'base64')
|
||||
}
|
||||
} else {
|
||||
state = newLegacyAuthCreds()
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
saveState: () => {
|
||||
const str = JSON.stringify(state, BufferJSON.replacer, 2)
|
||||
writeFileSync(file, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getAuthenticationCredsType = (creds: LegacyAuthenticationCreds | AuthenticationCreds) => {
|
||||
if('clientID' in creds && !!creds.clientID) {
|
||||
return 'legacy'
|
||||
}
|
||||
if('noiseKey' in creds && !!creds.noiseKey) {
|
||||
return 'md'
|
||||
}
|
||||
if('clientID' in creds && !!creds.clientID) {
|
||||
return 'legacy'
|
||||
}
|
||||
|
||||
if('noiseKey' in creds && !!creds.noiseKey) {
|
||||
return 'md'
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { hkdf } from './crypto'
|
||||
* if the same series of mutations was made sequentially.
|
||||
*/
|
||||
|
||||
const o = 128;
|
||||
const o = 128
|
||||
|
||||
class d {
|
||||
|
||||
@@ -16,41 +16,45 @@ class d {
|
||||
this.salt = e
|
||||
}
|
||||
add(e, t) {
|
||||
var r = this;
|
||||
var r = this
|
||||
for(const item of t) {
|
||||
e = r._addSingle(e, item)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
subtract(e, t) {
|
||||
var r = this;
|
||||
var r = this
|
||||
for(const item of t) {
|
||||
e = r._subtractSingle(e, item)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
subtractThenAdd(e, t, r) {
|
||||
var n = this;
|
||||
var n = this
|
||||
return n.add(n.subtract(e, r), t)
|
||||
}
|
||||
_addSingle(e, t) {
|
||||
var r = this;
|
||||
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer;
|
||||
return r.performPointwiseWithOverflow(e, n, ((e,t)=>e + t))
|
||||
var r = this
|
||||
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer
|
||||
return r.performPointwiseWithOverflow(e, n, ((e, t) => e + t))
|
||||
}
|
||||
_subtractSingle(e, t) {
|
||||
var r = this;
|
||||
var r = this
|
||||
|
||||
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer;
|
||||
return r.performPointwiseWithOverflow(e, n, ((e,t)=>e - t))
|
||||
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer
|
||||
return r.performPointwiseWithOverflow(e, n, ((e, t) => e - t))
|
||||
}
|
||||
performPointwiseWithOverflow(e, t, r) {
|
||||
const n = new DataView(e)
|
||||
, i = new DataView(t)
|
||||
, a = new ArrayBuffer(n.byteLength)
|
||||
, s = new DataView(a);
|
||||
for (let e = 0; e < n.byteLength; e += 2)
|
||||
s.setUint16(e, r(n.getUint16(e, !0), i.getUint16(e, !0)), !0);
|
||||
, s = new DataView(a)
|
||||
for(let e = 0; e < n.byteLength; e += 2) {
|
||||
s.setUint16(e, r(n.getUint16(e, !0), i.getUint16(e, !0)), !0)
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,20 @@ export default () => {
|
||||
let task = Promise.resolve() as Promise<any>
|
||||
return {
|
||||
mutex<T>(code: () => Promise<T>):Promise<T> {
|
||||
task = (async () => {
|
||||
task = (async() => {
|
||||
// wait for the previous task to complete
|
||||
// if there is an error, we swallow so as to not block the queue
|
||||
try { await task } catch { }
|
||||
try {
|
||||
await task
|
||||
} catch{ }
|
||||
|
||||
// execute the current task
|
||||
return code()
|
||||
})()
|
||||
// we replace the existing task, appending the new piece of execution to it
|
||||
// so the next task will have to wait for this one to finish
|
||||
return task
|
||||
})()
|
||||
// we replace the existing task, appending the new piece of execution to it
|
||||
// so the next task will have to wait for this one to finish
|
||||
return task
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,303 +1,341 @@
|
||||
import type { Logger } from 'pino'
|
||||
import type { IAudioMetadata } from 'music-metadata'
|
||||
import { Boom } from '@hapi/boom'
|
||||
import * as Crypto from 'crypto'
|
||||
import { Readable, Transform } from 'stream'
|
||||
import { createReadStream, createWriteStream, promises as fs, WriteStream } from 'fs'
|
||||
import { exec } from 'child_process'
|
||||
import { tmpdir } from 'os'
|
||||
import { URL } from 'url'
|
||||
import { join } from 'path'
|
||||
import { once } from 'events'
|
||||
import { MessageType, WAMessageContent, WAProto, WAGenericMediaMessage, WAMediaUpload, MediaType, DownloadableMessage, CommonSocketConfig, WAMediaUploadFunction, MediaConnInfo } from '../Types'
|
||||
import { generateMessageID } from './generics'
|
||||
import { hkdf } from './crypto'
|
||||
import { DEFAULT_ORIGIN, MEDIA_PATH_MAP } from '../Defaults'
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
import { exec } from 'child_process'
|
||||
import * as Crypto from 'crypto'
|
||||
import { once } from 'events'
|
||||
import { createReadStream, createWriteStream, promises as fs, WriteStream } from 'fs'
|
||||
import type { IAudioMetadata } from 'music-metadata'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import type { Logger } from 'pino'
|
||||
import { Readable, Transform } from 'stream'
|
||||
import { URL } from 'url'
|
||||
import { DEFAULT_ORIGIN, MEDIA_PATH_MAP } from '../Defaults'
|
||||
import { CommonSocketConfig, DownloadableMessage, MediaConnInfo, MediaType, MessageType, WAGenericMediaMessage, WAMediaUpload, WAMediaUploadFunction, WAMessageContent, WAProto } from '../Types'
|
||||
import { hkdf } from './crypto'
|
||||
import { generateMessageID } from './generics'
|
||||
|
||||
const getTmpFilesDirectory = () => tmpdir()
|
||||
|
||||
const getImageProcessingLibrary = async() => {
|
||||
const [jimp, sharp] = await Promise.all([
|
||||
(async() => {
|
||||
const jimp = await (
|
||||
import('jimp')
|
||||
.catch(() => { })
|
||||
)
|
||||
return jimp
|
||||
})(),
|
||||
(async() => {
|
||||
const sharp = await (
|
||||
import('sharp')
|
||||
.catch(() => { })
|
||||
)
|
||||
return sharp
|
||||
})()
|
||||
])
|
||||
if(sharp) return { sharp }
|
||||
if(jimp) return { jimp }
|
||||
const [jimp, sharp] = await Promise.all([
|
||||
(async() => {
|
||||
const jimp = await (
|
||||
import('jimp')
|
||||
.catch(() => { })
|
||||
)
|
||||
return jimp
|
||||
})(),
|
||||
(async() => {
|
||||
const sharp = await (
|
||||
import('sharp')
|
||||
.catch(() => { })
|
||||
)
|
||||
return sharp
|
||||
})()
|
||||
])
|
||||
if(sharp) {
|
||||
return { sharp }
|
||||
}
|
||||
|
||||
throw new Boom('No image processing library available')
|
||||
if(jimp) {
|
||||
return { jimp }
|
||||
}
|
||||
|
||||
throw new Boom('No image processing library available')
|
||||
}
|
||||
|
||||
export const hkdfInfoKey = (type: MediaType) => {
|
||||
let str: string = type
|
||||
if(type === 'sticker') str = 'image'
|
||||
if(type === 'md-app-state') str = 'App State'
|
||||
let str: string = type
|
||||
if(type === 'sticker') {
|
||||
str = 'image'
|
||||
}
|
||||
|
||||
if(type === 'md-app-state') {
|
||||
str = 'App State'
|
||||
}
|
||||
|
||||
let hkdfInfo = str[0].toUpperCase() + str.slice(1)
|
||||
const hkdfInfo = str[0].toUpperCase() + str.slice(1)
|
||||
return `WhatsApp ${hkdfInfo} Keys`
|
||||
}
|
||||
|
||||
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
||||
export function getMediaKeys(buffer, mediaType: MediaType) {
|
||||
if (typeof buffer === 'string') {
|
||||
buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64')
|
||||
}
|
||||
// expand using HKDF to 112 bytes, also pass in the relevant app info
|
||||
const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
|
||||
return {
|
||||
iv: expandedMediaKey.slice(0, 16),
|
||||
cipherKey: expandedMediaKey.slice(16, 48),
|
||||
macKey: expandedMediaKey.slice(48, 80),
|
||||
}
|
||||
if(typeof buffer === 'string') {
|
||||
buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64')
|
||||
}
|
||||
|
||||
// expand using HKDF to 112 bytes, also pass in the relevant app info
|
||||
const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
|
||||
return {
|
||||
iv: expandedMediaKey.slice(0, 16),
|
||||
cipherKey: expandedMediaKey.slice(16, 48),
|
||||
macKey: expandedMediaKey.slice(48, 80),
|
||||
}
|
||||
}
|
||||
|
||||
/** Extracts video thumb using FFMPEG */
|
||||
const extractVideoThumb = async (
|
||||
path: string,
|
||||
destPath: string,
|
||||
time: string,
|
||||
size: { width: number; height: number },
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}`
|
||||
exec(cmd, (err) => {
|
||||
if (err) reject(err)
|
||||
else resolve()
|
||||
})
|
||||
}) as Promise<void>
|
||||
const extractVideoThumb = async(
|
||||
path: string,
|
||||
destPath: string,
|
||||
time: string,
|
||||
size: { width: number; height: number },
|
||||
) => new Promise((resolve, reject) => {
|
||||
const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}`
|
||||
exec(cmd, (err) => {
|
||||
if(err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}) as Promise<void>
|
||||
|
||||
export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | string) => {
|
||||
if(bufferOrFilePath instanceof Readable) {
|
||||
bufferOrFilePath = await toBuffer(bufferOrFilePath)
|
||||
}
|
||||
const lib = await getImageProcessingLibrary()
|
||||
if('sharp' in lib) {
|
||||
const result = await lib.sharp!.default(bufferOrFilePath)
|
||||
.resize(32, 32)
|
||||
.jpeg({ quality: 50 })
|
||||
.toBuffer()
|
||||
return result
|
||||
} else {
|
||||
const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp
|
||||
export const extractImageThumb = async(bufferOrFilePath: Readable | Buffer | string) => {
|
||||
if(bufferOrFilePath instanceof Readable) {
|
||||
bufferOrFilePath = await toBuffer(bufferOrFilePath)
|
||||
}
|
||||
|
||||
const jimp = await read(bufferOrFilePath as any)
|
||||
const result = await jimp
|
||||
.quality(50)
|
||||
.resize(32, 32, RESIZE_BILINEAR)
|
||||
.getBufferAsync(MIME_JPEG)
|
||||
return result
|
||||
}
|
||||
const lib = await getImageProcessingLibrary()
|
||||
if('sharp' in lib) {
|
||||
const result = await lib.sharp!.default(bufferOrFilePath)
|
||||
.resize(32, 32)
|
||||
.jpeg({ quality: 50 })
|
||||
.toBuffer()
|
||||
return result
|
||||
} else {
|
||||
const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp
|
||||
|
||||
const jimp = await read(bufferOrFilePath as any)
|
||||
const result = await jimp
|
||||
.quality(50)
|
||||
.resize(32, 32, RESIZE_BILINEAR)
|
||||
.getBufferAsync(MIME_JPEG)
|
||||
return result
|
||||
}
|
||||
}
|
||||
export const generateProfilePicture = async (mediaUpload: WAMediaUpload) => {
|
||||
let bufferOrFilePath: Buffer | string
|
||||
if(Buffer.isBuffer(mediaUpload)) {
|
||||
bufferOrFilePath = mediaUpload
|
||||
} else if('url' in mediaUpload) {
|
||||
bufferOrFilePath = mediaUpload.url.toString()
|
||||
} else {
|
||||
bufferOrFilePath = await toBuffer(mediaUpload.stream)
|
||||
}
|
||||
|
||||
const lib = await getImageProcessingLibrary()
|
||||
let img: Promise<Buffer>
|
||||
if('sharp' in lib) {
|
||||
img = lib.sharp!.default(bufferOrFilePath)
|
||||
.resize(640, 640)
|
||||
.jpeg({
|
||||
quality: 50,
|
||||
})
|
||||
.toBuffer()
|
||||
} else {
|
||||
const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp
|
||||
const jimp = await read(bufferOrFilePath as any)
|
||||
const min = Math.min(jimp.getWidth(), jimp.getHeight())
|
||||
const cropped = jimp.crop(0, 0, min, min)
|
||||
export const generateProfilePicture = async(mediaUpload: WAMediaUpload) => {
|
||||
let bufferOrFilePath: Buffer | string
|
||||
if(Buffer.isBuffer(mediaUpload)) {
|
||||
bufferOrFilePath = mediaUpload
|
||||
} else if('url' in mediaUpload) {
|
||||
bufferOrFilePath = mediaUpload.url.toString()
|
||||
} else {
|
||||
bufferOrFilePath = await toBuffer(mediaUpload.stream)
|
||||
}
|
||||
|
||||
img = cropped
|
||||
.quality(50)
|
||||
.resize(640, 640, RESIZE_BILINEAR)
|
||||
.getBufferAsync(MIME_JPEG)
|
||||
}
|
||||
const lib = await getImageProcessingLibrary()
|
||||
let img: Promise<Buffer>
|
||||
if('sharp' in lib) {
|
||||
img = lib.sharp!.default(bufferOrFilePath)
|
||||
.resize(640, 640)
|
||||
.jpeg({
|
||||
quality: 50,
|
||||
})
|
||||
.toBuffer()
|
||||
} else {
|
||||
const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp
|
||||
const jimp = await read(bufferOrFilePath as any)
|
||||
const min = Math.min(jimp.getWidth(), jimp.getHeight())
|
||||
const cropped = jimp.crop(0, 0, min, min)
|
||||
|
||||
img = cropped
|
||||
.quality(50)
|
||||
.resize(640, 640, RESIZE_BILINEAR)
|
||||
.getBufferAsync(MIME_JPEG)
|
||||
}
|
||||
|
||||
return {
|
||||
img: await img,
|
||||
}
|
||||
return {
|
||||
img: await img,
|
||||
}
|
||||
}
|
||||
|
||||
/** gets the SHA256 of the given media message */
|
||||
export const mediaMessageSHA256B64 = (message: WAMessageContent) => {
|
||||
const media = Object.values(message)[0] as WAGenericMediaMessage
|
||||
return media?.fileSha256 && Buffer.from(media.fileSha256).toString ('base64')
|
||||
const media = Object.values(message)[0] as WAGenericMediaMessage
|
||||
return media?.fileSha256 && Buffer.from(media.fileSha256).toString ('base64')
|
||||
}
|
||||
export async function getAudioDuration (buffer: Buffer | string | Readable) {
|
||||
const musicMetadata = await import('music-metadata')
|
||||
let metadata: IAudioMetadata
|
||||
if(Buffer.isBuffer(buffer)) {
|
||||
metadata = await musicMetadata.parseBuffer(buffer, null, { duration: true })
|
||||
} else if(typeof buffer === 'string') {
|
||||
const rStream = createReadStream(buffer)
|
||||
metadata = await musicMetadata.parseStream(rStream, null, { duration: true })
|
||||
rStream.close()
|
||||
} else {
|
||||
metadata = await musicMetadata.parseStream(buffer, null, { duration: true })
|
||||
}
|
||||
return metadata.format.duration;
|
||||
|
||||
export async function getAudioDuration(buffer: Buffer | string | Readable) {
|
||||
const musicMetadata = await import('music-metadata')
|
||||
let metadata: IAudioMetadata
|
||||
if(Buffer.isBuffer(buffer)) {
|
||||
metadata = await musicMetadata.parseBuffer(buffer, null, { duration: true })
|
||||
} else if(typeof buffer === 'string') {
|
||||
const rStream = createReadStream(buffer)
|
||||
metadata = await musicMetadata.parseStream(rStream, null, { duration: true })
|
||||
rStream.close()
|
||||
} else {
|
||||
metadata = await musicMetadata.parseStream(buffer, null, { duration: true })
|
||||
}
|
||||
|
||||
return metadata.format.duration
|
||||
}
|
||||
|
||||
export const toReadable = (buffer: Buffer) => {
|
||||
const readable = new Readable({ read: () => {} })
|
||||
readable.push(buffer)
|
||||
readable.push(null)
|
||||
return readable
|
||||
const readable = new Readable({ read: () => {} })
|
||||
readable.push(buffer)
|
||||
readable.push(null)
|
||||
return readable
|
||||
}
|
||||
|
||||
export const toBuffer = async(stream: Readable) => {
|
||||
let buff = Buffer.alloc(0)
|
||||
for await(const chunk of stream) {
|
||||
buff = Buffer.concat([ buff, chunk ])
|
||||
}
|
||||
return buff
|
||||
let buff = Buffer.alloc(0)
|
||||
for await (const chunk of stream) {
|
||||
buff = Buffer.concat([ buff, chunk ])
|
||||
}
|
||||
|
||||
return buff
|
||||
}
|
||||
export const getStream = async (item: WAMediaUpload) => {
|
||||
if(Buffer.isBuffer(item)) return { stream: toReadable(item), type: 'buffer' }
|
||||
if('stream' in item) return { stream: item.stream, type: 'readable' }
|
||||
if(item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
|
||||
return { stream: await getHttpStream(item.url), type: 'remote' }
|
||||
}
|
||||
return { stream: createReadStream(item.url), type: 'file' }
|
||||
|
||||
export const getStream = async(item: WAMediaUpload) => {
|
||||
if(Buffer.isBuffer(item)) {
|
||||
return { stream: toReadable(item), type: 'buffer' }
|
||||
}
|
||||
|
||||
if('stream' in item) {
|
||||
return { stream: item.stream, type: 'readable' }
|
||||
}
|
||||
|
||||
if(item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
|
||||
return { stream: await getHttpStream(item.url), type: 'remote' }
|
||||
}
|
||||
|
||||
return { stream: createReadStream(item.url), type: 'file' }
|
||||
}
|
||||
|
||||
/** generates a thumbnail for a given media, if required */
|
||||
export async function generateThumbnail(
|
||||
file: string,
|
||||
mediaType: 'video' | 'image',
|
||||
options: {
|
||||
file: string,
|
||||
mediaType: 'video' | 'image',
|
||||
options: {
|
||||
logger?: Logger
|
||||
}
|
||||
) {
|
||||
let thumbnail: string
|
||||
if(mediaType === 'image') {
|
||||
const buff = await extractImageThumb(file)
|
||||
thumbnail = buff.toString('base64')
|
||||
} else if(mediaType === 'video') {
|
||||
const imgFilename = join(getTmpFilesDirectory(), generateMessageID() + '.jpg')
|
||||
try {
|
||||
await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 })
|
||||
const buff = await fs.readFile(imgFilename)
|
||||
thumbnail = buff.toString('base64')
|
||||
let thumbnail: string
|
||||
if(mediaType === 'image') {
|
||||
const buff = await extractImageThumb(file)
|
||||
thumbnail = buff.toString('base64')
|
||||
} else if(mediaType === 'video') {
|
||||
const imgFilename = join(getTmpFilesDirectory(), generateMessageID() + '.jpg')
|
||||
try {
|
||||
await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 })
|
||||
const buff = await fs.readFile(imgFilename)
|
||||
thumbnail = buff.toString('base64')
|
||||
|
||||
await fs.unlink(imgFilename)
|
||||
} catch (err) {
|
||||
options.logger?.debug('could not generate video thumb: ' + err)
|
||||
}
|
||||
}
|
||||
await fs.unlink(imgFilename)
|
||||
} catch(err) {
|
||||
options.logger?.debug('could not generate video thumb: ' + err)
|
||||
}
|
||||
}
|
||||
|
||||
return thumbnail
|
||||
return thumbnail
|
||||
}
|
||||
|
||||
export const getHttpStream = async(url: string | URL, options: AxiosRequestConfig & { isStream?: true } = {}) => {
|
||||
const { default: axios } = await import('axios')
|
||||
const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' })
|
||||
return fetched.data as Readable
|
||||
}
|
||||
const { default: axios } = await import('axios')
|
||||
const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' })
|
||||
return fetched.data as Readable
|
||||
}
|
||||
|
||||
export const encryptedStream = async(
|
||||
media: WAMediaUpload,
|
||||
mediaType: MediaType,
|
||||
saveOriginalFileIfRequired = true,
|
||||
logger?: Logger
|
||||
media: WAMediaUpload,
|
||||
mediaType: MediaType,
|
||||
saveOriginalFileIfRequired = true,
|
||||
logger?: Logger
|
||||
) => {
|
||||
const { stream, type } = await getStream(media)
|
||||
const { stream, type } = await getStream(media)
|
||||
|
||||
logger?.debug('fetched media stream')
|
||||
logger?.debug('fetched media stream')
|
||||
|
||||
const mediaKey = Crypto.randomBytes(32)
|
||||
const {cipherKey, iv, macKey} = getMediaKeys(mediaKey, mediaType)
|
||||
// random name
|
||||
//const encBodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '.enc')
|
||||
// const encWriteStream = createWriteStream(encBodyPath)
|
||||
const encWriteStream = new Readable({ read: () => {} })
|
||||
const mediaKey = Crypto.randomBytes(32)
|
||||
const { cipherKey, iv, macKey } = getMediaKeys(mediaKey, mediaType)
|
||||
// random name
|
||||
//const encBodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '.enc')
|
||||
// const encWriteStream = createWriteStream(encBodyPath)
|
||||
const encWriteStream = new Readable({ read: () => {} })
|
||||
|
||||
let bodyPath: string
|
||||
let writeStream: WriteStream
|
||||
let didSaveToTmpPath = false
|
||||
if(type === 'file') {
|
||||
bodyPath = (media as any).url
|
||||
} else if(saveOriginalFileIfRequired) {
|
||||
bodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageID())
|
||||
writeStream = createWriteStream(bodyPath)
|
||||
didSaveToTmpPath = true
|
||||
}
|
||||
let bodyPath: string
|
||||
let writeStream: WriteStream
|
||||
let didSaveToTmpPath = false
|
||||
if(type === 'file') {
|
||||
bodyPath = (media as any).url
|
||||
} else if(saveOriginalFileIfRequired) {
|
||||
bodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageID())
|
||||
writeStream = createWriteStream(bodyPath)
|
||||
didSaveToTmpPath = true
|
||||
}
|
||||
|
||||
let fileLength = 0
|
||||
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv)
|
||||
let hmac = Crypto.createHmac('sha256', macKey).update(iv)
|
||||
let sha256Plain = Crypto.createHash('sha256')
|
||||
let sha256Enc = Crypto.createHash('sha256')
|
||||
let fileLength = 0
|
||||
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv)
|
||||
let hmac = Crypto.createHmac('sha256', macKey).update(iv)
|
||||
let sha256Plain = Crypto.createHash('sha256')
|
||||
let sha256Enc = Crypto.createHash('sha256')
|
||||
|
||||
const onChunk = (buff: Buffer) => {
|
||||
sha256Enc = sha256Enc.update(buff)
|
||||
hmac = hmac.update(buff)
|
||||
encWriteStream.push(buff)
|
||||
}
|
||||
const onChunk = (buff: Buffer) => {
|
||||
sha256Enc = sha256Enc.update(buff)
|
||||
hmac = hmac.update(buff)
|
||||
encWriteStream.push(buff)
|
||||
}
|
||||
|
||||
try {
|
||||
for await(const data of stream) {
|
||||
fileLength += data.length
|
||||
sha256Plain = sha256Plain.update(data)
|
||||
if(writeStream) {
|
||||
if(!writeStream.write(data)) await once(writeStream, 'drain')
|
||||
}
|
||||
onChunk(aes.update(data))
|
||||
}
|
||||
onChunk(aes.final())
|
||||
try {
|
||||
for await (const data of stream) {
|
||||
fileLength += data.length
|
||||
sha256Plain = sha256Plain.update(data)
|
||||
if(writeStream) {
|
||||
if(!writeStream.write(data)) {
|
||||
await once(writeStream, 'drain')
|
||||
}
|
||||
}
|
||||
|
||||
onChunk(aes.update(data))
|
||||
}
|
||||
|
||||
onChunk(aes.final())
|
||||
|
||||
const mac = hmac.digest().slice(0, 10)
|
||||
sha256Enc = sha256Enc.update(mac)
|
||||
const mac = hmac.digest().slice(0, 10)
|
||||
sha256Enc = sha256Enc.update(mac)
|
||||
|
||||
const fileSha256 = sha256Plain.digest()
|
||||
const fileEncSha256 = sha256Enc.digest()
|
||||
const fileSha256 = sha256Plain.digest()
|
||||
const fileEncSha256 = sha256Enc.digest()
|
||||
|
||||
encWriteStream.push(mac)
|
||||
encWriteStream.push(null)
|
||||
encWriteStream.push(mac)
|
||||
encWriteStream.push(null)
|
||||
|
||||
writeStream && writeStream.end()
|
||||
stream.destroy()
|
||||
writeStream && writeStream.end()
|
||||
stream.destroy()
|
||||
|
||||
logger?.debug('encrypted data successfully')
|
||||
logger?.debug('encrypted data successfully')
|
||||
|
||||
return {
|
||||
mediaKey,
|
||||
encWriteStream,
|
||||
bodyPath,
|
||||
mac,
|
||||
fileEncSha256,
|
||||
fileSha256,
|
||||
fileLength,
|
||||
didSaveToTmpPath
|
||||
}
|
||||
} catch(error) {
|
||||
encWriteStream.destroy(error)
|
||||
writeStream.destroy(error)
|
||||
aes.destroy(error)
|
||||
hmac.destroy(error)
|
||||
sha256Plain.destroy(error)
|
||||
sha256Enc.destroy(error)
|
||||
stream.destroy(error)
|
||||
return {
|
||||
mediaKey,
|
||||
encWriteStream,
|
||||
bodyPath,
|
||||
mac,
|
||||
fileEncSha256,
|
||||
fileSha256,
|
||||
fileLength,
|
||||
didSaveToTmpPath
|
||||
}
|
||||
} catch(error) {
|
||||
encWriteStream.destroy(error)
|
||||
writeStream.destroy(error)
|
||||
aes.destroy(error)
|
||||
hmac.destroy(error)
|
||||
sha256Plain.destroy(error)
|
||||
sha256Enc.destroy(error)
|
||||
stream.destroy(error)
|
||||
|
||||
throw error
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const DEF_HOST = 'mmg.whatsapp.net'
|
||||
const AES_CHUNK_SIZE = 16
|
||||
|
||||
const toSmallestChunkSize = (num: number) => {
|
||||
return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE
|
||||
return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE
|
||||
}
|
||||
|
||||
type MediaDownloadOptions = {
|
||||
@@ -306,103 +344,106 @@ type MediaDownloadOptions = {
|
||||
}
|
||||
|
||||
export const downloadContentFromMessage = async(
|
||||
{ mediaKey, directPath, url }: DownloadableMessage,
|
||||
type: MediaType,
|
||||
{ startByte, endByte }: MediaDownloadOptions = { }
|
||||
{ mediaKey, directPath, url }: DownloadableMessage,
|
||||
type: MediaType,
|
||||
{ startByte, endByte }: MediaDownloadOptions = { }
|
||||
) => {
|
||||
const downloadUrl = url || `https://${DEF_HOST}${directPath}`
|
||||
let bytesFetched = 0
|
||||
let startChunk = 0
|
||||
let firstBlockIsIV = false
|
||||
// if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
|
||||
if(startByte) {
|
||||
const chunk = toSmallestChunkSize(startByte || 0)
|
||||
if(chunk) {
|
||||
startChunk = chunk-AES_CHUNK_SIZE
|
||||
bytesFetched = chunk
|
||||
const downloadUrl = url || `https://${DEF_HOST}${directPath}`
|
||||
let bytesFetched = 0
|
||||
let startChunk = 0
|
||||
let firstBlockIsIV = false
|
||||
// if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
|
||||
if(startByte) {
|
||||
const chunk = toSmallestChunkSize(startByte || 0)
|
||||
if(chunk) {
|
||||
startChunk = chunk-AES_CHUNK_SIZE
|
||||
bytesFetched = chunk
|
||||
|
||||
firstBlockIsIV = true
|
||||
}
|
||||
}
|
||||
const endChunk = endByte ? toSmallestChunkSize(endByte || 0)+AES_CHUNK_SIZE : undefined
|
||||
firstBlockIsIV = true
|
||||
}
|
||||
}
|
||||
|
||||
const headers: { [_: string]: string } = {
|
||||
Origin: DEFAULT_ORIGIN,
|
||||
}
|
||||
if(startChunk || endChunk) {
|
||||
headers.Range = `bytes=${startChunk}-`
|
||||
if(endChunk) headers.Range += endChunk
|
||||
}
|
||||
const endChunk = endByte ? toSmallestChunkSize(endByte || 0)+AES_CHUNK_SIZE : undefined
|
||||
|
||||
// download the message
|
||||
const fetched = await getHttpStream(
|
||||
downloadUrl,
|
||||
{
|
||||
headers,
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity,
|
||||
}
|
||||
)
|
||||
const headers: { [_: string]: string } = {
|
||||
Origin: DEFAULT_ORIGIN,
|
||||
}
|
||||
if(startChunk || endChunk) {
|
||||
headers.Range = `bytes=${startChunk}-`
|
||||
if(endChunk) {
|
||||
headers.Range += endChunk
|
||||
}
|
||||
}
|
||||
|
||||
let remainingBytes = Buffer.from([])
|
||||
const { cipherKey, iv } = getMediaKeys(mediaKey, type)
|
||||
// download the message
|
||||
const fetched = await getHttpStream(
|
||||
downloadUrl,
|
||||
{
|
||||
headers,
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity,
|
||||
}
|
||||
)
|
||||
|
||||
let aes: Crypto.Decipher
|
||||
let remainingBytes = Buffer.from([])
|
||||
const { cipherKey, iv } = getMediaKeys(mediaKey, type)
|
||||
|
||||
const pushBytes = (bytes: Buffer, push: (bytes: Buffer) => void) => {
|
||||
if(startByte || endByte) {
|
||||
const start = bytesFetched >= startByte ? undefined : Math.max(startByte-bytesFetched, 0)
|
||||
const end = bytesFetched+bytes.length < endByte ? undefined : Math.max(endByte-bytesFetched, 0)
|
||||
let aes: Crypto.Decipher
|
||||
|
||||
const pushBytes = (bytes: Buffer, push: (bytes: Buffer) => void) => {
|
||||
if(startByte || endByte) {
|
||||
const start = bytesFetched >= startByte ? undefined : Math.max(startByte-bytesFetched, 0)
|
||||
const end = bytesFetched+bytes.length < endByte ? undefined : Math.max(endByte-bytesFetched, 0)
|
||||
|
||||
push(bytes.slice(start, end))
|
||||
push(bytes.slice(start, end))
|
||||
|
||||
bytesFetched += bytes.length
|
||||
} else {
|
||||
push(bytes)
|
||||
}
|
||||
}
|
||||
bytesFetched += bytes.length
|
||||
} else {
|
||||
push(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
const output = new Transform({
|
||||
transform(chunk, _, callback) {
|
||||
let data = Buffer.concat([remainingBytes, chunk])
|
||||
const output = new Transform({
|
||||
transform(chunk, _, callback) {
|
||||
let data = Buffer.concat([remainingBytes, chunk])
|
||||
|
||||
const decryptLength = toSmallestChunkSize(data.length)
|
||||
remainingBytes = data.slice(decryptLength)
|
||||
data = data.slice(0, decryptLength)
|
||||
const decryptLength = toSmallestChunkSize(data.length)
|
||||
remainingBytes = data.slice(decryptLength)
|
||||
data = data.slice(0, decryptLength)
|
||||
|
||||
if(!aes) {
|
||||
let ivValue = iv
|
||||
if(firstBlockIsIV) {
|
||||
ivValue = data.slice(0, AES_CHUNK_SIZE)
|
||||
data = data.slice(AES_CHUNK_SIZE)
|
||||
}
|
||||
if(!aes) {
|
||||
let ivValue = iv
|
||||
if(firstBlockIsIV) {
|
||||
ivValue = data.slice(0, AES_CHUNK_SIZE)
|
||||
data = data.slice(AES_CHUNK_SIZE)
|
||||
}
|
||||
|
||||
aes = Crypto.createDecipheriv("aes-256-cbc", cipherKey, ivValue)
|
||||
// if an end byte that is not EOF is specified
|
||||
// stop auto padding (PKCS7) -- otherwise throws an error for decryption
|
||||
if(endByte) {
|
||||
aes.setAutoPadding(false)
|
||||
}
|
||||
aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue)
|
||||
// if an end byte that is not EOF is specified
|
||||
// stop auto padding (PKCS7) -- otherwise throws an error for decryption
|
||||
if(endByte) {
|
||||
aes.setAutoPadding(false)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
pushBytes(aes.update(data), b => this.push(b))
|
||||
callback()
|
||||
} catch(error) {
|
||||
callback(error)
|
||||
}
|
||||
},
|
||||
final(callback) {
|
||||
try {
|
||||
pushBytes(aes.final(), b => this.push(b))
|
||||
callback()
|
||||
} catch(error) {
|
||||
callback(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
return fetched.pipe(output, { end: true })
|
||||
try {
|
||||
pushBytes(aes.update(data), b => this.push(b))
|
||||
callback()
|
||||
} catch(error) {
|
||||
callback(error)
|
||||
}
|
||||
},
|
||||
final(callback) {
|
||||
try {
|
||||
pushBytes(aes.final(), b => this.push(b))
|
||||
callback()
|
||||
} catch(error) {
|
||||
callback(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
return fetched.pipe(output, { end: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,121 +451,130 @@ export const downloadContentFromMessage = async(
|
||||
* @param message the media message you want to decode
|
||||
*/
|
||||
export async function decryptMediaMessageBuffer(message: WAMessageContent): Promise<Readable> {
|
||||
/*
|
||||
/*
|
||||
One can infer media type from the key in the message
|
||||
it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc.
|
||||
*/
|
||||
const type = Object.keys(message)[0] as MessageType
|
||||
if(
|
||||
const type = Object.keys(message)[0] as MessageType
|
||||
if(
|
||||
!type ||
|
||||
type === 'conversation' ||
|
||||
type === 'extendedTextMessage'
|
||||
) {
|
||||
throw new Boom(`no media message for "${type}"`, { statusCode: 400 })
|
||||
}
|
||||
if (type === 'locationMessage' || type === 'liveLocationMessage') {
|
||||
const buffer = Buffer.from(message[type].jpegThumbnail)
|
||||
const readable = new Readable({ read: () => {} })
|
||||
readable.push(buffer)
|
||||
readable.push(null)
|
||||
return readable
|
||||
}
|
||||
let messageContent: WAGenericMediaMessage
|
||||
if (message.productMessage) {
|
||||
const product = message.productMessage.product?.productImage
|
||||
if (!product) throw new Boom('product has no image', { statusCode: 400 })
|
||||
messageContent = product
|
||||
} else {
|
||||
messageContent = message[type]
|
||||
}
|
||||
return downloadContentFromMessage(messageContent, type.replace('Message', '') as MediaType)
|
||||
throw new Boom(`no media message for "${type}"`, { statusCode: 400 })
|
||||
}
|
||||
|
||||
if(type === 'locationMessage' || type === 'liveLocationMessage') {
|
||||
const buffer = Buffer.from(message[type].jpegThumbnail)
|
||||
const readable = new Readable({ read: () => {} })
|
||||
readable.push(buffer)
|
||||
readable.push(null)
|
||||
return readable
|
||||
}
|
||||
|
||||
let messageContent: WAGenericMediaMessage
|
||||
if(message.productMessage) {
|
||||
const product = message.productMessage.product?.productImage
|
||||
if(!product) {
|
||||
throw new Boom('product has no image', { statusCode: 400 })
|
||||
}
|
||||
|
||||
messageContent = product
|
||||
} else {
|
||||
messageContent = message[type]
|
||||
}
|
||||
|
||||
return downloadContentFromMessage(messageContent, type.replace('Message', '') as MediaType)
|
||||
}
|
||||
|
||||
export function extensionForMediaMessage(message: WAMessageContent) {
|
||||
const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1]
|
||||
const type = Object.keys(message)[0] as MessageType
|
||||
let extension: string
|
||||
if(
|
||||
const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1]
|
||||
const type = Object.keys(message)[0] as MessageType
|
||||
let extension: string
|
||||
if(
|
||||
type === 'locationMessage' ||
|
||||
type === 'liveLocationMessage' ||
|
||||
type === 'productMessage'
|
||||
) {
|
||||
extension = '.jpeg'
|
||||
} else {
|
||||
const messageContent = message[type] as
|
||||
extension = '.jpeg'
|
||||
} else {
|
||||
const messageContent = message[type] as
|
||||
| WAProto.VideoMessage
|
||||
| WAProto.ImageMessage
|
||||
| WAProto.AudioMessage
|
||||
| WAProto.DocumentMessage
|
||||
extension = getExtension (messageContent.mimetype)
|
||||
}
|
||||
return extension
|
||||
extension = getExtension (messageContent.mimetype)
|
||||
}
|
||||
|
||||
return extension
|
||||
}
|
||||
|
||||
export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger }: CommonSocketConfig<any>, refreshMediaConn: (force: boolean) => Promise<MediaConnInfo>): WAMediaUploadFunction => {
|
||||
return async(stream, { mediaType, fileEncSha256B64, timeoutMs }) => {
|
||||
return async(stream, { mediaType, fileEncSha256B64, timeoutMs }) => {
|
||||
const { default: axios } = await import('axios')
|
||||
// send a query JSON to obtain the url & auth token to upload our media
|
||||
// send a query JSON to obtain the url & auth token to upload our media
|
||||
let uploadInfo = await refreshMediaConn(false)
|
||||
|
||||
let urls: { mediaUrl: string, directPath: string }
|
||||
const hosts = [ ...customUploadHosts, ...uploadInfo.hosts ]
|
||||
const hosts = [ ...customUploadHosts, ...uploadInfo.hosts ]
|
||||
|
||||
let chunks: Buffer[] = []
|
||||
for await(const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
let reqBody = Buffer.concat(chunks)
|
||||
let reqBody = Buffer.concat(chunks)
|
||||
|
||||
for (let { hostname, maxContentLengthBytes } of hosts) {
|
||||
logger.debug(`uploading to "${hostname}"`)
|
||||
for(const { hostname, maxContentLengthBytes } of hosts) {
|
||||
logger.debug(`uploading to "${hostname}"`)
|
||||
|
||||
const auth = encodeURIComponent(uploadInfo.auth) // the auth token
|
||||
const url = `https://${hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
|
||||
let result: any
|
||||
try {
|
||||
if(maxContentLengthBytes && reqBody.length > maxContentLengthBytes) {
|
||||
throw new Boom(`Body too large for "${hostname}"`, { statusCode: 413 })
|
||||
}
|
||||
if(maxContentLengthBytes && reqBody.length > maxContentLengthBytes) {
|
||||
throw new Boom(`Body too large for "${hostname}"`, { statusCode: 413 })
|
||||
}
|
||||
|
||||
const body = await axios.post(
|
||||
url,
|
||||
reqBody,
|
||||
url,
|
||||
reqBody,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Origin': DEFAULT_ORIGIN
|
||||
},
|
||||
httpsAgent: fetchAgent,
|
||||
timeout: timeoutMs,
|
||||
responseType: 'json',
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity,
|
||||
timeout: timeoutMs,
|
||||
responseType: 'json',
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity,
|
||||
}
|
||||
)
|
||||
result = body.data
|
||||
result = body.data
|
||||
|
||||
if(result?.url || result?.directPath) {
|
||||
urls = {
|
||||
mediaUrl: result.url,
|
||||
directPath: result.direct_path
|
||||
}
|
||||
break
|
||||
} else {
|
||||
urls = {
|
||||
mediaUrl: result.url,
|
||||
directPath: result.direct_path
|
||||
}
|
||||
break
|
||||
} else {
|
||||
uploadInfo = await refreshMediaConn(true)
|
||||
throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
|
||||
}
|
||||
} catch (error) {
|
||||
if(axios.isAxiosError(error)) {
|
||||
result = error.response?.data
|
||||
}
|
||||
} catch(error) {
|
||||
if(axios.isAxiosError(error)) {
|
||||
result = error.response?.data
|
||||
}
|
||||
|
||||
const isLast = hostname === hosts[uploadInfo.hosts.length-1]?.hostname
|
||||
logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`)
|
||||
}
|
||||
}
|
||||
// clear buffer just to be sure we're releasing the memory
|
||||
reqBody = undefined
|
||||
|
||||
// clear buffer just to be sure we're releasing the memory
|
||||
reqBody = undefined
|
||||
|
||||
if(!urls) {
|
||||
throw new Boom(
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { promises as fs } from "fs"
|
||||
import { promises as fs } from 'fs'
|
||||
import { proto } from '../../WAProto'
|
||||
import { MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from "../Defaults"
|
||||
import { MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults'
|
||||
import {
|
||||
AnyMediaMessageContent,
|
||||
AnyMessageContent,
|
||||
MediaGenerationOptions,
|
||||
MediaType,
|
||||
MessageContentGenerationOptions,
|
||||
MessageGenerationOptions,
|
||||
MessageGenerationOptionsFromContent,
|
||||
@@ -13,13 +14,11 @@ import {
|
||||
WAMediaUpload,
|
||||
WAMessage,
|
||||
WAMessageContent,
|
||||
WAMessageStatus,
|
||||
WAProto,
|
||||
WATextMessage,
|
||||
MediaType,
|
||||
WAMessageStatus
|
||||
} from "../Types"
|
||||
import { generateMessageID, unixTimestampSeconds } from "./generics"
|
||||
import { encryptedStream, generateThumbnail, getAudioDuration } from "./messages-media"
|
||||
WATextMessage } from '../Types'
|
||||
import { generateMessageID, unixTimestampSeconds } from './generics'
|
||||
import { encryptedStream, generateThumbnail, getAudioDuration } from './messages-media'
|
||||
|
||||
type MediaUploadData = {
|
||||
media: WAMediaUpload
|
||||
@@ -33,20 +32,20 @@ type MediaUploadData = {
|
||||
}
|
||||
|
||||
const MIMETYPE_MAP: { [T in MediaType]: string } = {
|
||||
image: 'image/jpeg',
|
||||
video: 'video/mp4',
|
||||
document: 'application/pdf',
|
||||
audio: 'audio/ogg; codecs=opus',
|
||||
sticker: 'image/webp',
|
||||
image: 'image/jpeg',
|
||||
video: 'video/mp4',
|
||||
document: 'application/pdf',
|
||||
audio: 'audio/ogg; codecs=opus',
|
||||
sticker: 'image/webp',
|
||||
history: 'application/x-protobuf',
|
||||
"md-app-state": 'application/x-protobuf',
|
||||
'md-app-state': 'application/x-protobuf',
|
||||
}
|
||||
|
||||
const MessageTypeProto = {
|
||||
'image': WAProto.ImageMessage,
|
||||
'video': WAProto.VideoMessage,
|
||||
'audio': WAProto.AudioMessage,
|
||||
'sticker': WAProto.StickerMessage,
|
||||
'image': WAProto.ImageMessage,
|
||||
'video': WAProto.VideoMessage,
|
||||
'audio': WAProto.AudioMessage,
|
||||
'sticker': WAProto.StickerMessage,
|
||||
'document': WAProto.DocumentMessage,
|
||||
} as const
|
||||
|
||||
@@ -64,6 +63,7 @@ export const prepareWAMessageMedia = async(
|
||||
mediaType = key
|
||||
}
|
||||
}
|
||||
|
||||
const uploadData: MediaUploadData = {
|
||||
...message,
|
||||
media: message[mediaType]
|
||||
@@ -74,13 +74,14 @@ export const prepareWAMessageMedia = async(
|
||||
('url' in uploadData.media) &&
|
||||
!!uploadData.media.url &&
|
||||
!!options.mediaCache && (
|
||||
// generate the key
|
||||
mediaType + ':' + uploadData.media.url!.toString()
|
||||
)
|
||||
// generate the key
|
||||
mediaType + ':' + uploadData.media.url!.toString()
|
||||
)
|
||||
|
||||
if(mediaType === 'document' && !uploadData.fileName) {
|
||||
uploadData.fileName = 'file'
|
||||
}
|
||||
|
||||
if(!uploadData.mimetype) {
|
||||
uploadData.mimetype = MIMETYPE_MAP[mediaType]
|
||||
}
|
||||
@@ -89,7 +90,7 @@ export const prepareWAMessageMedia = async(
|
||||
if(cacheableKey) {
|
||||
const mediaBuff: Buffer = options.mediaCache!.get(cacheableKey)
|
||||
if(mediaBuff) {
|
||||
logger?.debug({ cacheableKey }, `got media cache hit`)
|
||||
logger?.debug({ cacheableKey }, 'got media cache hit')
|
||||
|
||||
const obj = WAProto.Message.decode(mediaBuff)
|
||||
const key = `${mediaType}Message`
|
||||
@@ -117,9 +118,9 @@ export const prepareWAMessageMedia = async(
|
||||
// url safe Base64 encode the SHA256 hash of the body
|
||||
const fileEncSha256B64 = encodeURIComponent(
|
||||
fileEncSha256.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/\=+$/, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/\=+$/, '')
|
||||
)
|
||||
|
||||
const [{ mediaUrl, directPath }] = await Promise.all([
|
||||
@@ -128,34 +129,35 @@ export const prepareWAMessageMedia = async(
|
||||
encWriteStream,
|
||||
{ fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs }
|
||||
)
|
||||
logger?.debug(`uploaded media`)
|
||||
logger?.debug('uploaded media')
|
||||
return result
|
||||
})(),
|
||||
(async() => {
|
||||
try {
|
||||
if(requiresThumbnailComputation) {
|
||||
uploadData.jpegThumbnail = await generateThumbnail(bodyPath, mediaType as any, options)
|
||||
logger?.debug(`generated thumbnail`)
|
||||
logger?.debug('generated thumbnail')
|
||||
}
|
||||
if (requiresDurationComputation) {
|
||||
|
||||
if(requiresDurationComputation) {
|
||||
uploadData.seconds = await getAudioDuration(bodyPath)
|
||||
logger?.debug(`computed audio duration`)
|
||||
logger?.debug('computed audio duration')
|
||||
}
|
||||
} catch (error) {
|
||||
} catch(error) {
|
||||
logger?.warn({ trace: error.stack }, 'failed to obtain extra info')
|
||||
}
|
||||
})(),
|
||||
])
|
||||
.finally(
|
||||
async() => {
|
||||
encWriteStream.destroy()
|
||||
// remove tmp files
|
||||
if(didSaveToTmpPath && bodyPath) {
|
||||
await fs.unlink(bodyPath)
|
||||
logger?.debug('removed tmp files')
|
||||
.finally(
|
||||
async() => {
|
||||
encWriteStream.destroy()
|
||||
// remove tmp files
|
||||
if(didSaveToTmpPath && bodyPath) {
|
||||
await fs.unlink(bodyPath)
|
||||
logger?.debug('removed tmp files')
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
delete uploadData.media
|
||||
|
||||
@@ -175,12 +177,13 @@ export const prepareWAMessageMedia = async(
|
||||
})
|
||||
|
||||
if(cacheableKey) {
|
||||
logger.debug({ cacheableKey }, `set cache`)
|
||||
logger.debug({ cacheableKey }, 'set cache')
|
||||
options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish())
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
export const prepareDisappearingMessageSettingContent = (ephemeralExpiration?: number) => {
|
||||
ephemeralExpiration = ephemeralExpiration || 0
|
||||
const content: WAMessageContent = {
|
||||
@@ -195,6 +198,7 @@ export const prepareDisappearingMessageSettingContent = (ephemeralExpiration?: n
|
||||
}
|
||||
return WAProto.Message.fromObject(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate forwarded message content like WA does
|
||||
* @param message the message to forward
|
||||
@@ -205,7 +209,10 @@ export const generateForwardMessageContent = (
|
||||
forceForward?: boolean
|
||||
) => {
|
||||
let content = message.message
|
||||
if (!content) throw new Boom('no content in message', { statusCode: 400 })
|
||||
if(!content) {
|
||||
throw new Boom('no content in message', { statusCode: 400 })
|
||||
}
|
||||
|
||||
// hacky copy
|
||||
content = proto.Message.decode(proto.Message.encode(message.message).finish())
|
||||
|
||||
@@ -213,17 +220,22 @@ export const generateForwardMessageContent = (
|
||||
|
||||
let score = content[key].contextInfo?.forwardingScore || 0
|
||||
score += message.key.fromMe && !forceForward ? 0 : 1
|
||||
if (key === 'conversation') {
|
||||
if(key === 'conversation') {
|
||||
content.extendedTextMessage = { text: content[key] }
|
||||
delete content.conversation
|
||||
|
||||
key = 'extendedTextMessage'
|
||||
}
|
||||
if (score > 0) content[key].contextInfo = { forwardingScore: score, isForwarded: true }
|
||||
else content[key].contextInfo = {}
|
||||
|
||||
if(score > 0) {
|
||||
content[key].contextInfo = { forwardingScore: score, isForwarded: true }
|
||||
} else {
|
||||
content[key].contextInfo = {}
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
export const generateWAMessageContent = async(
|
||||
message: AnyMessageContent,
|
||||
options: MessageContentGenerationOptions
|
||||
@@ -231,7 +243,7 @@ export const generateWAMessageContent = async(
|
||||
let m: WAMessageContent = {}
|
||||
if('text' in message) {
|
||||
const extContent = { ...message } as WATextMessage
|
||||
if (!!options.getUrlInfo && message.text.match(URL_REGEX)) {
|
||||
if(!!options.getUrlInfo && message.text.match(URL_REGEX)) {
|
||||
try {
|
||||
const data = await options.getUrlInfo(message.text)
|
||||
extContent.canonicalUrl = data['canonical-url']
|
||||
@@ -240,16 +252,18 @@ export const generateWAMessageContent = async(
|
||||
extContent.description = data.description
|
||||
extContent.title = data.title
|
||||
extContent.previewType = 0
|
||||
} catch (error) { // ignore if fails
|
||||
} catch(error) { // ignore if fails
|
||||
options.logger?.warn({ trace: error.stack }, 'url generation failed')
|
||||
}
|
||||
}
|
||||
|
||||
m.extendedTextMessage = extContent
|
||||
} else if('contacts' in message) {
|
||||
const contactLen = message.contacts.contacts.length
|
||||
if(!contactLen) {
|
||||
throw new Boom('require atleast 1 contact', { statusCode: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
if(contactLen === 1) {
|
||||
m.contactMessage = WAProto.ContactMessage.fromObject(message.contacts.contacts[0])
|
||||
} else {
|
||||
@@ -269,8 +283,8 @@ export const generateWAMessageContent = async(
|
||||
)
|
||||
} else if('disappearingMessagesInChat' in message) {
|
||||
const exp = typeof message.disappearingMessagesInChat === 'boolean' ?
|
||||
(message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) :
|
||||
message.disappearingMessagesInChat
|
||||
(message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) :
|
||||
message.disappearingMessagesInChat
|
||||
m = prepareDisappearingMessageSettingContent(exp)
|
||||
} else {
|
||||
m = await prepareWAMessageMedia(
|
||||
@@ -278,6 +292,7 @@ export const generateWAMessageContent = async(
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
if('buttons' in message && !!message.buttons) {
|
||||
const buttonsMessage: proto.IButtonsMessage = {
|
||||
buttons: message.buttons!.map(b => ({ ...b, type: proto.Button.ButtonType.RESPONSE }))
|
||||
@@ -289,13 +304,14 @@ export const generateWAMessageContent = async(
|
||||
if('caption' in message) {
|
||||
buttonsMessage.contentText = message.caption
|
||||
}
|
||||
|
||||
const type = Object.keys(m)[0].replace('Message', '').toUpperCase()
|
||||
buttonsMessage.headerType = ButtonType[type]
|
||||
|
||||
Object.assign(buttonsMessage, m)
|
||||
}
|
||||
|
||||
if ('footer' in message && !!message.footer) {
|
||||
if('footer' in message && !!message.footer) {
|
||||
buttonsMessage.footerText = message.footer
|
||||
}
|
||||
|
||||
@@ -325,7 +341,7 @@ export const generateWAMessageContent = async(
|
||||
m = { templateMessage }
|
||||
}
|
||||
|
||||
if ('sections' in message && !!message.sections) {
|
||||
if('sections' in message && !!message.sections) {
|
||||
const listMessage: proto.IListMessage = {
|
||||
sections: message.sections,
|
||||
buttonText: message.buttonText,
|
||||
@@ -341,19 +357,24 @@ export const generateWAMessageContent = async(
|
||||
if('viewOnce' in message && !!message.viewOnce) {
|
||||
m = { viewOnceMessage: { message: m } }
|
||||
}
|
||||
|
||||
if('mentions' in message && message.mentions?.length) {
|
||||
const [messageType] = Object.keys(m)
|
||||
m[messageType].contextInfo = m[messageType] || { }
|
||||
m[messageType].contextInfo.mentionedJid = message.mentions
|
||||
}
|
||||
|
||||
return WAProto.Message.fromObject(m)
|
||||
}
|
||||
|
||||
export const generateWAMessageFromContent = (
|
||||
jid: string,
|
||||
message: WAMessageContent,
|
||||
options: MessageGenerationOptionsFromContent
|
||||
) => {
|
||||
if(!options.timestamp) options.timestamp = new Date() // set timestamp to now
|
||||
if(!options.timestamp) {
|
||||
options.timestamp = new Date()
|
||||
} // set timestamp to now
|
||||
|
||||
const key = Object.keys(message)[0]
|
||||
const timestamp = unixTimestampSeconds(options.timestamp)
|
||||
@@ -373,6 +394,7 @@ export const generateWAMessageFromContent = (
|
||||
message[key].contextInfo.remoteJid = quoted.key.remoteJid
|
||||
}
|
||||
}
|
||||
|
||||
if(
|
||||
// if we want to send a disappearing message
|
||||
!!options?.ephemeralExpiration &&
|
||||
@@ -409,6 +431,7 @@ export const generateWAMessageFromContent = (
|
||||
}
|
||||
return WAProto.WebMessageInfo.fromObject(messageJSON)
|
||||
}
|
||||
|
||||
export const generateWAMessage = async(
|
||||
jid: string,
|
||||
content: AnyMessageContent,
|
||||
@@ -434,6 +457,7 @@ export const getContentType = (content: WAProto.IMessage | undefined) => {
|
||||
return key as keyof typeof content
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the true message content from a message
|
||||
* Eg. extracts the inner message from a disappearing message/view once message
|
||||
@@ -447,17 +471,18 @@ export const extractMessageContent = (content: WAMessageContent | undefined | nu
|
||||
if(content?.buttonsMessage) {
|
||||
const { buttonsMessage } = content
|
||||
if(buttonsMessage.imageMessage) {
|
||||
return { imageMessage: buttonsMessage.imageMessage }
|
||||
return { imageMessage: buttonsMessage.imageMessage }
|
||||
} else if(buttonsMessage.documentMessage) {
|
||||
return { documentMessage: buttonsMessage.documentMessage }
|
||||
return { documentMessage: buttonsMessage.documentMessage }
|
||||
} else if(buttonsMessage.videoMessage) {
|
||||
return { videoMessage: buttonsMessage.videoMessage }
|
||||
return { videoMessage: buttonsMessage.videoMessage }
|
||||
} else if(buttonsMessage.locationMessage) {
|
||||
return { locationMessage: buttonsMessage.locationMessage }
|
||||
return { locationMessage: buttonsMessage.locationMessage }
|
||||
} else {
|
||||
return { conversation: buttonsMessage.contentText }
|
||||
return { conversation: buttonsMessage.contentText }
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -465,6 +490,6 @@ export const extractMessageContent = (content: WAMessageContent | undefined | nu
|
||||
* Returns the device predicted by message ID
|
||||
*/
|
||||
export const getDevice = (id: string) => {
|
||||
const deviceType = id.length > 21 ? 'android' : id.substring(0, 2) == '3A' ? 'ios' : 'web'
|
||||
return deviceType
|
||||
const deviceType = id.length > 21 ? 'android' : id.substring(0, 2) === '3A' ? 'ios' : 'web'
|
||||
return deviceType
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { sha256, Curve, hkdf } from "./crypto";
|
||||
import { Binary } from "../WABinary";
|
||||
import { createCipheriv, createDecipheriv } from "crypto";
|
||||
import { NOISE_MODE, NOISE_WA_HEADER } from "../Defaults";
|
||||
import { KeyPair } from "../Types";
|
||||
import { BinaryNode, decodeBinaryNode } from "../WABinary";
|
||||
import { Boom } from "@hapi/boom";
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { createCipheriv, createDecipheriv } from 'crypto'
|
||||
import { proto } from '../../WAProto'
|
||||
import { NOISE_MODE, NOISE_WA_HEADER } from '../Defaults'
|
||||
import { KeyPair } from '../Types'
|
||||
import { Binary } from '../WABinary'
|
||||
import { BinaryNode, decodeBinaryNode } from '../WABinary'
|
||||
import { Curve, hkdf, sha256 } from './crypto'
|
||||
|
||||
const generateIV = (counter: number) => {
|
||||
const iv = new ArrayBuffer(12);
|
||||
new DataView(iv).setUint32(8, counter);
|
||||
const iv = new ArrayBuffer(12)
|
||||
new DataView(iv).setUint32(8, counter)
|
||||
|
||||
return new Uint8Array(iv)
|
||||
}
|
||||
@@ -18,9 +18,10 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
|
||||
|
||||
const authenticate = (data: Uint8Array) => {
|
||||
if(!isFinished) {
|
||||
hash = sha256( Buffer.from(Binary.build(hash, data).readByteArray()) )
|
||||
hash = sha256(Buffer.from(Binary.build(hash, data).readByteArray()))
|
||||
}
|
||||
}
|
||||
|
||||
const encrypt = (plaintext: Uint8Array) => {
|
||||
const authTagLength = 128 >> 3
|
||||
const cipher = createCipheriv('aes-256-gcm', encKey, generateIV(writeCounter), { authTagLength })
|
||||
@@ -33,6 +34,7 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
|
||||
authenticate(result)
|
||||
return result
|
||||
}
|
||||
|
||||
const decrypt = (ciphertext: Uint8Array) => {
|
||||
// before the handshake is finished, we use the same counter
|
||||
// after handshake, the counters are different
|
||||
@@ -48,16 +50,21 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
|
||||
|
||||
const result = Buffer.concat([cipher.update(enc), cipher.final()])
|
||||
|
||||
if(isFinished) readCounter += 1
|
||||
else writeCounter += 1
|
||||
if(isFinished) {
|
||||
readCounter += 1
|
||||
} else {
|
||||
writeCounter += 1
|
||||
}
|
||||
|
||||
authenticate(ciphertext)
|
||||
return result
|
||||
}
|
||||
|
||||
const localHKDF = (data: Uint8Array) => {
|
||||
const key = hkdf(Buffer.from(data), 64, { salt, info: '' })
|
||||
return [key.slice(0, 32), key.slice(32)]
|
||||
}
|
||||
|
||||
const mixIntoKey = (data: Uint8Array) => {
|
||||
const [write, read] = localHKDF(data)
|
||||
salt = write
|
||||
@@ -66,15 +73,16 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
|
||||
readCounter = 0
|
||||
writeCounter = 0
|
||||
}
|
||||
|
||||
const finishInit = () => {
|
||||
const [write, read] = localHKDF(new Uint8Array(0))
|
||||
encKey = write
|
||||
const [write, read] = localHKDF(new Uint8Array(0))
|
||||
encKey = write
|
||||
decKey = read
|
||||
hash = Buffer.from([])
|
||||
readCounter = 0
|
||||
writeCounter = 0
|
||||
isFinished = true
|
||||
}
|
||||
}
|
||||
|
||||
const data = Binary.build(NOISE_MODE).readBuffer()
|
||||
let hash = Buffer.from(data.byteLength === 32 ? data : sha256(Buffer.from(data)))
|
||||
@@ -123,11 +131,12 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
|
||||
if(isFinished) {
|
||||
data = encrypt(data)
|
||||
}
|
||||
|
||||
const introSize = sentIntro ? 0 : NOISE_WA_HEADER.length
|
||||
|
||||
outBinary.ensureAdditionalCapacity(introSize + 3 + data.byteLength)
|
||||
|
||||
if (!sentIntro) {
|
||||
if(!sentIntro) {
|
||||
outBinary.writeByteArray(NOISE_WA_HEADER)
|
||||
sentIntro = true
|
||||
}
|
||||
@@ -146,6 +155,7 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
|
||||
const getBytesSize = () => {
|
||||
return (inBinary.readUint8() << 16) | inBinary.readUint16()
|
||||
}
|
||||
|
||||
const peekSize = () => {
|
||||
return !(inBinary.size() < 3) && getBytesSize() <= inBinary.size()
|
||||
}
|
||||
@@ -159,8 +169,10 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
|
||||
const unpacked = new Binary(result).decompressed()
|
||||
frame = decodeBinaryNode(unpacked)
|
||||
}
|
||||
|
||||
onFrame(frame)
|
||||
}
|
||||
|
||||
inBinary.peek(peekSize)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as libsignal from 'libsignal'
|
||||
import { encodeBigEndian } from "./generics"
|
||||
import { Curve } from "./crypto"
|
||||
import { SenderKeyDistributionMessage, GroupSessionBuilder, SenderKeyRecord, SenderKeyName, GroupCipher } from '../../WASignalGroup'
|
||||
import { SignalIdentity, SignalKeyStore, SignedKeyPair, KeyPair, SignalAuthState, AuthenticationCreds } from "../Types/Auth"
|
||||
import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildUInt, jidDecode, JidWithDevice, getBinaryNodeChildren } from "../WABinary"
|
||||
import { proto } from "../../WAProto"
|
||||
import { proto } from '../../WAProto'
|
||||
import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage, SenderKeyName, SenderKeyRecord } from '../../WASignalGroup'
|
||||
import { AuthenticationCreds, KeyPair, SignalAuthState, SignalIdentity, SignalKeyStore, SignedKeyPair } from '../Types/Auth'
|
||||
import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, jidDecode, JidWithDevice } from '../WABinary'
|
||||
import { Curve } from './crypto'
|
||||
import { encodeBigEndian } from './generics'
|
||||
|
||||
export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) => {
|
||||
const newPub = Buffer.alloc(33)
|
||||
@@ -38,12 +38,13 @@ export const getPreKeys = async({ get }: SignalKeyStore, min: number, limit: num
|
||||
for(let id = min; id < limit;id++) {
|
||||
idList.push(id.toString())
|
||||
}
|
||||
|
||||
return get('pre-key', idList)
|
||||
}
|
||||
|
||||
export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number) => {
|
||||
const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId
|
||||
const remaining = range - avaliable
|
||||
const remaining = range - avaliable
|
||||
const lastPreKeyId = creds.nextPreKeyId + remaining - 1
|
||||
const newPreKeys: { [id: number]: KeyPair } = { }
|
||||
if(remaining > 0) {
|
||||
@@ -51,6 +52,7 @@ export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number)
|
||||
newPreKeys[i] = Curve.generateKeyPair()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
newPreKeys,
|
||||
lastPreKeyId,
|
||||
@@ -83,7 +85,7 @@ export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => (
|
||||
)
|
||||
|
||||
export const signalStorage = ({ creds, keys }: SignalAuthState) => ({
|
||||
loadSession: async (id: string) => {
|
||||
loadSession: async(id: string) => {
|
||||
const { [id]: sess } = await keys.get('session', [id])
|
||||
if(sess) {
|
||||
return libsignal.SessionRecord.deserialize(sess)
|
||||
@@ -115,7 +117,9 @@ export const signalStorage = ({ creds, keys }: SignalAuthState) => ({
|
||||
},
|
||||
loadSenderKey: async(keyId: string) => {
|
||||
const { [keyId]: key } = await keys.get('sender-key', [keyId])
|
||||
if(key) return new SenderKeyRecord(key)
|
||||
if(key) {
|
||||
return new SenderKeyRecord(key)
|
||||
}
|
||||
},
|
||||
storeSenderKey: async(keyId, key) => {
|
||||
await keys.set({ 'sender-key': { [keyId]: key.serialize() } })
|
||||
@@ -144,7 +148,7 @@ export const processSenderKeyMessage = async(
|
||||
item: proto.ISenderKeyDistributionMessage,
|
||||
auth: SignalAuthState
|
||||
) => {
|
||||
const builder = new GroupSessionBuilder(signalStorage(auth))
|
||||
const builder = new GroupSessionBuilder(signalStorage(auth))
|
||||
const senderName = jidToSignalSenderKeyName(item.groupId, authorJid)
|
||||
|
||||
const senderMsg = new SenderKeyDistributionMessage(null, null, null, null, item.axolotlSenderKeyDistributionMessage)
|
||||
@@ -153,6 +157,7 @@ export const processSenderKeyMessage = async(
|
||||
const record = new SenderKeyRecord()
|
||||
await auth.keys.set({ 'sender-key': { [senderName]: record } })
|
||||
}
|
||||
|
||||
await builder.process(senderName, senderMsg)
|
||||
}
|
||||
|
||||
@@ -160,14 +165,15 @@ export const decryptSignalProto = async(user: string, type: 'pkmsg' | 'msg', msg
|
||||
const addr = jidToSignalProtocolAddress(user)
|
||||
const session = new libsignal.SessionCipher(signalStorage(auth), addr)
|
||||
let result: Buffer
|
||||
switch(type) {
|
||||
case 'pkmsg':
|
||||
result = await session.decryptPreKeyWhisperMessage(msg)
|
||||
switch (type) {
|
||||
case 'pkmsg':
|
||||
result = await session.decryptPreKeyWhisperMessage(msg)
|
||||
break
|
||||
case 'msg':
|
||||
result = await session.decryptWhisperMessage(msg)
|
||||
case 'msg':
|
||||
result = await session.decryptWhisperMessage(msg)
|
||||
break
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -205,17 +211,18 @@ export const encryptSenderKeyMsgSignalProto = async(group: string, data: Uint8Ar
|
||||
export const parseAndInjectE2ESessions = async(node: BinaryNode, auth: SignalAuthState) => {
|
||||
const extractKey = (key: BinaryNode) => (
|
||||
key ? ({
|
||||
keyId: getBinaryNodeChildUInt(key, 'id', 3),
|
||||
publicKey: generateSignalPubKey(
|
||||
keyId: getBinaryNodeChildUInt(key, 'id', 3),
|
||||
publicKey: generateSignalPubKey(
|
||||
getBinaryNodeChildBuffer(key, 'value')
|
||||
),
|
||||
signature: getBinaryNodeChildBuffer(key, 'signature'),
|
||||
}) : undefined
|
||||
signature: getBinaryNodeChildBuffer(key, 'signature'),
|
||||
}) : undefined
|
||||
)
|
||||
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user')
|
||||
for(const node of nodes) {
|
||||
assertNodeErrorFree(node)
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
nodes.map(
|
||||
async node => {
|
||||
@@ -264,5 +271,6 @@ export const extractDeviceJids = (result: BinaryNode, myJid: string, excludeZero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extracted
|
||||
}
|
||||
@@ -1,152 +1,152 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { proto } from '../../WAProto'
|
||||
import type { SocketConfig, AuthenticationCreds, SignalCreds } from "../Types"
|
||||
import type { AuthenticationCreds, SignalCreds, SocketConfig } from '../Types'
|
||||
import { Binary, BinaryNode, getAllBinaryNodeChildren, jidDecode, S_WHATSAPP_NET } from '../WABinary'
|
||||
import { Curve, hmacSign } from './crypto'
|
||||
import { encodeInt } from './generics'
|
||||
import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary, getAllBinaryNodeChildren } from '../WABinary'
|
||||
import { createSignalIdentity } from './signal'
|
||||
|
||||
const ENCODED_VERSION = 'S9Kdc4pc4EJryo21snc5cg=='
|
||||
const getUserAgent = ({ version, browser }: Pick<SocketConfig, 'version' | 'browser'>) => ({
|
||||
appVersion: {
|
||||
primary: version[0],
|
||||
secondary: version[1],
|
||||
tertiary: version[2],
|
||||
},
|
||||
platform: 14,
|
||||
releaseChannel: 0,
|
||||
mcc: "000",
|
||||
mnc: "000",
|
||||
osVersion: browser[2],
|
||||
manufacturer: "",
|
||||
device: browser[1],
|
||||
osBuildNumber: "0.1",
|
||||
localeLanguageIso6391: 'en',
|
||||
localeCountryIso31661Alpha2: 'en',
|
||||
appVersion: {
|
||||
primary: version[0],
|
||||
secondary: version[1],
|
||||
tertiary: version[2],
|
||||
},
|
||||
platform: 14,
|
||||
releaseChannel: 0,
|
||||
mcc: '000',
|
||||
mnc: '000',
|
||||
osVersion: browser[2],
|
||||
manufacturer: '',
|
||||
device: browser[1],
|
||||
osBuildNumber: '0.1',
|
||||
localeLanguageIso6391: 'en',
|
||||
localeCountryIso31661Alpha2: 'en',
|
||||
})
|
||||
|
||||
export const generateLoginNode = (userJid: string, config: Pick<SocketConfig, 'version' | 'browser'>) => {
|
||||
const { user, device } = jidDecode(userJid)
|
||||
const payload = {
|
||||
passive: true,
|
||||
connectType: 1,
|
||||
connectReason: 1,
|
||||
userAgent: getUserAgent(config),
|
||||
webInfo: { webSubPlatform: 0 },
|
||||
username: parseInt(user, 10),
|
||||
device: device,
|
||||
}
|
||||
return proto.ClientPayload.encode(payload).finish()
|
||||
const { user, device } = jidDecode(userJid)
|
||||
const payload = {
|
||||
passive: true,
|
||||
connectType: 1,
|
||||
connectReason: 1,
|
||||
userAgent: getUserAgent(config),
|
||||
webInfo: { webSubPlatform: 0 },
|
||||
username: parseInt(user, 10),
|
||||
device: device,
|
||||
}
|
||||
return proto.ClientPayload.encode(payload).finish()
|
||||
}
|
||||
|
||||
export const generateRegistrationNode = (
|
||||
{ registrationId, signedPreKey, signedIdentityKey }: SignalCreds,
|
||||
config: Pick<SocketConfig, 'version' | 'browser'>
|
||||
{ registrationId, signedPreKey, signedIdentityKey }: SignalCreds,
|
||||
config: Pick<SocketConfig, 'version' | 'browser'>
|
||||
) => {
|
||||
const appVersionBuf = new Uint8Array(Buffer.from(ENCODED_VERSION, "base64"));
|
||||
const appVersionBuf = new Uint8Array(Buffer.from(ENCODED_VERSION, 'base64'))
|
||||
|
||||
const companion = {
|
||||
os: config.browser[0],
|
||||
version: {
|
||||
primary: 10,
|
||||
secondary: undefined,
|
||||
tertiary: undefined,
|
||||
},
|
||||
platformType: 1,
|
||||
requireFullSync: false,
|
||||
};
|
||||
const companion = {
|
||||
os: config.browser[0],
|
||||
version: {
|
||||
primary: 10,
|
||||
secondary: undefined,
|
||||
tertiary: undefined,
|
||||
},
|
||||
platformType: 1,
|
||||
requireFullSync: false,
|
||||
}
|
||||
|
||||
const companionProto = proto.CompanionProps.encode(companion).finish()
|
||||
const companionProto = proto.CompanionProps.encode(companion).finish()
|
||||
|
||||
const registerPayload = {
|
||||
connectReason: 1,
|
||||
connectType: 1,
|
||||
passive: false,
|
||||
regData: {
|
||||
buildHash: appVersionBuf,
|
||||
companionProps: companionProto,
|
||||
eRegid: encodeInt(4, registrationId),
|
||||
eKeytype: encodeInt(1, 5),
|
||||
eIdent: signedIdentityKey.public,
|
||||
eSkeyId: encodeInt(3, signedPreKey.keyId),
|
||||
eSkeyVal: signedPreKey.keyPair.public,
|
||||
eSkeySig: signedPreKey.signature,
|
||||
},
|
||||
userAgent: getUserAgent(config),
|
||||
webInfo: {
|
||||
webSubPlatform: 0,
|
||||
},
|
||||
}
|
||||
const registerPayload = {
|
||||
connectReason: 1,
|
||||
connectType: 1,
|
||||
passive: false,
|
||||
regData: {
|
||||
buildHash: appVersionBuf,
|
||||
companionProps: companionProto,
|
||||
eRegid: encodeInt(4, registrationId),
|
||||
eKeytype: encodeInt(1, 5),
|
||||
eIdent: signedIdentityKey.public,
|
||||
eSkeyId: encodeInt(3, signedPreKey.keyId),
|
||||
eSkeyVal: signedPreKey.keyPair.public,
|
||||
eSkeySig: signedPreKey.signature,
|
||||
},
|
||||
userAgent: getUserAgent(config),
|
||||
webInfo: {
|
||||
webSubPlatform: 0,
|
||||
},
|
||||
}
|
||||
|
||||
return proto.ClientPayload.encode(registerPayload).finish()
|
||||
return proto.ClientPayload.encode(registerPayload).finish()
|
||||
}
|
||||
|
||||
export const configureSuccessfulPairing = (
|
||||
stanza: BinaryNode,
|
||||
{ advSecretKey, signedIdentityKey, signalIdentities }: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'>
|
||||
stanza: BinaryNode,
|
||||
{ advSecretKey, signedIdentityKey, signalIdentities }: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'>
|
||||
) => {
|
||||
const [pair] = getAllBinaryNodeChildren(stanza)
|
||||
const pairContent = Array.isArray(pair.content) ? pair.content : []
|
||||
const [pair] = getAllBinaryNodeChildren(stanza)
|
||||
const pairContent = Array.isArray(pair.content) ? pair.content : []
|
||||
|
||||
const msgId = stanza.attrs.id
|
||||
const deviceIdentity = pairContent.find(m => m.tag === 'device-identity')?.content
|
||||
const businessName = pairContent.find(m => m.tag === 'biz')?.attrs?.name
|
||||
const verifiedName = businessName || ''
|
||||
const jid = pairContent.find(m => m.tag === 'device')?.attrs?.jid
|
||||
const msgId = stanza.attrs.id
|
||||
const deviceIdentity = pairContent.find(m => m.tag === 'device-identity')?.content
|
||||
const businessName = pairContent.find(m => m.tag === 'biz')?.attrs?.name
|
||||
const verifiedName = businessName || ''
|
||||
const jid = pairContent.find(m => m.tag === 'device')?.attrs?.jid
|
||||
|
||||
const { details, hmac } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentity as Buffer)
|
||||
const { details, hmac } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentity as Buffer)
|
||||
|
||||
const advSign = hmacSign(details, Buffer.from(advSecretKey, 'base64'))
|
||||
const advSign = hmacSign(details, Buffer.from(advSecretKey, 'base64'))
|
||||
|
||||
if (Buffer.compare(hmac, advSign) !== 0) {
|
||||
throw new Boom('Invalid pairing')
|
||||
}
|
||||
if(Buffer.compare(hmac, advSign) !== 0) {
|
||||
throw new Boom('Invalid pairing')
|
||||
}
|
||||
|
||||
const account = proto.ADVSignedDeviceIdentity.decode(details)
|
||||
const { accountSignatureKey, accountSignature } = account
|
||||
const account = proto.ADVSignedDeviceIdentity.decode(details)
|
||||
const { accountSignatureKey, accountSignature } = account
|
||||
|
||||
const accountMsg = Binary.build(new Uint8Array([6, 0]), account.details, signedIdentityKey.public).readByteArray()
|
||||
if (!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) {
|
||||
throw new Boom('Failed to verify account signature')
|
||||
}
|
||||
const accountMsg = Binary.build(new Uint8Array([6, 0]), account.details, signedIdentityKey.public).readByteArray()
|
||||
if(!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) {
|
||||
throw new Boom('Failed to verify account signature')
|
||||
}
|
||||
|
||||
const deviceMsg = Binary.build(new Uint8Array([6, 1]), account.details, signedIdentityKey.public, account.accountSignatureKey).readByteArray()
|
||||
account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg)
|
||||
const deviceMsg = Binary.build(new Uint8Array([6, 1]), account.details, signedIdentityKey.public, account.accountSignatureKey).readByteArray()
|
||||
account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg)
|
||||
|
||||
const identity = createSignalIdentity(jid, accountSignatureKey)
|
||||
const identity = createSignalIdentity(jid, accountSignatureKey)
|
||||
|
||||
const keyIndex = proto.ADVDeviceIdentity.decode(account.details).keyIndex
|
||||
const keyIndex = proto.ADVDeviceIdentity.decode(account.details).keyIndex
|
||||
|
||||
const accountEnc = proto.ADVSignedDeviceIdentity.encode({
|
||||
...account.toJSON(),
|
||||
accountSignatureKey: undefined
|
||||
}).finish()
|
||||
const accountEnc = proto.ADVSignedDeviceIdentity.encode({
|
||||
...account.toJSON(),
|
||||
accountSignatureKey: undefined
|
||||
}).finish()
|
||||
|
||||
const reply: BinaryNode = {
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'result',
|
||||
id: msgId,
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'pair-device-sign',
|
||||
attrs: { },
|
||||
content: [
|
||||
{ tag: 'device-identity', attrs: { 'key-index': `${keyIndex}` }, content: accountEnc }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
const reply: BinaryNode = {
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'result',
|
||||
id: msgId,
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'pair-device-sign',
|
||||
attrs: { },
|
||||
content: [
|
||||
{ tag: 'device-identity', attrs: { 'key-index': `${keyIndex}` }, content: accountEnc }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const authUpdate: Partial<AuthenticationCreds> = {
|
||||
account,
|
||||
me: { id: jid, verifiedName },
|
||||
signalIdentities: [...(signalIdentities || []), identity]
|
||||
}
|
||||
return {
|
||||
creds: authUpdate,
|
||||
reply
|
||||
}
|
||||
const authUpdate: Partial<AuthenticationCreds> = {
|
||||
account,
|
||||
me: { id: jid, verifiedName },
|
||||
signalIdentities: [...(signalIdentities || []), identity]
|
||||
}
|
||||
return {
|
||||
creds: authUpdate,
|
||||
reply
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user