feat: add signal repository + tests

This commit is contained in:
Adhiraj Singh
2023-03-18 12:25:47 +05:30
parent 2eea17fe9f
commit fe1d0649b5
21 changed files with 500 additions and 206 deletions

View File

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

View File

@@ -1,9 +1,8 @@
import { Boom } from '@hapi/boom'
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 +12,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 +94,12 @@ 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
) => {
const { fullMessage, author, sender } = decodeMessageNode(stanza, meId)
return {
fullMessage,
category: stanza.attrs.category,
@@ -118,18 +124,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 +152,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) {

View File

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

View File

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