Merge pull request #2637 from adiwajshing/libsignal-update

Signal Repository
This commit is contained in:
Adhiraj Singh
2023-03-19 11:29:27 +05:30
committed by GitHub
24 changed files with 541 additions and 217 deletions

View File

@@ -5,4 +5,5 @@ coverage
.eslintrc.json
src/WABinary/index.ts
WAProto
WASignalGroup
WASignalGroup
Example/test.ts

View File

@@ -41,7 +41,7 @@ class GroupCipher {
const senderKeyMessage = new SenderKeyMessage(null, null, null, null, senderKeyMessageBytes);
const senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId());
//senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic());
senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic());
const senderKey = this.getSenderKey(senderKeyState, senderKeyMessage.getIteration());
// senderKeyState.senderKeyStateStructure.senderSigningKey.private =

View File

@@ -64,7 +64,7 @@ class SenderKeyMessage extends CiphertextMessage {
}
verifySignature(signatureKey) {
const part1 = this.serialized.slice(0, this.serialized.length - this.SIGNATURE_LENGTH + 1);
const part1 = this.serialized.slice(0, this.serialized.length - this.SIGNATURE_LENGTH);
const part2 = this.serialized.slice(-1 * this.SIGNATURE_LENGTH);
const res = curve.verifySignature(signatureKey, part1, part2);
if (!res) throw new Error('Invalid signature!');

View File

@@ -8,4 +8,7 @@ module.exports = {
'transform': {
'^.+\\.(ts|tsx)$': 'ts-jest'
},
moduleNameMapper: {
'^axios$': require.resolve('axios'),
},
}

View File

@@ -1,5 +1,6 @@
import { proto } from '../../WAProto'
import type { MediaType, SocketConfig } from '../Types'
import { makeLibSignalRepository } from '../Signal/libsignal'
import type { AuthenticationState, MediaType, SocketConfig, WAVersion } from '../Types'
import { Browsers } from '../Utils'
import logger from '../Utils/logger'
import { version } from './baileys-version.json'
@@ -35,7 +36,7 @@ export const PROCESSABLE_HISTORY_TYPES = [
]
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
version: version as any,
version: version as WAVersion,
browser: Browsers.baileys('Chrome'),
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
connectTimeoutMs: 20_000,
@@ -47,7 +48,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
customUploadHosts: [],
retryRequestDelayMs: 250,
fireInitQueries: true,
auth: undefined as any,
auth: undefined as unknown as AuthenticationState,
markOnlineOnConnect: true,
syncFullHistory: false,
patchMessageBeforeSending: msg => msg,
@@ -61,7 +62,8 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
patch: false,
snapshot: false,
},
getMessage: async() => undefined
getMessage: async() => undefined,
makeSignalRepository: makeLibSignalRepository
}
export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {

141
src/Signal/libsignal.ts Normal file
View File

@@ -0,0 +1,141 @@
import * as libsignal from 'libsignal'
import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage, SenderKeyName, SenderKeyRecord } from '../../WASignalGroup'
import { SignalAuthState } from '../Types'
import { SignalRepository } from '../Types/Signal'
import { generateSignalPubKey } from '../Utils'
export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository {
const storage = signalStorage(auth)
return {
decryptGroupMessage({ group, authorJid, msg }) {
const senderName = jidToSignalSenderKeyName(group, authorJid)
const cipher = new GroupCipher(storage, senderName)
return cipher.decrypt(msg)
},
async processSenderKeyDistributionMessage({ item, authorJid }) {
const builder = new GroupSessionBuilder(storage)
const senderName = jidToSignalSenderKeyName(item.groupId!, authorJid)
const senderMsg = new SenderKeyDistributionMessage(null, null, null, null, item.axolotlSenderKeyDistributionMessage)
const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName])
if(!senderKey) {
await storage.storeSenderKey(senderName, new SenderKeyRecord())
}
await builder.process(senderName, senderMsg)
},
async decryptMessage({ jid, type, ciphertext }) {
const addr = jidToSignalProtocolAddress(jid)
const session = new libsignal.SessionCipher(storage, addr)
let result: Buffer
switch (type) {
case 'pkmsg':
result = await session.decryptPreKeyWhisperMessage(ciphertext)
break
case 'msg':
result = await session.decryptWhisperMessage(ciphertext)
break
}
return result
},
async encryptMessage({ jid, data }) {
const addr = jidToSignalProtocolAddress(jid)
const cipher = new libsignal.SessionCipher(storage, addr)
const { type: sigType, body } = await cipher.encrypt(data)
const type = sigType === 3 ? 'pkmsg' : 'msg'
return { type, ciphertext: Buffer.from(body, 'binary') }
},
async encryptGroupMessage({ group, meId, data }) {
const senderName = jidToSignalSenderKeyName(group, meId)
const builder = new GroupSessionBuilder(storage)
const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName])
if(!senderKey) {
await storage.storeSenderKey(senderName, new SenderKeyRecord())
}
const senderKeyDistributionMessage = await builder.create(senderName)
const session = new GroupCipher(storage, senderName)
const ciphertext = await session.encrypt(data)
return {
ciphertext,
senderKeyDistributionMessage: senderKeyDistributionMessage.serialize(),
}
},
async injectE2ESession({ jid, session }) {
const cipher = new libsignal.SessionBuilder(storage, jidToSignalProtocolAddress(jid))
await cipher.initOutgoing(session)
},
jidToSignalProtocolAddress(jid) {
return jidToSignalProtocolAddress(jid).toString()
},
}
}
const jidToSignalAddress = (jid: string) => jid.split('@')[0]
const jidToSignalProtocolAddress = (jid: string) => {
return new libsignal.ProtocolAddress(jidToSignalAddress(jid), 0)
}
const jidToSignalSenderKeyName = (group: string, user: string): string => {
return new SenderKeyName(group, jidToSignalProtocolAddress(user)).toString()
}
function signalStorage({ creds, keys }: SignalAuthState) {
return {
loadSession: async(id: string) => {
const { [id]: sess } = await keys.get('session', [id])
if(sess) {
return libsignal.SessionRecord.deserialize(sess)
}
},
storeSession: async(id, session) => {
await keys.set({ 'session': { [id]: session.serialize() } })
},
isTrustedIdentity: () => {
return true
},
loadPreKey: async(id: number | string) => {
const keyId = id.toString()
const { [keyId]: key } = await keys.get('pre-key', [keyId])
if(key) {
return {
privKey: Buffer.from(key.private),
pubKey: Buffer.from(key.public)
}
}
},
removePreKey: (id: number) => keys.set({ 'pre-key': { [id]: null } }),
loadSignedPreKey: () => {
const key = creds.signedPreKey
return {
privKey: Buffer.from(key.keyPair.private),
pubKey: Buffer.from(key.keyPair.public)
}
},
loadSenderKey: async(keyId: string) => {
const { [keyId]: key } = await keys.get('sender-key', [keyId])
if(key) {
return new SenderKeyRecord(key)
}
},
storeSenderKey: async(keyId, key) => {
await keys.set({ 'sender-key': { [keyId]: key.serialize() } })
},
getOurRegistrationId: () => (
creds.registrationId
),
getOurIdentity: () => {
const { signedIdentityKey } = creds
return {
privKey: Buffer.from(signedIdentityKey.private),
pubKey: generateSignalPubKey(signedIdentityKey.public),
}
}
}
}

