mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Merge pull request #2637 from adiwajshing/libsignal-update
Signal Repository
This commit is contained in:
@@ -5,4 +5,5 @@ coverage
|
|||||||
.eslintrc.json
|
.eslintrc.json
|
||||||
src/WABinary/index.ts
|
src/WABinary/index.ts
|
||||||
WAProto
|
WAProto
|
||||||
WASignalGroup
|
WASignalGroup
|
||||||
|
Example/test.ts
|
||||||
@@ -41,7 +41,7 @@ class GroupCipher {
|
|||||||
const senderKeyMessage = new SenderKeyMessage(null, null, null, null, senderKeyMessageBytes);
|
const senderKeyMessage = new SenderKeyMessage(null, null, null, null, senderKeyMessageBytes);
|
||||||
|
|
||||||
const senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId());
|
const senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId());
|
||||||
//senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic());
|
senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic());
|
||||||
const senderKey = this.getSenderKey(senderKeyState, senderKeyMessage.getIteration());
|
const senderKey = this.getSenderKey(senderKeyState, senderKeyMessage.getIteration());
|
||||||
// senderKeyState.senderKeyStateStructure.senderSigningKey.private =
|
// senderKeyState.senderKeyStateStructure.senderSigningKey.private =
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class SenderKeyMessage extends CiphertextMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
verifySignature(signatureKey) {
|
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 part2 = this.serialized.slice(-1 * this.SIGNATURE_LENGTH);
|
||||||
const res = curve.verifySignature(signatureKey, part1, part2);
|
const res = curve.verifySignature(signatureKey, part1, part2);
|
||||||
if (!res) throw new Error('Invalid signature!');
|
if (!res) throw new Error('Invalid signature!');
|
||||||
|
|||||||
@@ -8,4 +8,7 @@ module.exports = {
|
|||||||
'transform': {
|
'transform': {
|
||||||
'^.+\\.(ts|tsx)$': 'ts-jest'
|
'^.+\\.(ts|tsx)$': 'ts-jest'
|
||||||
},
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^axios$': require.resolve('axios'),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { proto } from '../../WAProto'
|
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 { Browsers } from '../Utils'
|
||||||
import logger from '../Utils/logger'
|
import logger from '../Utils/logger'
|
||||||
import { version } from './baileys-version.json'
|
import { version } from './baileys-version.json'
|
||||||
@@ -35,7 +36,7 @@ export const PROCESSABLE_HISTORY_TYPES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
||||||
version: version as any,
|
version: version as WAVersion,
|
||||||
browser: Browsers.baileys('Chrome'),
|
browser: Browsers.baileys('Chrome'),
|
||||||
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
|
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
|
||||||
connectTimeoutMs: 20_000,
|
connectTimeoutMs: 20_000,
|
||||||
@@ -47,7 +48,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
|||||||
customUploadHosts: [],
|
customUploadHosts: [],
|
||||||
retryRequestDelayMs: 250,
|
retryRequestDelayMs: 250,
|
||||||
fireInitQueries: true,
|
fireInitQueries: true,
|
||||||
auth: undefined as any,
|
auth: undefined as unknown as AuthenticationState,
|
||||||
markOnlineOnConnect: true,
|
markOnlineOnConnect: true,
|
||||||
syncFullHistory: false,
|
syncFullHistory: false,
|
||||||
patchMessageBeforeSending: msg => msg,
|
patchMessageBeforeSending: msg => msg,
|
||||||
@@ -61,7 +62,8 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
|||||||
patch: false,
|
patch: false,
|
||||||
snapshot: false,
|
snapshot: false,
|
||||||
},
|
},
|
||||||
getMessage: async() => undefined
|
getMessage: async() => undefined,
|
||||||
|
makeSignalRepository: makeLibSignalRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
|
export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
|
||||||
|
|||||||
141
src/Signal/libsignal.ts
Normal file
141
src/Signal/libsignal.ts
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
tag: 'product_catalog',
|
tag: 'product_catalog',
|
||||||
attrs: {
|
attrs: {
|
||||||
jid,
|
jid,
|
||||||
allow_shop_source: 'true'
|
'allow_shop_source': 'true'
|
||||||
},
|
},
|
||||||
content: queryParamNodes
|
content: queryParamNodes
|
||||||
}
|
}
|
||||||
@@ -72,13 +72,13 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET,
|
||||||
type: 'get',
|
type: 'get',
|
||||||
xmlns: 'w:biz:catalog',
|
xmlns: 'w:biz:catalog',
|
||||||
smax_id: '35'
|
'smax_id': '35'
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'collections',
|
tag: 'collections',
|
||||||
attrs: {
|
attrs: {
|
||||||
biz_jid: jid,
|
'biz_jid': jid,
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -116,7 +116,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET,
|
||||||
type: 'get',
|
type: 'get',
|
||||||
xmlns: 'fb:thrift_iq',
|
xmlns: 'fb:thrift_iq',
|
||||||
smax_id: '5'
|
'smax_id': '5'
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { proto } from '../../WAProto'
|
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 { generateMessageID, unixTimestampSeconds } from '../Utils'
|
||||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString, jidEncode, jidNormalizedUser } from '../WABinary'
|
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString, jidEncode, jidNormalizedUser } from '../WABinary'
|
||||||
import { makeChatsSocket } from './chats'
|
import { makeChatsSocket } from './chats'
|
||||||
@@ -278,7 +278,7 @@ export const extractGroupMetadata = (result: BinaryNode) => {
|
|||||||
({ attrs }) => {
|
({ attrs }) => {
|
||||||
return {
|
return {
|
||||||
id: attrs.jid,
|
id: attrs.jid,
|
||||||
admin: attrs.type || null as any,
|
admin: (attrs.type || null) as GroupParticipant['admin'],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
ev,
|
ev,
|
||||||
authState,
|
authState,
|
||||||
ws,
|
ws,
|
||||||
query,
|
|
||||||
processingMutex,
|
processingMutex,
|
||||||
|
signalRepository,
|
||||||
|
query,
|
||||||
upsertMessage,
|
upsertMessage,
|
||||||
resyncAppState,
|
resyncAppState,
|
||||||
onUnexpectedError,
|
onUnexpectedError,
|
||||||
@@ -543,7 +544,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMessage = async(node: BinaryNode) => {
|
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!)) {
|
if(shouldIgnoreJid(msg.key.remoteJid!)) {
|
||||||
logger.debug({ key: msg.key }, 'ignored message')
|
logger.debug({ key: msg.key }, 'ignored message')
|
||||||
await sendMessageAck(node)
|
await sendMessageAck(node)
|
||||||
@@ -556,10 +562,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
await decrypt()
|
await decrypt()
|
||||||
// message failed to decrypt
|
// message failed to decrypt
|
||||||
if(msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
|
if(msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
|
||||||
logger.error(
|
|
||||||
{ key: msg.key, params: msg.messageStubParameters },
|
|
||||||
'failure in decrypting message'
|
|
||||||
)
|
|
||||||
retryMutex.mutex(
|
retryMutex.mutex(
|
||||||
async() => {
|
async() => {
|
||||||
if(ws.readyState === ws.OPEN) {
|
if(ws.readyState === ws.OPEN) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import NodeCache from 'node-cache'
|
|||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults'
|
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults'
|
||||||
import { AnyMessageContent, MediaConnInfo, MessageReceiptType, MessageRelayOptions, MiscMessageGenerationOptions, SocketConfig, WAMessageKey } from '../Types'
|
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 { getUrlInfo } from '../Utils/link-preview'
|
||||||
import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidEncode, jidNormalizedUser, JidWithDevice, S_WHATSAPP_NET } from '../WABinary'
|
import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidEncode, jidNormalizedUser, JidWithDevice, S_WHATSAPP_NET } from '../WABinary'
|
||||||
import { makeGroupsSocket } from './groups'
|
import { makeGroupsSocket } from './groups'
|
||||||
@@ -22,6 +22,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
ev,
|
ev,
|
||||||
authState,
|
authState,
|
||||||
processingMutex,
|
processingMutex,
|
||||||
|
signalRepository,
|
||||||
upsertMessage,
|
upsertMessage,
|
||||||
query,
|
query,
|
||||||
fetchPrivacySettings,
|
fetchPrivacySettings,
|
||||||
@@ -215,10 +216,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
if(force) {
|
if(force) {
|
||||||
jidsRequiringFetch = jids
|
jidsRequiringFetch = jids
|
||||||
} else {
|
} 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)
|
const sessions = await authState.keys.get('session', addrs)
|
||||||
for(const jid of jids) {
|
for(const jid of jids) {
|
||||||
const signalId = jidToSignalProtocolAddress(jid).toString()
|
const signalId = signalRepository
|
||||||
|
.jidToSignalProtocolAddress(jid)
|
||||||
if(!sessions[signalId]) {
|
if(!sessions[signalId]) {
|
||||||
jidsRequiringFetch.push(jid)
|
jidsRequiringFetch.push(jid)
|
||||||
}
|
}
|
||||||
@@ -247,7 +252,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
await parseAndInjectE2ESessions(result, authState)
|
await parseAndInjectE2ESessions(result, signalRepository)
|
||||||
|
|
||||||
didFetchNewSession = true
|
didFetchNewSession = true
|
||||||
}
|
}
|
||||||
@@ -267,7 +272,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
const nodes = await Promise.all(
|
const nodes = await Promise.all(
|
||||||
jids.map(
|
jids.map(
|
||||||
async jid => {
|
async jid => {
|
||||||
const { type, ciphertext } = await encryptSignalProto(jid, bytes, authState)
|
const { type, ciphertext } = await signalRepository
|
||||||
|
.encryptMessage({ jid, data: bytes })
|
||||||
if(type === 'pkmsg') {
|
if(type === 'pkmsg') {
|
||||||
shouldIncludeDeviceIdentity = true
|
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 patched = await patchMessageBeforeSending(message, devices.map(d => jidEncode(d.user, 's.whatsapp.net', d.device)))
|
||||||
const bytes = encodeWAMessage(patched)
|
const bytes = encodeWAMessage(patched)
|
||||||
|
|
||||||
const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(
|
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage(
|
||||||
destinationJid,
|
{
|
||||||
bytes,
|
group: destinationJid,
|
||||||
meId,
|
data: bytes,
|
||||||
authState
|
meId,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const senderKeyJids: string[] = []
|
const senderKeyJids: string[] = []
|
||||||
@@ -390,7 +397,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
const senderKeyMsg: proto.IMessage = {
|
const senderKeyMsg: proto.IMessage = {
|
||||||
senderKeyDistributionMessage: {
|
senderKeyDistributionMessage: {
|
||||||
axolotlSenderKeyDistributionMessage: senderKeyDistributionMessageKey,
|
axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage,
|
||||||
groupId: destinationJid
|
groupId: destinationJid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const makeSocket = ({
|
|||||||
transactionOpts,
|
transactionOpts,
|
||||||
qrTimeout,
|
qrTimeout,
|
||||||
options,
|
options,
|
||||||
|
makeSignalRepository
|
||||||
}: SocketConfig) => {
|
}: SocketConfig) => {
|
||||||
const ws = new WebSocket(waWebSocketUrl, undefined, {
|
const ws = new WebSocket(waWebSocketUrl, undefined, {
|
||||||
origin: DEFAULT_ORIGIN,
|
origin: DEFAULT_ORIGIN,
|
||||||
@@ -48,6 +49,7 @@ export const makeSocket = ({
|
|||||||
const { creds } = authState
|
const { creds } = authState
|
||||||
// add transaction capability
|
// add transaction capability
|
||||||
const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
|
const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
|
||||||
|
const signalRepository = makeSignalRepository({ creds, keys })
|
||||||
|
|
||||||
let lastDateRecv: Date
|
let lastDateRecv: Date
|
||||||
let epoch = 1
|
let epoch = 1
|
||||||
@@ -90,24 +92,26 @@ export const makeSocket = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** log & process any unexpected errors */
|
/** log & process any unexpected errors */
|
||||||
const onUnexpectedError = (error: Error, msg: string) => {
|
const onUnexpectedError = (err: Error | Boom, msg: string) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
{ trace: error.stack, output: (error as any).output },
|
{ err },
|
||||||
`unexpected error in '${msg}'`
|
`unexpected error in '${msg}'`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** await the next incoming message */
|
/** await the next incoming message */
|
||||||
const awaitNextMessage = async(sendMsg?: Uint8Array) => {
|
const awaitNextMessage = async<T>(sendMsg?: Uint8Array) => {
|
||||||
if(ws.readyState !== ws.OPEN) {
|
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
|
let onClose: (err: Error) => void
|
||||||
|
|
||||||
const result = promiseTimeout<any>(connectTimeoutMs, (resolve, reject) => {
|
const result = promiseTimeout<T>(connectTimeoutMs, (resolve, reject) => {
|
||||||
onOpen = (data: any) => resolve(data)
|
onOpen = resolve
|
||||||
onClose = mapWebSocketError(reject)
|
onClose = mapWebSocketError(reject)
|
||||||
ws.on('frame', onOpen)
|
ws.on('frame', onOpen)
|
||||||
ws.on('close', onClose)
|
ws.on('close', onClose)
|
||||||
@@ -132,11 +136,11 @@ export const makeSocket = ({
|
|||||||
* @param json query that was sent
|
* @param json query that was sent
|
||||||
* @param timeoutMs timeout after which the promise will reject
|
* @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 onRecv: (json) => void
|
||||||
let onErr: (err) => void
|
let onErr: (err) => void
|
||||||
try {
|
try {
|
||||||
const result = await promiseTimeout(timeoutMs,
|
const result = await promiseTimeout<T>(timeoutMs,
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
onRecv = resolve
|
onRecv = resolve
|
||||||
onErr = err => {
|
onErr = err => {
|
||||||
@@ -148,7 +152,7 @@ export const makeSocket = ({
|
|||||||
ws.off('error', onErr)
|
ws.off('error', onErr)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return result as any
|
return result
|
||||||
} finally {
|
} finally {
|
||||||
ws.off(`TAG:${msgId}`, onRecv!)
|
ws.off(`TAG:${msgId}`, onRecv!)
|
||||||
ws.off('close', onErr!) // if the socket closes, you'll never receive the message
|
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 init = proto.HandshakeMessage.encode(helloMsg).finish()
|
||||||
|
|
||||||
const result = await awaitNextMessage(init)
|
const result = await awaitNextMessage<Uint8Array>(init)
|
||||||
const handshake = proto.HandshakeMessage.decode(result)
|
const handshake = proto.HandshakeMessage.decode(result)
|
||||||
|
|
||||||
logger.trace({ handshake }, 'handshake recv from WA Web')
|
logger.trace({ handshake }, 'handshake recv from WA Web')
|
||||||
@@ -591,6 +595,7 @@ export const makeSocket = ({
|
|||||||
ws,
|
ws,
|
||||||
ev,
|
ev,
|
||||||
authState: { creds, keys },
|
authState: { creds, keys },
|
||||||
|
signalRepository,
|
||||||
get user() {
|
get user() {
|
||||||
return authState.creds.me
|
return authState.creds.me
|
||||||
},
|
},
|
||||||
|
|||||||
186
src/Tests/test.libsignal.ts
Normal file
186
src/Tests/test.libsignal.ts
Normal 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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,12 @@ import type { Contact } from './Contact'
|
|||||||
import type { MinimalMessage } from './Message'
|
import type { MinimalMessage } from './Message'
|
||||||
|
|
||||||
export type KeyPair = { public: Uint8Array, private: Uint8Array }
|
export type KeyPair = { public: Uint8Array, private: Uint8Array }
|
||||||
export type SignedKeyPair = { keyPair: KeyPair, signature: Uint8Array, keyId: number }
|
export type SignedKeyPair = {
|
||||||
|
keyPair: KeyPair
|
||||||
|
signature: Uint8Array
|
||||||
|
keyId: number
|
||||||
|
timestampS?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type ProtocolAddress = {
|
export type ProtocolAddress = {
|
||||||
name: string // jid
|
name: string // jid
|
||||||
@@ -57,8 +62,8 @@ export type AuthenticationCreds = SignalCreds & {
|
|||||||
|
|
||||||
export type SignalDataTypeMap = {
|
export type SignalDataTypeMap = {
|
||||||
'pre-key': KeyPair
|
'pre-key': KeyPair
|
||||||
'session': any
|
'session': Uint8Array
|
||||||
'sender-key': any
|
'sender-key': Uint8Array
|
||||||
'sender-key-memory': { [jid: string]: boolean }
|
'sender-key-memory': { [jid: string]: boolean }
|
||||||
'app-state-sync-key': proto.Message.IAppStateSyncKeyData
|
'app-state-sync-key': proto.Message.IAppStateSyncKeyData
|
||||||
'app-state-sync-version': LTHashState
|
'app-state-sync-version': LTHashState
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ export interface GroupMetadata {
|
|||||||
export interface WAGroupCreateResponse {
|
export interface WAGroupCreateResponse {
|
||||||
status: number
|
status: number
|
||||||
gid?: string
|
gid?: string
|
||||||
participants?: [{ [key: string]: any }]
|
participants?: [{ [key: string]: {} }]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupModificationResponse {
|
export interface GroupModificationResponse {
|
||||||
status: number
|
status: number
|
||||||
participants?: { [key: string]: any }
|
participants?: { [key: string]: {} }
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AxiosRequestConfig } from 'axios'
|
import { AxiosRequestConfig } from 'axios'
|
||||||
import type NodeCache from 'node-cache'
|
|
||||||
import type { Logger } from 'pino'
|
import type { Logger } from 'pino'
|
||||||
import type { Readable } from 'stream'
|
import type { Readable } from 'stream'
|
||||||
import type { URL } from 'url'
|
import type { URL } from 'url'
|
||||||
@@ -19,9 +18,9 @@ export type WATextMessage = proto.Message.IExtendedTextMessage
|
|||||||
export type WAContextInfo = proto.IContextInfo
|
export type WAContextInfo = proto.IContextInfo
|
||||||
export type WALocationMessage = proto.Message.ILocationMessage
|
export type WALocationMessage = proto.Message.ILocationMessage
|
||||||
export type WAGenericMediaMessage = proto.Message.IVideoMessage | proto.Message.IImageMessage | proto.Message.IAudioMessage | proto.Message.IDocumentMessage | proto.Message.IStickerMessage
|
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
|
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 import WAMessageStatus = proto.WebMessageInfo.Status
|
||||||
export type WAMediaUpload = Buffer | { url: URL | string } | { stream: Readable }
|
export type WAMediaUpload = Buffer | { url: URL | string } | { stream: Readable }
|
||||||
/** Set of message types that are supported by the library */
|
/** Set of message types that are supported by the library */
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { WAMediaUpload } from './Message'
|
|||||||
export type CatalogResult = {
|
export type CatalogResult = {
|
||||||
data: {
|
data: {
|
||||||
paging: { cursors: { before: string, after: string } }
|
paging: { cursors: { before: string, after: string } }
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
data: any[]
|
data: any[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductCreateResult = {
|
export type ProductCreateResult = {
|
||||||
data: { product: any }
|
data: { product: {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CatalogStatus = {
|
export type CatalogStatus = {
|
||||||
|
|||||||
68
src/Types/Signal.ts
Normal file
68
src/Types/Signal.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -4,8 +4,9 @@ import type { Agent } from 'https'
|
|||||||
import type { Logger } from 'pino'
|
import type { Logger } from 'pino'
|
||||||
import type { URL } from 'url'
|
import type { URL } from 'url'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { AuthenticationState, TransactionCapabilityOptions } from './Auth'
|
import { AuthenticationState, SignalAuthState, TransactionCapabilityOptions } from './Auth'
|
||||||
import { MediaConnInfo } from './Message'
|
import { MediaConnInfo } from './Message'
|
||||||
|
import { SignalRepository } from './Signal'
|
||||||
|
|
||||||
export type WAVersion = [number, number, number]
|
export type WAVersion = [number, number, number]
|
||||||
export type WABrowserDescription = [string, string, string]
|
export type WABrowserDescription = [string, string, string]
|
||||||
@@ -106,7 +107,10 @@ export type SocketConfig = {
|
|||||||
options: AxiosRequestConfig<{}>
|
options: AxiosRequestConfig<{}>
|
||||||
/**
|
/**
|
||||||
* fetch a message from your store
|
* 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>
|
getMessage: (key: proto.IMessageKey) => Promise<proto.IMessage | undefined>
|
||||||
|
|
||||||
|
makeSignalRepository: (auth: SignalAuthState) => SignalRepository
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export * from './Socket'
|
|||||||
export * from './Events'
|
export * from './Events'
|
||||||
export * from './Product'
|
export * from './Product'
|
||||||
export * from './Call'
|
export * from './Call'
|
||||||
|
export * from './Signal'
|
||||||
|
|
||||||
import { AuthenticationState } from './Auth'
|
import { AuthenticationState } from './Auth'
|
||||||
import { SocketConfig } from './Socket'
|
import { SocketConfig } from './Socket'
|
||||||
|
|||||||
@@ -97,15 +97,21 @@ export const addTransactionCapability = (
|
|||||||
* prefetches some data and stores in memory,
|
* prefetches some data and stores in memory,
|
||||||
* useful if these data points will be used together often
|
* 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 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
|
// only fetch if there are any items to fetch
|
||||||
if(idsRequiringFetch.length) {
|
if(idsRequiringFetch.length) {
|
||||||
dbQueriesInTransaction += 1
|
dbQueriesInTransaction += 1
|
||||||
const result = await state.get(type, idsRequiringFetch)
|
const result = await state.get(type, idsRequiringFetch)
|
||||||
|
|
||||||
transactionCache[type] = Object.assign(transactionCache[type] || { }, result)
|
transactionCache[type] ||= {}
|
||||||
|
transactionCache[type] = Object.assign(
|
||||||
|
transactionCache[type]!,
|
||||||
|
result
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export const toProductNode = (productId: string | undefined, product: ProductCre
|
|||||||
|
|
||||||
if('originCountryCode' in product) {
|
if('originCountryCode' in product) {
|
||||||
if(typeof product.originCountryCode === 'undefined') {
|
if(typeof product.originCountryCode === 'undefined') {
|
||||||
attrs.compliance_category = 'COUNTRY_ORIGIN_EXEMPT'
|
attrs['compliance_category'] = 'COUNTRY_ORIGIN_EXEMPT'
|
||||||
} else {
|
} else {
|
||||||
content.push({
|
content.push({
|
||||||
tag: 'compliance_info',
|
tag: 'compliance_info',
|
||||||
@@ -166,7 +166,7 @@ export const toProductNode = (productId: string | undefined, product: ProductCre
|
|||||||
|
|
||||||
|
|
||||||
if(typeof product.isHidden !== 'undefined') {
|
if(typeof product.isHidden !== 'undefined') {
|
||||||
attrs.is_hidden = product.isHidden.toString()
|
attrs['is_hidden'] = product.isHidden.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const node: BinaryNode = {
|
const node: BinaryNode = {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
|
import { Logger } from 'pino'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { AuthenticationState, WAMessageKey } from '../Types'
|
import { SignalRepository, WAMessageKey } from '../Types'
|
||||||
import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidStatusBroadcast, isJidUser } from '../WABinary'
|
import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidStatusBroadcast, isJidUser } from '../WABinary'
|
||||||
import { unpadRandomMax16 } from './generics'
|
import { unpadRandomMax16 } from './generics'
|
||||||
import { decryptGroupSignalProto, decryptSignalProto, processSenderKeyMessage } from './signal'
|
|
||||||
|
|
||||||
const NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node'
|
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.
|
* Decode the received node as a message.
|
||||||
* @note this will only parse the message, not decrypt it
|
* @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 msgType: MessageType
|
||||||
let chatId: string
|
let chatId: string
|
||||||
let author: string
|
let author: string
|
||||||
@@ -92,8 +95,13 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decryptMessageNode = (stanza: BinaryNode, auth: AuthenticationState) => {
|
export const decryptMessageNode = (
|
||||||
const { fullMessage, author, sender } = decodeMessageNode(stanza, auth.creds.me!.id)
|
stanza: BinaryNode,
|
||||||
|
meId: string,
|
||||||
|
repository: SignalRepository,
|
||||||
|
logger: Logger
|
||||||
|
) => {
|
||||||
|
const { fullMessage, author, sender } = decodeMessageNode(stanza, meId)
|
||||||
return {
|
return {
|
||||||
fullMessage,
|
fullMessage,
|
||||||
category: stanza.attrs.category,
|
category: stanza.attrs.category,
|
||||||
@@ -118,18 +126,26 @@ export const decryptMessageNode = (stanza: BinaryNode, auth: AuthenticationState
|
|||||||
|
|
||||||
decryptables += 1
|
decryptables += 1
|
||||||
|
|
||||||
let msgBuffer: Buffer
|
let msgBuffer: Uint8Array
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const e2eType = attrs.type
|
const e2eType = attrs.type
|
||||||
switch (e2eType) {
|
switch (e2eType) {
|
||||||
case 'skmsg':
|
case 'skmsg':
|
||||||
msgBuffer = await decryptGroupSignalProto(sender, author, content, auth)
|
msgBuffer = await repository.decryptGroupMessage({
|
||||||
|
group: sender,
|
||||||
|
authorJid: author,
|
||||||
|
msg: content
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case 'pkmsg':
|
case 'pkmsg':
|
||||||
case 'msg':
|
case 'msg':
|
||||||
const user = isJidUser(sender) ? sender : author
|
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
|
break
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown e2e type: ${e2eType}`)
|
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))
|
let msg: proto.IMessage = proto.Message.decode(unpadRandomMax16(msgBuffer))
|
||||||
msg = msg.deviceSentMessage?.message || msg
|
msg = msg.deviceSentMessage?.message || msg
|
||||||
if(msg.senderKeyDistributionMessage) {
|
if(msg.senderKeyDistributionMessage) {
|
||||||
await processSenderKeyMessage(author, msg.senderKeyDistributionMessage, auth)
|
await repository.processSenderKeyDistributionMessage({
|
||||||
|
authorJid: author,
|
||||||
|
item: msg.senderKeyDistributionMessage
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if(fullMessage.message) {
|
if(fullMessage.message) {
|
||||||
@@ -146,9 +165,13 @@ export const decryptMessageNode = (stanza: BinaryNode, auth: AuthenticationState
|
|||||||
} else {
|
} else {
|
||||||
fullMessage.message = msg
|
fullMessage.message = msg
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(err) {
|
||||||
|
logger.error(
|
||||||
|
{ key: fullMessage.key, err },
|
||||||
|
'failed to decrypt message'
|
||||||
|
)
|
||||||
fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
|
fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
|
||||||
fullMessage.messageStubParameters = [error.message]
|
fullMessage.messageStubParameters = [err.message]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export const delayCancellable = (ms: number) => {
|
|||||||
return { delay, cancel }
|
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) {
|
if(!ms) {
|
||||||
return new Promise(promise)
|
return new Promise(promise)
|
||||||
}
|
}
|
||||||
@@ -177,7 +177,7 @@ export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEve
|
|||||||
let listener: (item: BaileysEventMap[T]) => void
|
let listener: (item: BaileysEventMap[T]) => void
|
||||||
let closeListener: any
|
let closeListener: any
|
||||||
await (
|
await (
|
||||||
promiseTimeout(
|
promiseTimeout<void>(
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
closeListener = ({ connection, lastDisconnect }) => {
|
closeListener = ({ connection, lastDisconnect }) => {
|
||||||
|
|||||||
@@ -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 { 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 { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, jidDecode, JidWithDevice, S_WHATSAPP_NET } from '../WABinary'
|
||||||
import { Curve, generateSignalPubKey } from './crypto'
|
import { Curve, generateSignalPubKey } from './crypto'
|
||||||
import { encodeBigEndian } from './generics'
|
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 = (
|
export const createSignalIdentity = (
|
||||||
wid: string,
|
wid: string,
|
||||||
accountSignatureKey: Uint8Array
|
accountSignatureKey: Uint8Array
|
||||||
@@ -77,134 +65,15 @@ export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const signalStorage = ({ creds, keys }: SignalAuthState) => ({
|
export const parseAndInjectE2ESessions = async(
|
||||||
loadSession: async(id: string) => {
|
node: BinaryNode,
|
||||||
const { [id]: sess } = await keys.get('session', [id])
|
repository: SignalRepository
|
||||||
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
|
|
||||||
) => {
|
) => {
|
||||||
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) => (
|
const extractKey = (key: BinaryNode) => (
|
||||||
key ? ({
|
key ? ({
|
||||||
keyId: getBinaryNodeChildUInt(key, 'id', 3),
|
keyId: getBinaryNodeChildUInt(key, 'id', 3)!,
|
||||||
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!),
|
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!)!,
|
||||||
signature: getBinaryNodeChildBuffer(key, 'signature'),
|
signature: getBinaryNodeChildBuffer(key, 'signature')!,
|
||||||
}) : undefined
|
}) : undefined
|
||||||
)
|
)
|
||||||
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user')
|
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 jid = node.attrs.jid
|
||||||
const registrationId = getBinaryNodeChildUInt(node, 'registration', 4)
|
const registrationId = getBinaryNodeChildUInt(node, 'registration', 4)
|
||||||
|
|
||||||
const device = {
|
await repository.injectE2ESession({
|
||||||
registrationId,
|
jid,
|
||||||
identityKey: generateSignalPubKey(identity),
|
session: {
|
||||||
signedPreKey: extractKey(signedKey),
|
registrationId: registrationId!,
|
||||||
preKey: extractKey(key)
|
identityKey: generateSignalPubKey(identity),
|
||||||
}
|
signedPreKey: extractKey(signedKey)!,
|
||||||
const cipher = new libsignal.SessionBuilder(signalStorage(auth), jidToSignalProtocolAddress(jid))
|
preKey: extractKey(key)!
|
||||||
await cipher.initOutgoing(device)
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user