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
|
||||
src/WABinary/index.ts
|
||||
WAProto
|
||||
WASignalGroup
|
||||
WASignalGroup
|
||||
Example/test.ts
|
||||
@@ -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 =
|
||||
|
||||
|
||||
@@ -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!');
|
||||
|
||||
@@ -8,4 +8,7 @@ module.exports = {
|
||||
'transform': {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest'
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^axios$': require.resolve('axios'),
|
||||
},
|
||||
}
|
||||
@@ -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
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',
|
||||
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: [
|
||||
{
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
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'
|
||||
|
||||
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
|
||||
|
||||
@@ -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]: {} }
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
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 { 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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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)!
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user