View File

@@ -54,7 +54,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
tag: 'product_catalog',
attrs: {
jid,
allow_shop_source: 'true'
'allow_shop_source': 'true'
},
content: queryParamNodes
}
@@ -72,13 +72,13 @@ export const makeBusinessSocket = (config: SocketConfig) => {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'w:biz:catalog',
smax_id: '35'
'smax_id': '35'
},
content: [
{
tag: 'collections',
attrs: {
biz_jid: jid,
'biz_jid': jid,
},
content: [
{
@@ -116,7 +116,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'fb:thrift_iq',
smax_id: '5'
'smax_id': '5'
},
content: [
{

View File

@@ -1,5 +1,5 @@
import { proto } from '../../WAProto'
import { GroupMetadata, ParticipantAction, SocketConfig, WAMessageKey, WAMessageStubType } from '../Types'
import { GroupMetadata, GroupParticipant, ParticipantAction, SocketConfig, WAMessageKey, WAMessageStubType } from '../Types'
import { generateMessageID, unixTimestampSeconds } from '../Utils'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString, jidEncode, jidNormalizedUser } from '../WABinary'
import { makeChatsSocket } from './chats'
@@ -278,7 +278,7 @@ export const extractGroupMetadata = (result: BinaryNode) => {
({ attrs }) => {
return {
id: attrs.jid,
admin: attrs.type || null as any,
admin: (attrs.type || null) as GroupParticipant['admin'],
}
}
),

View File

@@ -22,8 +22,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
ev,
authState,
ws,
query,
processingMutex,
signalRepository,
query,
upsertMessage,
resyncAppState,
onUnexpectedError,
@@ -543,7 +544,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
const handleMessage = async(node: BinaryNode) => {
const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState)
const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(
node,
authState.creds.me!.id,
signalRepository,
logger,
)
if(shouldIgnoreJid(msg.key.remoteJid!)) {
logger.debug({ key: msg.key }, 'ignored message')
await sendMessageAck(node)
@@ -556,10 +562,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
await decrypt()
// message failed to decrypt
if(msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
logger.error(
{ key: msg.key, params: msg.messageStubParameters },
'failure in decrypting message'
)
retryMutex.mutex(
async() => {
if(ws.readyState === ws.OPEN) {

View File

@@ -4,7 +4,7 @@ import NodeCache from 'node-cache'
import { proto } from '../../WAProto'
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults'
import { AnyMessageContent, MediaConnInfo, MessageReceiptType, MessageRelayOptions, MiscMessageGenerationOptions, SocketConfig, WAMessageKey } from '../Types'
import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, encryptSenderKeyMsgSignalProto, encryptSignalProto, extractDeviceJids, generateMessageID, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, jidToSignalProtocolAddress, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils'
import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageID, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils'
import { getUrlInfo } from '../Utils/link-preview'
import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidEncode, jidNormalizedUser, JidWithDevice, S_WHATSAPP_NET } from '../WABinary'
import { makeGroupsSocket } from './groups'
@@ -22,6 +22,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
ev,
authState,
processingMutex,
signalRepository,
upsertMessage,
query,
fetchPrivacySettings,
@@ -215,10 +216,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
if(force) {
jidsRequiringFetch = jids
} else {
const addrs = jids.map(jid => jidToSignalProtocolAddress(jid).toString())
const addrs = jids.map(jid => (
signalRepository
.jidToSignalProtocolAddress(jid)
))
const sessions = await authState.keys.get('session', addrs)
for(const jid of jids) {
const signalId = jidToSignalProtocolAddress(jid).toString()
const signalId = signalRepository
.jidToSignalProtocolAddress(jid)
if(!sessions[signalId]) {
jidsRequiringFetch.push(jid)
}
@@ -247,7 +252,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
}
]
})
await parseAndInjectE2ESessions(result, authState)
await parseAndInjectE2ESessions(result, signalRepository)
didFetchNewSession = true
}
@@ -267,7 +272,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const nodes = await Promise.all(
jids.map(
async jid => {
const { type, ciphertext } = await encryptSignalProto(jid, bytes, authState)
const { type, ciphertext } = await signalRepository
.encryptMessage({ jid, data: bytes })
if(type === 'pkmsg') {
shouldIncludeDeviceIdentity = true
}
@@ -365,11 +371,12 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const patched = await patchMessageBeforeSending(message, devices.map(d => jidEncode(d.user, 's.whatsapp.net', d.device)))
const bytes = encodeWAMessage(patched)
const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(
destinationJid,
bytes,
meId,
authState
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage(
{
group: destinationJid,
data: bytes,
meId,
}
)
const senderKeyJids: string[] = []
@@ -390,7 +397,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const senderKeyMsg: proto.IMessage = {
senderKeyDistributionMessage: {
axolotlSenderKeyDistributionMessage: senderKeyDistributionMessageKey,
axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage,
groupId: destinationJid
}
}

View File

@@ -29,6 +29,7 @@ export const makeSocket = ({
transactionOpts,
qrTimeout,
options,
makeSignalRepository
}: SocketConfig) => {
const ws = new WebSocket(waWebSocketUrl, undefined, {
origin: DEFAULT_ORIGIN,
@@ -48,6 +49,7 @@ export const makeSocket = ({
const { creds } = authState
// add transaction capability
const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
const signalRepository = makeSignalRepository({ creds, keys })
let lastDateRecv: Date
let epoch = 1
@@ -90,24 +92,26 @@ export const makeSocket = ({
}
/** log & process any unexpected errors */
const onUnexpectedError = (error: Error, msg: string) => {
const onUnexpectedError = (err: Error | Boom, msg: string) => {
logger.error(
{ trace: error.stack, output: (error as any).output },
{ err },
`unexpected error in '${msg}'`
)
}
/** await the next incoming message */
const awaitNextMessage = async(sendMsg?: Uint8Array) => {
const awaitNextMessage = async<T>(sendMsg?: Uint8Array) => {
if(ws.readyState !== ws.OPEN) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
throw new Boom('Connection Closed', {
statusCode: DisconnectReason.connectionClosed
})
}
let onOpen: (data: any) => void
let onOpen: (data: T) => void
let onClose: (err: Error) => void
const result = promiseTimeout<any>(connectTimeoutMs, (resolve, reject) => {
onOpen = (data: any) => resolve(data)
const result = promiseTimeout<T>(connectTimeoutMs, (resolve, reject) => {
onOpen = resolve
onClose = mapWebSocketError(reject)
ws.on('frame', onOpen)
ws.on('close', onClose)
@@ -132,11 +136,11 @@ export const makeSocket = ({
* @param json query that was sent
* @param timeoutMs timeout after which the promise will reject
*/
const waitForMessage = async(msgId: string, timeoutMs = defaultQueryTimeoutMs) => {
const waitForMessage = async<T>(msgId: string, timeoutMs = defaultQueryTimeoutMs) => {
let onRecv: (json) => void
let onErr: (err) => void
try {
const result = await promiseTimeout(timeoutMs,
const result = await promiseTimeout<T>(timeoutMs,
(resolve, reject) => {
onRecv = resolve
onErr = err => {
@@ -148,7 +152,7 @@ export const makeSocket = ({
ws.off('error', onErr)
},
)
return result as any
return result
} finally {
ws.off(`TAG:${msgId}`, onRecv!)
ws.off('close', onErr!) // if the socket closes, you'll never receive the message
@@ -186,7 +190,7 @@ export const makeSocket = ({
const init = proto.HandshakeMessage.encode(helloMsg).finish()
const result = await awaitNextMessage(init)
const result = await awaitNextMessage<Uint8Array>(init)
const handshake = proto.HandshakeMessage.decode(result)
logger.trace({ handshake }, 'handshake recv from WA Web')
@@ -591,6 +595,7 @@ export const makeSocket = ({
ws,
ev,
authState: { creds, keys },
signalRepository,
get user() {
return authState.creds.me
},

186
src/Tests/test.libsignal.ts Normal file
View File

@@ -0,0 +1,186 @@
import { makeLibSignalRepository } from '../Signal/libsignal'
import { SignalAuthState, SignalDataTypeMap } from '../Types'
import { Curve, generateRegistrationId, generateSignalPubKey, signedKeyPair } from '../Utils'
describe('Signal Tests', () => {
it('should correctly encrypt/decrypt 1 message', async() => {
const user1 = makeUser()
const user2 = makeUser()
const msg = Buffer.from('hello there!')
await prepareForSendingMessage(user1, user2)
const result = await user1.repository.encryptMessage(
{ jid: user2.jid, data: msg }
)
const dec = await user2.repository.decryptMessage(
{ jid: user1.jid, ...result }
)
expect(dec).toEqual(msg)
})
it('should correctly override a session', async() => {
const user1 = makeUser()
const user2 = makeUser()
const msg = Buffer.from('hello there!')
for(let preKeyId = 2; preKeyId <= 3;preKeyId++) {
await prepareForSendingMessage(user1, user2, preKeyId)
const result = await user1.repository.encryptMessage(
{ jid: user2.jid, data: msg }
)
const dec = await user2.repository.decryptMessage(
{ jid: user1.jid, ...result }
)
expect(dec).toEqual(msg)
}
})
it('should correctly encrypt/decrypt multiple messages', async() => {
const user1 = makeUser()
const user2 = makeUser()
const msg = Buffer.from('hello there!')
await prepareForSendingMessage(user1, user2)
for(let i = 0;i < 10;i++) {
const result = await user1.repository.encryptMessage(
{ jid: user2.jid, data: msg }
)
const dec = await user2.repository.decryptMessage(
{ jid: user1.jid, ...result }
)
expect(dec).toEqual(msg)
}
})
it('should encrypt/decrypt messages from group', async() => {
const groupId = '123456@g.us'
const participants = [...Array(5)].map(makeUser)
const msg = Buffer.from('hello there!')
const sender = participants[0]
const enc = await sender.repository.encryptGroupMessage(
{
group: groupId,
meId: sender.jid,
data: msg
}
)
for(const participant of participants) {
if(participant === sender) {
continue
}
await participant.repository.processSenderKeyDistributionMessage(
{
item: {
groupId,
axolotlSenderKeyDistributionMessage: enc.senderKeyDistributionMessage
},
authorJid: sender.jid
}
)
const dec = await participant.repository.decryptGroupMessage(
{
group: groupId,
authorJid: sender.jid,
msg: enc.ciphertext
}
)
expect(dec).toEqual(msg)
}
})
})
type User = ReturnType<typeof makeUser>
function makeUser() {
const store = makeTestAuthState()
const jid = `${Math.random().toString().replace('.', '')}@s.whatsapp.net`
const repository = makeLibSignalRepository(store)
return { store, jid, repository }
}
async function prepareForSendingMessage(
sender: User,
receiver: User,
preKeyId = 2
) {
const preKey = Curve.generateKeyPair()
await sender.repository.injectE2ESession(
{
jid: receiver.jid,
session: {
registrationId: receiver.store.creds.registrationId,
identityKey: generateSignalPubKey(receiver.store.creds.signedIdentityKey.public),
signedPreKey: {
keyId: receiver.store.creds.signedPreKey.keyId,
publicKey: generateSignalPubKey(receiver.store.creds.signedPreKey.keyPair.public),
signature: receiver.store.creds.signedPreKey.signature,
},
preKey: {
keyId: preKeyId,
publicKey: generateSignalPubKey(preKey.public),
}
}
}
)
await receiver.store.keys.set({
'pre-key': {
[preKeyId]: preKey
}
})
}
function makeTestAuthState(): SignalAuthState {
const identityKey = Curve.generateKeyPair()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const store: { [_: string]: any } = {}
return {
creds: {
signedIdentityKey: identityKey,
registrationId: generateRegistrationId(),
signedPreKey: signedKeyPair(identityKey, 1),
},
keys: {
get(type, ids) {
const data: { [_: string]: SignalDataTypeMap[typeof type] } = { }
for(const id of ids) {
const item = store[getUniqueId(type, id)]
if(typeof item !== 'undefined') {
data[id] = item
}
}
return data
},
set(data) {
for(const type in data) {
for(const id in data[type]) {
store[getUniqueId(type, id)] = data[type][id]
}
}
},
}
}
function getUniqueId(type: string, id: string) {
return `${type}.${id}`
}
}

View File

@@ -3,7 +3,12 @@ import type { Contact } from './Contact'
import type { MinimalMessage } from './Message'
export type KeyPair = { public: Uint8Array, private: Uint8Array }
export type SignedKeyPair = { keyPair: KeyPair, signature: Uint8Array, keyId: number }
export type SignedKeyPair = {
keyPair: KeyPair
signature: Uint8Array
keyId: number
timestampS?: number
}
export type ProtocolAddress = {
name: string // jid
@@ -57,8 +62,8 @@ export type AuthenticationCreds = SignalCreds & {
export type SignalDataTypeMap = {
'pre-key': KeyPair
'session': any
'sender-key': any
'session': Uint8Array
'sender-key': Uint8Array
'sender-key-memory': { [jid: string]: boolean }
'app-state-sync-key': proto.Message.IAppStateSyncKeyData
'app-state-sync-version': LTHashState

View File

@@ -32,10 +32,10 @@ export interface GroupMetadata {
export interface WAGroupCreateResponse {
status: number
gid?: string
participants?: [{ [key: string]: any }]
participants?: [{ [key: string]: {} }]
}
export interface GroupModificationResponse {
status: number
participants?: { [key: string]: any }
participants?: { [key: string]: {} }
}

View File

@@ -1,5 +1,4 @@
import { AxiosRequestConfig } from 'axios'
import type NodeCache from 'node-cache'
import type { Logger } from 'pino'
import type { Readable } from 'stream'
import type { URL } from 'url'
@@ -19,9 +18,9 @@ export type WATextMessage = proto.Message.IExtendedTextMessage
export type WAContextInfo = proto.IContextInfo
export type WALocationMessage = proto.Message.ILocationMessage
export type WAGenericMediaMessage = proto.Message.IVideoMessage | proto.Message.IImageMessage | proto.Message.IAudioMessage | proto.Message.IDocumentMessage | proto.Message.IStickerMessage
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
export import WAMessageStubType = proto.WebMessageInfo.StubType
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
export import WAMessageStatus = proto.WebMessageInfo.Status
export type WAMediaUpload = Buffer | { url: URL | string } | { stream: Readable }
/** Set of message types that are supported by the library */

View File

@@ -3,12 +3,13 @@ import { WAMediaUpload } from './Message'
export type CatalogResult = {
data: {
paging: { cursors: { before: string, after: string } }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any[]
}
}
export type ProductCreateResult = {
data: { product: any }
data: { product: {} }
}
export type CatalogStatus = {

68
src/Types/Signal.ts Normal file
View File

@@ -0,0 +1,68 @@
import { proto } from '../../WAProto'
type DecryptGroupSignalOpts = {
group: string
authorJid: string
msg: Uint8Array
}
type ProcessSenderKeyDistributionMessageOpts = {
item: proto.Message.ISenderKeyDistributionMessage
authorJid: string
}
type DecryptSignalProtoOpts = {
jid: string
type: 'pkmsg' | 'msg'
ciphertext: Uint8Array
}
type EncryptMessageOpts = {
jid: string
data: Uint8Array
}
type EncryptGroupMessageOpts = {
group: string
data: Uint8Array
meId: string
}
type PreKey = {
keyId: number
publicKey: Uint8Array
}
type SignedPreKey = PreKey & {
signature: Uint8Array
}
type E2ESession = {
registrationId: number
identityKey: Uint8Array
signedPreKey: SignedPreKey
preKey: PreKey
}
type E2ESessionOpts = {
jid: string
session: E2ESession
}
export type SignalRepository = {
decryptGroupMessage(opts: DecryptGroupSignalOpts): Promise<Uint8Array>
processSenderKeyDistributionMessage(
opts: ProcessSenderKeyDistributionMessageOpts
): Promise<void>
decryptMessage(opts: DecryptSignalProtoOpts): Promise<Uint8Array>
encryptMessage(opts: EncryptMessageOpts): Promise<{
type: 'pkmsg' | 'msg'
ciphertext: Uint8Array
}>
encryptGroupMessage(opts: EncryptGroupMessageOpts): Promise<{
senderKeyDistributionMessage: Uint8Array
ciphertext: Uint8Array
}>
injectE2ESession(opts: E2ESessionOpts): Promise<void>
jidToSignalProtocolAddress(jid: string): string
}

View File

@@ -4,8 +4,9 @@ import type { Agent } from 'https'
import type { Logger } from 'pino'
import type { URL } from 'url'
import { proto } from '../../WAProto'
import { AuthenticationState, TransactionCapabilityOptions } from './Auth'
import { AuthenticationState, SignalAuthState, TransactionCapabilityOptions } from './Auth'
import { MediaConnInfo } from './Message'
import { SignalRepository } from './Signal'
export type WAVersion = [number, number, number]
export type WABrowserDescription = [string, string, string]
@@ -106,7 +107,10 @@ export type SocketConfig = {
options: AxiosRequestConfig<{}>
/**
* fetch a message from your store
* implement this so that messages failed to send (solves the "this message can take a while" issue) can be retried
* implement this so that messages failed to send
* (solves the "this message can take a while" issue) can be retried
* */
getMessage: (key: proto.IMessageKey) => Promise<proto.IMessage | undefined>
makeSignalRepository: (auth: SignalAuthState) => SignalRepository
}

View File

@@ -8,6 +8,7 @@ export * from './Socket'
export * from './Events'
export * from './Product'
export * from './Call'
export * from './Signal'
import { AuthenticationState } from './Auth'
import { SocketConfig } from './Socket'

View File

@@ -97,15 +97,21 @@ export const addTransactionCapability = (
* prefetches some data and stores in memory,
* useful if these data points will be used together often
* */
const prefetch = async(type: keyof SignalDataTypeMap, ids: string[]) => {
const prefetch = async<T extends keyof SignalDataTypeMap>(type: T, ids: string[]) => {
const dict = transactionCache[type]
const idsRequiringFetch = dict ? ids.filter(item => !(item in dict)) : ids
const idsRequiringFetch = dict
? ids.filter(item => typeof dict[item] !== 'undefined')
: ids
// only fetch if there are any items to fetch
if(idsRequiringFetch.length) {
dbQueriesInTransaction += 1
const result = await state.get(type, idsRequiringFetch)
transactionCache[type] = Object.assign(transactionCache[type] || { }, result)
transactionCache[type] ||= {}
transactionCache[type] = Object.assign(
transactionCache[type]!,
result
)
}
}

View File

@@ -148,7 +148,7 @@ export const toProductNode = (productId: string | undefined, product: ProductCre
if('originCountryCode' in product) {
if(typeof product.originCountryCode === 'undefined') {
attrs.compliance_category = 'COUNTRY_ORIGIN_EXEMPT'
attrs['compliance_category'] = 'COUNTRY_ORIGIN_EXEMPT'
} else {
content.push({
tag: 'compliance_info',
@@ -166,7 +166,7 @@ export const toProductNode = (productId: string | undefined, product: ProductCre
if(typeof product.isHidden !== 'undefined') {
attrs.is_hidden = product.isHidden.toString()
attrs['is_hidden'] = product.isHidden.toString()
}
const node: BinaryNode = {

View File

@@ -1,9 +1,9 @@
import { Boom } from '@hapi/boom'
import { Logger } from 'pino'
import { proto } from '../../WAProto'
import { AuthenticationState, WAMessageKey } from '../Types'
import { SignalRepository, WAMessageKey } from '../Types'
import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidStatusBroadcast, isJidUser } from '../WABinary'
import { unpadRandomMax16 } from './generics'
import { decryptGroupSignalProto, decryptSignalProto, processSenderKeyMessage } from './signal'
const NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node'
@@ -13,7 +13,10 @@ type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'di
* Decode the received node as a message.
* @note this will only parse the message, not decrypt it
*/
export function decodeMessageNode(stanza: BinaryNode, meId: string) {
export function decodeMessageNode(
stanza: BinaryNode,
meId: string
) {
let msgType: MessageType
let chatId: string
let author: string
@@ -92,8 +95,13 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string) {
}
}
export const decryptMessageNode = (stanza: BinaryNode, auth: AuthenticationState) => {
const { fullMessage, author, sender } = decodeMessageNode(stanza, auth.creds.me!.id)
export const decryptMessageNode = (
stanza: BinaryNode,
meId: string,
repository: SignalRepository,
logger: Logger
) => {
const { fullMessage, author, sender } = decodeMessageNode(stanza, meId)
return {
fullMessage,
category: stanza.attrs.category,
@@ -118,18 +126,26 @@ export const decryptMessageNode = (stanza: BinaryNode, auth: AuthenticationState
decryptables += 1
let msgBuffer: Buffer
let msgBuffer: Uint8Array
try {
const e2eType = attrs.type
switch (e2eType) {
case 'skmsg':
msgBuffer = await decryptGroupSignalProto(sender, author, content, auth)
msgBuffer = await repository.decryptGroupMessage({
group: sender,
authorJid: author,
msg: content
})
break
case 'pkmsg':
case 'msg':
const user = isJidUser(sender) ? sender : author
msgBuffer = await decryptSignalProto(user, e2eType, content as Buffer, auth)
msgBuffer = await repository.decryptMessage({
jid: user,
type: e2eType,
ciphertext: content
})
break
default:
throw new Error(`Unknown e2e type: ${e2eType}`)
@@ -138,7 +154,10 @@ export const decryptMessageNode = (stanza: BinaryNode, auth: AuthenticationState
let msg: proto.IMessage = proto.Message.decode(unpadRandomMax16(msgBuffer))
msg = msg.deviceSentMessage?.message || msg
if(msg.senderKeyDistributionMessage) {
await processSenderKeyMessage(author, msg.senderKeyDistributionMessage, auth)
await repository.processSenderKeyDistributionMessage({
authorJid: author,
item: msg.senderKeyDistributionMessage
})
}
if(fullMessage.message) {
@@ -146,9 +165,13 @@ export const decryptMessageNode = (stanza: BinaryNode, auth: AuthenticationState
} else {
fullMessage.message = msg
}
} catch(error) {
} catch(err) {
logger.error(
{ key: fullMessage.key, err },
'failed to decrypt message'
)
fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
fullMessage.messageStubParameters = [error.message]
fullMessage.messageStubParameters = [err.message]
}
}
}

View File

@@ -143,7 +143,7 @@ export const delayCancellable = (ms: number) => {
return { delay, cancel }
}
export async function promiseTimeout<T>(ms: number | undefined, promise: (resolve: (v?: T) => void, reject: (error) => void) => void) {
export async function promiseTimeout<T>(ms: number | undefined, promise: (resolve: (v: T) => void, reject: (error) => void) => void) {
if(!ms) {
return new Promise(promise)
}
@@ -177,7 +177,7 @@ export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEve
let listener: (item: BaileysEventMap[T]) => void
let closeListener: any
await (
promiseTimeout(
promiseTimeout<void>(
timeoutMs,
(resolve, reject) => {
closeListener = ({ connection, lastDisconnect }) => {

View File

@@ -1,22 +1,10 @@
import * as libsignal from 'libsignal'
import { proto } from '../../WAProto'
import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage, SenderKeyName, SenderKeyRecord } from '../../WASignalGroup'
import { KEY_BUNDLE_TYPE } from '../Defaults'
import { AuthenticationCreds, AuthenticationState, KeyPair, SignalAuthState, SignalIdentity, SignalKeyStore, SignedKeyPair } from '../Types/Auth'
import { SignalRepository } from '../Types'
import { AuthenticationCreds, AuthenticationState, KeyPair, SignalIdentity, SignalKeyStore, SignedKeyPair } from '../Types/Auth'
import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, jidDecode, JidWithDevice, S_WHATSAPP_NET } from '../WABinary'
import { Curve, generateSignalPubKey } from './crypto'
import { encodeBigEndian } from './generics'
const jidToSignalAddress = (jid: string) => jid.split('@')[0]
export const jidToSignalProtocolAddress = (jid: string) => {
return new libsignal.ProtocolAddress(jidToSignalAddress(jid), 0)
}
export const jidToSignalSenderKeyName = (group: string, user: string): string => {
return new SenderKeyName(group, jidToSignalProtocolAddress(user)).toString()
}
export const createSignalIdentity = (
wid: string,
accountSignatureKey: Uint8Array
@@ -77,134 +65,15 @@ export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => (
}
)
export const signalStorage = ({ creds, keys }: SignalAuthState) => ({
loadSession: async(id: string) => {
const { [id]: sess } = await keys.get('session', [id])
if(sess) {
return libsignal.SessionRecord.deserialize(sess)
}
},
storeSession: async(id, session) => {
await keys.set({ 'session': { [id]: session.serialize() } })
},
isTrustedIdentity: () => {
return true
},
loadPreKey: async(id: number | string) => {
const keyId = id.toString()
const { [keyId]: key } = await keys.get('pre-key', [keyId])
if(key) {
return {
privKey: Buffer.from(key.private),
pubKey: Buffer.from(key.public)
}
}
},
removePreKey: (id: number) => keys.set({ 'pre-key': { [id]: null } }),
loadSignedPreKey: () => {
const key = creds.signedPreKey
return {
privKey: Buffer.from(key.keyPair.private),
pubKey: Buffer.from(key.keyPair.public)
}
},
loadSenderKey: async(keyId: string) => {
const { [keyId]: key } = await keys.get('sender-key', [keyId])
if(key) {
return new SenderKeyRecord(key)
}
},
storeSenderKey: async(keyId, key) => {
await keys.set({ 'sender-key': { [keyId]: key.serialize() } })
},
getOurRegistrationId: () => (
creds.registrationId
),
getOurIdentity: () => {
const { signedIdentityKey } = creds
return {
privKey: Buffer.from(signedIdentityKey.private),
pubKey: generateSignalPubKey(signedIdentityKey.public),
}
}
})
export const decryptGroupSignalProto = (group: string, user: string, msg: Buffer | Uint8Array, auth: SignalAuthState) => {
const senderName = jidToSignalSenderKeyName(group, user)
const cipher = new GroupCipher(signalStorage(auth), senderName)
return cipher.decrypt(Buffer.from(msg))
}
export const processSenderKeyMessage = async(
authorJid: string,
item: proto.Message.ISenderKeyDistributionMessage,
auth: SignalAuthState
export const parseAndInjectE2ESessions = async(
node: BinaryNode,
repository: SignalRepository
) => {
const builder = new GroupSessionBuilder(signalStorage(auth))
const senderName = jidToSignalSenderKeyName(item.groupId!, authorJid)
const senderMsg = new SenderKeyDistributionMessage(null, null, null, null, item.axolotlSenderKeyDistributionMessage)
const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName])
if(!senderKey) {
const record = new SenderKeyRecord()
await auth.keys.set({ 'sender-key': { [senderName]: record } })
}
await builder.process(senderName, senderMsg)
}
export const decryptSignalProto = async(user: string, type: 'pkmsg' | 'msg', msg: Buffer | Uint8Array, auth: SignalAuthState) => {
const addr = jidToSignalProtocolAddress(user)
const session = new libsignal.SessionCipher(signalStorage(auth), addr)
let result: Buffer
switch (type) {
case 'pkmsg':
result = await session.decryptPreKeyWhisperMessage(msg)
break
case 'msg':
result = await session.decryptWhisperMessage(msg)
break
}
return result
}
export const encryptSignalProto = async(user: string, buffer: Buffer, auth: SignalAuthState) => {
const addr = jidToSignalProtocolAddress(user)
const cipher = new libsignal.SessionCipher(signalStorage(auth), addr)
const { type: sigType, body } = await cipher.encrypt(buffer)
const type = sigType === 3 ? 'pkmsg' : 'msg'
return { type, ciphertext: Buffer.from(body, 'binary') }
}
export const encryptSenderKeyMsgSignalProto = async(group: string, data: Uint8Array | Buffer, meId: string, auth: SignalAuthState) => {
const storage = signalStorage(auth)
const senderName = jidToSignalSenderKeyName(group, meId)
const builder = new GroupSessionBuilder(storage)
const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName])
if(!senderKey) {
const record = new SenderKeyRecord()
await auth.keys.set({ 'sender-key': { [senderName]: record } })
}
const senderKeyDistributionMessage = await builder.create(senderName)
const session = new GroupCipher(storage, senderName)
return {
ciphertext: await session.encrypt(data) as Uint8Array,
senderKeyDistributionMessageKey: senderKeyDistributionMessage.serialize() as Buffer,
}
}
export const parseAndInjectE2ESessions = async(node: BinaryNode, auth: SignalAuthState) => {
const extractKey = (key: BinaryNode) => (
key ? ({
keyId: getBinaryNodeChildUInt(key, 'id', 3),
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!),
signature: getBinaryNodeChildBuffer(key, 'signature'),
keyId: getBinaryNodeChildUInt(key, 'id', 3)!,
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!)!,
signature: getBinaryNodeChildBuffer(key, 'signature')!,
}) : undefined
)
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user')
@@ -221,14 +90,15 @@ export const parseAndInjectE2ESessions = async(node: BinaryNode, auth: SignalAut
const jid = node.attrs.jid
const registrationId = getBinaryNodeChildUInt(node, 'registration', 4)
const device = {
registrationId,
identityKey: generateSignalPubKey(identity),
signedPreKey: extractKey(signedKey),
preKey: extractKey(key)
}
const cipher = new libsignal.SessionBuilder(signalStorage(auth), jidToSignalProtocolAddress(jid))
await cipher.initOutgoing(device)
await repository.injectE2ESession({
jid,
session: {
registrationId: registrationId!,
identityKey: generateSignalPubKey(identity),
signedPreKey: extractKey(signedKey)!,
preKey: extractKey(key)!
}
})
}
)
)