diff --git a/package.json b/package.json index cda0ecaa796..0f78fa4142c 100644 --- a/package.json +++ b/package.json @@ -44,12 +44,12 @@ "@cacheable/node-cache": "^1.4.0", "@hapi/boom": "^9.1.3", "async-mutex": "^0.5.0", - "libsignal": "git+https://github.com/whiskeysockets/libsignal-node", "lru-cache": "^11.1.0", "music-metadata": "^11.7.0", "p-queue": "^9.0.0", "pino": "^9.6", "protobufjs": "^7.2.4", + "whatsapp-rust-bridge": "^0.5.0-alpha.1", "ws": "^8.13.0" }, "devDependencies": { diff --git a/src/Signal/Group/ciphertext-message.ts b/src/Signal/Group/ciphertext-message.ts deleted file mode 100644 index 238e0151767..00000000000 --- a/src/Signal/Group/ciphertext-message.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class CiphertextMessage { - readonly UNSUPPORTED_VERSION: number = 1 - readonly CURRENT_VERSION: number = 3 - readonly WHISPER_TYPE: number = 2 - readonly PREKEY_TYPE: number = 3 - readonly SENDERKEY_TYPE: number = 4 - readonly SENDERKEY_DISTRIBUTION_TYPE: number = 5 - readonly ENCRYPTED_MESSAGE_OVERHEAD: number = 53 -} diff --git a/src/Signal/Group/group-session-builder.ts b/src/Signal/Group/group-session-builder.ts deleted file mode 100644 index b2a90b61e82..00000000000 --- a/src/Signal/Group/group-session-builder.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as keyhelper from './keyhelper' -import { SenderKeyDistributionMessage } from './sender-key-distribution-message' -import { SenderKeyName } from './sender-key-name' -import { SenderKeyRecord } from './sender-key-record' - -interface SenderKeyStore { - loadSenderKey(senderKeyName: SenderKeyName): Promise - storeSenderKey(senderKeyName: SenderKeyName, record: SenderKeyRecord): Promise -} - -export class GroupSessionBuilder { - private readonly senderKeyStore: SenderKeyStore - - constructor(senderKeyStore: SenderKeyStore) { - this.senderKeyStore = senderKeyStore - } - - public async process( - senderKeyName: SenderKeyName, - senderKeyDistributionMessage: SenderKeyDistributionMessage - ): Promise { - const senderKeyRecord = await this.senderKeyStore.loadSenderKey(senderKeyName) - senderKeyRecord.addSenderKeyState( - senderKeyDistributionMessage.getId(), - senderKeyDistributionMessage.getIteration(), - senderKeyDistributionMessage.getChainKey(), - senderKeyDistributionMessage.getSignatureKey() - ) - await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord) - } - - public async create(senderKeyName: SenderKeyName): Promise { - const senderKeyRecord = await this.senderKeyStore.loadSenderKey(senderKeyName) - - if (senderKeyRecord.isEmpty()) { - const keyId = keyhelper.generateSenderKeyId() - const senderKey = keyhelper.generateSenderKey() - const signingKey = keyhelper.generateSenderSigningKey() - - senderKeyRecord.setSenderKeyState(keyId, 0, senderKey, signingKey) - await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord) - } - - const state = senderKeyRecord.getSenderKeyState() - if (!state) { - throw new Error('No session state available') - } - - return new SenderKeyDistributionMessage( - state.getKeyId(), - state.getSenderChainKey().getIteration(), - state.getSenderChainKey().getSeed(), - state.getSigningKeyPublic() - ) - } -} diff --git a/src/Signal/Group/group_cipher.ts b/src/Signal/Group/group_cipher.ts deleted file mode 100644 index 0f6c7f67ddc..00000000000 --- a/src/Signal/Group/group_cipher.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { decrypt, encrypt } from 'libsignal/src/crypto' -import { SenderKeyMessage } from './sender-key-message' -import { SenderKeyName } from './sender-key-name' -import { SenderKeyRecord } from './sender-key-record' -import { SenderKeyState } from './sender-key-state' - -export interface SenderKeyStore { - loadSenderKey(senderKeyName: SenderKeyName): Promise - - storeSenderKey(senderKeyName: SenderKeyName, record: SenderKeyRecord): Promise -} - -export class GroupCipher { - private readonly senderKeyStore: SenderKeyStore - private readonly senderKeyName: SenderKeyName - - constructor(senderKeyStore: SenderKeyStore, senderKeyName: SenderKeyName) { - this.senderKeyStore = senderKeyStore - this.senderKeyName = senderKeyName - } - - public async encrypt(paddedPlaintext: Uint8Array): Promise { - const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName) - if (!record) { - throw new Error('No SenderKeyRecord found for encryption') - } - - const senderKeyState = record.getSenderKeyState() - if (!senderKeyState) { - throw new Error('No session to encrypt message') - } - - const iteration = senderKeyState.getSenderChainKey().getIteration() - const senderKey = this.getSenderKey(senderKeyState, iteration === 0 ? 0 : iteration + 1) - - const ciphertext = await this.getCipherText(senderKey.getIv(), senderKey.getCipherKey(), paddedPlaintext) - - const senderKeyMessage = new SenderKeyMessage( - senderKeyState.getKeyId(), - senderKey.getIteration(), - ciphertext, - senderKeyState.getSigningKeyPrivate() - ) - - await this.senderKeyStore.storeSenderKey(this.senderKeyName, record) - return senderKeyMessage.serialize() - } - - public async decrypt(senderKeyMessageBytes: Uint8Array): Promise { - const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName) - if (!record) { - throw new Error('No SenderKeyRecord found for decryption') - } - - const senderKeyMessage = new SenderKeyMessage(null, null, null, null, senderKeyMessageBytes) - const senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId()) - if (!senderKeyState) { - throw new Error('No session found to decrypt message') - } - - senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic()) - const senderKey = this.getSenderKey(senderKeyState, senderKeyMessage.getIteration()) - - const plaintext = await this.getPlainText( - senderKey.getIv(), - senderKey.getCipherKey(), - senderKeyMessage.getCipherText() - ) - - await this.senderKeyStore.storeSenderKey(this.senderKeyName, record) - return plaintext - } - - private getSenderKey(senderKeyState: SenderKeyState, iteration: number) { - let senderChainKey = senderKeyState.getSenderChainKey() - if (senderChainKey.getIteration() > iteration) { - if (senderKeyState.hasSenderMessageKey(iteration)) { - const messageKey = senderKeyState.removeSenderMessageKey(iteration) - if (!messageKey) { - throw new Error('No sender message key found for iteration') - } - - return messageKey - } - - throw new Error(`Received message with old counter: ${senderChainKey.getIteration()}, ${iteration}`) - } - - if (iteration - senderChainKey.getIteration() > 2000) { - throw new Error('Over 2000 messages into the future!') - } - - while (senderChainKey.getIteration() < iteration) { - senderKeyState.addSenderMessageKey(senderChainKey.getSenderMessageKey()) - senderChainKey = senderChainKey.getNext() - } - - senderKeyState.setSenderChainKey(senderChainKey.getNext()) - return senderChainKey.getSenderMessageKey() - } - - private async getPlainText(iv: Uint8Array, key: Uint8Array, ciphertext: Uint8Array): Promise { - try { - return decrypt(key, ciphertext, iv) - } catch (e) { - throw new Error('InvalidMessageException') - } - } - - private async getCipherText(iv: Uint8Array, key: Uint8Array, plaintext: Uint8Array): Promise { - try { - return encrypt(key, plaintext, iv) - } catch (e) { - throw new Error('InvalidMessageException') - } - } -} diff --git a/src/Signal/Group/index.ts b/src/Signal/Group/index.ts deleted file mode 100644 index 52c983d7b10..00000000000 --- a/src/Signal/Group/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { GroupSessionBuilder } from './group-session-builder' -export { SenderKeyDistributionMessage } from './sender-key-distribution-message' -export { SenderKeyRecord } from './sender-key-record' -export { SenderKeyName } from './sender-key-name' -export { GroupCipher } from './group_cipher' -export { SenderKeyState } from './sender-key-state' -export { SenderKeyMessage } from './sender-key-message' -export { SenderMessageKey } from './sender-message-key' -export { SenderChainKey } from './sender-chain-key' -export { CiphertextMessage } from './ciphertext-message' -export * as keyhelper from './keyhelper' diff --git a/src/Signal/Group/keyhelper.ts b/src/Signal/Group/keyhelper.ts deleted file mode 100644 index acf274c660c..00000000000 --- a/src/Signal/Group/keyhelper.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as nodeCrypto from 'crypto' -import { generateKeyPair } from 'libsignal/src/curve' - -type KeyPairType = ReturnType - -export function generateSenderKey(): Buffer { - return nodeCrypto.randomBytes(32) -} - -export function generateSenderKeyId(): number { - return nodeCrypto.randomInt(2147483647) -} - -export interface SigningKeyPair { - public: Buffer - private: Buffer -} - -export function generateSenderSigningKey(key?: KeyPairType): SigningKeyPair { - if (!key) { - key = generateKeyPair() - } - - return { - public: Buffer.from(key.pubKey), - private: Buffer.from(key.privKey) - } -} diff --git a/src/Signal/Group/sender-chain-key.ts b/src/Signal/Group/sender-chain-key.ts deleted file mode 100644 index 18d5cbf883b..00000000000 --- a/src/Signal/Group/sender-chain-key.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { calculateMAC } from 'libsignal/src/crypto' -import { SenderMessageKey } from './sender-message-key' - -export class SenderChainKey { - private readonly MESSAGE_KEY_SEED: Uint8Array = Buffer.from([0x01]) - private readonly CHAIN_KEY_SEED: Uint8Array = Buffer.from([0x02]) - private readonly iteration: number - private readonly chainKey: Buffer - - constructor(iteration: number, chainKey: Uint8Array | Buffer) { - this.iteration = iteration - this.chainKey = Buffer.from(chainKey) - } - - public getIteration(): number { - return this.iteration - } - - public getSenderMessageKey(): SenderMessageKey { - return new SenderMessageKey(this.iteration, this.getDerivative(this.MESSAGE_KEY_SEED, this.chainKey)) - } - - public getNext(): SenderChainKey { - return new SenderChainKey(this.iteration + 1, this.getDerivative(this.CHAIN_KEY_SEED, this.chainKey)) - } - - public getSeed(): Uint8Array { - return this.chainKey - } - - private getDerivative(seed: Uint8Array, key: Buffer): Uint8Array { - return calculateMAC(key, seed) - } -} diff --git a/src/Signal/Group/sender-key-distribution-message.ts b/src/Signal/Group/sender-key-distribution-message.ts deleted file mode 100644 index 9888ae3895d..00000000000 --- a/src/Signal/Group/sender-key-distribution-message.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { proto } from '../../../WAProto/index.js' -import { CiphertextMessage } from './ciphertext-message' - -interface SenderKeyDistributionMessageStructure { - id: number - iteration: number - chainKey: string | Uint8Array - signingKey: string | Uint8Array -} - -export class SenderKeyDistributionMessage extends CiphertextMessage { - private readonly id: number - private readonly iteration: number - private readonly chainKey: Uint8Array - private readonly signatureKey: Uint8Array - private readonly serialized: Uint8Array - - constructor( - id?: number | null, - iteration?: number | null, - chainKey?: Uint8Array | null, - signatureKey?: Uint8Array | null, - serialized?: Uint8Array | null - ) { - super() - - if (serialized) { - try { - const message = serialized.slice(1) - const distributionMessage = proto.SenderKeyDistributionMessage.decode( - message - ).toJSON() as SenderKeyDistributionMessageStructure - - this.serialized = serialized - this.id = distributionMessage.id - this.iteration = distributionMessage.iteration - this.chainKey = - typeof distributionMessage.chainKey === 'string' - ? Buffer.from(distributionMessage.chainKey, 'base64') - : distributionMessage.chainKey - this.signatureKey = - typeof distributionMessage.signingKey === 'string' - ? Buffer.from(distributionMessage.signingKey, 'base64') - : distributionMessage.signingKey - } catch (e) { - throw new Error(String(e)) - } - } else { - const version = this.intsToByteHighAndLow(this.CURRENT_VERSION, this.CURRENT_VERSION) - this.id = id! - this.iteration = iteration! - this.chainKey = chainKey! - this.signatureKey = signatureKey! - - const message = proto.SenderKeyDistributionMessage.encode( - proto.SenderKeyDistributionMessage.create({ - id, - iteration, - chainKey, - signingKey: this.signatureKey - }) - ).finish() - - this.serialized = Buffer.concat([Buffer.from([version]), message]) - } - } - - private intsToByteHighAndLow(highValue: number, lowValue: number): number { - return (((highValue << 4) | lowValue) & 0xff) % 256 - } - - public serialize(): Uint8Array { - return this.serialized - } - - public getType(): number { - return this.SENDERKEY_DISTRIBUTION_TYPE - } - - public getIteration(): number { - return this.iteration - } - - public getChainKey(): Uint8Array { - return this.chainKey - } - - public getSignatureKey(): Uint8Array { - return this.signatureKey - } - - public getId(): number { - return this.id - } -} diff --git a/src/Signal/Group/sender-key-message.ts b/src/Signal/Group/sender-key-message.ts deleted file mode 100644 index e6d8ac14058..00000000000 --- a/src/Signal/Group/sender-key-message.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { calculateSignature, verifySignature } from 'libsignal/src/curve' -import { proto } from '../../../WAProto/index.js' -import { CiphertextMessage } from './ciphertext-message' - -interface SenderKeyMessageStructure { - id: number - iteration: number - ciphertext: string | Buffer -} - -export class SenderKeyMessage extends CiphertextMessage { - private readonly SIGNATURE_LENGTH = 64 - private readonly messageVersion: number - private readonly keyId: number - private readonly iteration: number - private readonly ciphertext: Uint8Array - private readonly signature: Uint8Array - private readonly serialized: Uint8Array - - constructor( - keyId?: number | null, - iteration?: number | null, - ciphertext?: Uint8Array | null, - signatureKey?: Uint8Array | null, - serialized?: Uint8Array | null - ) { - super() - - if (serialized) { - const version = serialized[0]! - const message = serialized.slice(1, serialized.length - this.SIGNATURE_LENGTH) - const signature = serialized.slice(-1 * this.SIGNATURE_LENGTH) - const senderKeyMessage = proto.SenderKeyMessage.decode(message).toJSON() as SenderKeyMessageStructure - - this.serialized = serialized - this.messageVersion = (version & 0xff) >> 4 - this.keyId = senderKeyMessage.id - this.iteration = senderKeyMessage.iteration - this.ciphertext = - typeof senderKeyMessage.ciphertext === 'string' - ? Buffer.from(senderKeyMessage.ciphertext, 'base64') - : senderKeyMessage.ciphertext - this.signature = signature - } else { - const version = (((this.CURRENT_VERSION << 4) | this.CURRENT_VERSION) & 0xff) % 256 - const ciphertextBuffer = Buffer.from(ciphertext!) - const message = proto.SenderKeyMessage.encode( - proto.SenderKeyMessage.create({ - id: keyId!, - iteration: iteration!, - ciphertext: ciphertextBuffer - }) - ).finish() - - const signature = this.getSignature(signatureKey!, Buffer.concat([Buffer.from([version]), message])) - - this.serialized = Buffer.concat([Buffer.from([version]), message, Buffer.from(signature)]) - this.messageVersion = this.CURRENT_VERSION - this.keyId = keyId! - this.iteration = iteration! - this.ciphertext = ciphertextBuffer - this.signature = signature - } - } - - public getKeyId(): number { - return this.keyId - } - - public getIteration(): number { - return this.iteration - } - - public getCipherText(): Uint8Array { - return this.ciphertext - } - - public verifySignature(signatureKey: Uint8Array): void { - const part1 = this.serialized.slice(0, this.serialized.length - this.SIGNATURE_LENGTH) - const part2 = this.serialized.slice(-1 * this.SIGNATURE_LENGTH) - const res = verifySignature(signatureKey, part1, part2) - if (!res) throw new Error('Invalid signature!') - } - - private getSignature(signatureKey: Uint8Array, serialized: Uint8Array): Uint8Array { - return Buffer.from(calculateSignature(signatureKey, serialized)) - } - - public serialize(): Uint8Array { - return this.serialized - } - - public getType(): number { - return 4 - } -} diff --git a/src/Signal/Group/sender-key-name.ts b/src/Signal/Group/sender-key-name.ts deleted file mode 100644 index 09486876b96..00000000000 --- a/src/Signal/Group/sender-key-name.ts +++ /dev/null @@ -1,66 +0,0 @@ -interface Sender { - id: string - deviceId: number - toString(): string -} - -function isNull(str: string | null): boolean { - return str === null || str === '' -} - -function intValue(num: number): number { - const MAX_VALUE = 0x7fffffff - const MIN_VALUE = -0x80000000 - if (num > MAX_VALUE || num < MIN_VALUE) { - return num & 0xffffffff - } - - return num -} - -function hashCode(strKey: string): number { - let hash = 0 - if (!isNull(strKey)) { - for (let i = 0; i < strKey.length; i++) { - hash = hash * 31 + strKey.charCodeAt(i) - hash = intValue(hash) - } - } - - return hash -} - -export class SenderKeyName { - private readonly groupId: string - private readonly sender: Sender - - constructor(groupId: string, sender: Sender) { - this.groupId = groupId - this.sender = sender - } - - public getGroupId(): string { - return this.groupId - } - - public getSender(): Sender { - return this.sender - } - - public serialize(): string { - return `${this.groupId}::${this.sender.id}::${this.sender.deviceId}` - } - - public toString(): string { - return this.serialize() - } - - public equals(other: SenderKeyName | null): boolean { - if (other === null) return false - return this.groupId === other.groupId && this.sender.toString() === other.sender.toString() - } - - public hashCode(): number { - return hashCode(this.groupId) ^ hashCode(this.sender.toString()) - } -} diff --git a/src/Signal/Group/sender-key-record.ts b/src/Signal/Group/sender-key-record.ts deleted file mode 100644 index dda30c1eb16..00000000000 --- a/src/Signal/Group/sender-key-record.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { BufferJSON } from '../../Utils/generics' -import { SenderKeyState } from './sender-key-state' - -export interface SenderKeyStateStructure { - senderKeyId: number - senderChainKey: { - iteration: number - seed: Uint8Array - } - senderSigningKey: { - public: Uint8Array - private?: Uint8Array - } - senderMessageKeys: Array<{ - iteration: number - seed: Uint8Array - }> -} - -export class SenderKeyRecord { - private readonly MAX_STATES = 5 - private readonly senderKeyStates: SenderKeyState[] = [] - - constructor(serialized?: SenderKeyStateStructure[]) { - if (serialized) { - for (const structure of serialized) { - this.senderKeyStates.push(new SenderKeyState(null, null, null, null, null, null, structure)) - } - } - } - - public isEmpty(): boolean { - return this.senderKeyStates.length === 0 - } - - public getSenderKeyState(keyId?: number): SenderKeyState | undefined { - if (keyId === undefined && this.senderKeyStates.length) { - return this.senderKeyStates[this.senderKeyStates.length - 1] - } - - return this.senderKeyStates.find(state => state.getKeyId() === keyId) - } - - public addSenderKeyState(id: number, iteration: number, chainKey: Uint8Array, signatureKey: Uint8Array): void { - this.senderKeyStates.push(new SenderKeyState(id, iteration, chainKey, null, signatureKey)) - if (this.senderKeyStates.length > this.MAX_STATES) { - this.senderKeyStates.shift() - } - } - - public setSenderKeyState( - id: number, - iteration: number, - chainKey: Uint8Array, - keyPair: { public: Uint8Array; private: Uint8Array } - ): void { - this.senderKeyStates.length = 0 - this.senderKeyStates.push(new SenderKeyState(id, iteration, chainKey, keyPair)) - } - - public serialize(): SenderKeyStateStructure[] { - return this.senderKeyStates.map(state => state.getStructure()) - } - static deserialize(data: Uint8Array): SenderKeyRecord { - const str = Buffer.from(data).toString('utf-8') - const parsed = JSON.parse(str, BufferJSON.reviver) - return new SenderKeyRecord(parsed) - } -} diff --git a/src/Signal/Group/sender-key-state.ts b/src/Signal/Group/sender-key-state.ts deleted file mode 100644 index 412972200c2..00000000000 --- a/src/Signal/Group/sender-key-state.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { SenderChainKey } from './sender-chain-key' -import { SenderMessageKey } from './sender-message-key' - -interface SenderChainKeyStructure { - iteration: number - seed: Uint8Array -} - -interface SenderSigningKeyStructure { - public: Uint8Array - private?: Uint8Array -} - -interface SenderMessageKeyStructure { - iteration: number - seed: Uint8Array -} - -interface SenderKeyStateStructure { - senderKeyId: number - senderChainKey: SenderChainKeyStructure - senderSigningKey: SenderSigningKeyStructure - senderMessageKeys: SenderMessageKeyStructure[] -} - -export class SenderKeyState { - private readonly MAX_MESSAGE_KEYS = 2000 - private readonly senderKeyStateStructure: SenderKeyStateStructure - - constructor( - id?: number | null, - iteration?: number | null, - chainKey?: Uint8Array | null | string, - signatureKeyPair?: { public: Uint8Array | string; private: Uint8Array | string } | null, - signatureKeyPublic?: Uint8Array | string | null, - signatureKeyPrivate?: Uint8Array | string | null, - senderKeyStateStructure?: SenderKeyStateStructure | null - ) { - if (senderKeyStateStructure) { - this.senderKeyStateStructure = { - ...senderKeyStateStructure, - senderMessageKeys: Array.isArray(senderKeyStateStructure.senderMessageKeys) - ? senderKeyStateStructure.senderMessageKeys - : [] - } - } else { - if (signatureKeyPair) { - signatureKeyPublic = signatureKeyPair.public - signatureKeyPrivate = signatureKeyPair.private - } - - this.senderKeyStateStructure = { - senderKeyId: id || 0, - senderChainKey: { - iteration: iteration || 0, - seed: Buffer.from(chainKey || []) - }, - senderSigningKey: { - public: Buffer.from(signatureKeyPublic || []), - private: Buffer.from(signatureKeyPrivate || []) - }, - senderMessageKeys: [] - } - } - } - - public getKeyId(): number { - return this.senderKeyStateStructure.senderKeyId - } - - public getSenderChainKey(): SenderChainKey { - return new SenderChainKey( - this.senderKeyStateStructure.senderChainKey.iteration, - this.senderKeyStateStructure.senderChainKey.seed - ) - } - - public setSenderChainKey(chainKey: SenderChainKey): void { - this.senderKeyStateStructure.senderChainKey = { - iteration: chainKey.getIteration(), - seed: chainKey.getSeed() - } - } - - public getSigningKeyPublic(): Buffer { - const publicKey = Buffer.from(this.senderKeyStateStructure.senderSigningKey.public) - - if (publicKey.length === 32) { - const fixed = Buffer.alloc(33) - fixed[0] = 0x05 - publicKey.copy(fixed, 1) - return fixed - } - - return publicKey - } - - public getSigningKeyPrivate(): Buffer | undefined { - const privateKey = this.senderKeyStateStructure.senderSigningKey.private - - return Buffer.from(privateKey || []) - } - - public hasSenderMessageKey(iteration: number): boolean { - return this.senderKeyStateStructure.senderMessageKeys.some(key => key.iteration === iteration) - } - - public addSenderMessageKey(senderMessageKey: SenderMessageKey): void { - this.senderKeyStateStructure.senderMessageKeys.push({ - iteration: senderMessageKey.getIteration(), - seed: senderMessageKey.getSeed() - }) - - if (this.senderKeyStateStructure.senderMessageKeys.length > this.MAX_MESSAGE_KEYS) { - this.senderKeyStateStructure.senderMessageKeys.shift() - } - } - - public removeSenderMessageKey(iteration: number): SenderMessageKey | null { - const index = this.senderKeyStateStructure.senderMessageKeys.findIndex(key => key.iteration === iteration) - - if (index !== -1) { - const messageKey = this.senderKeyStateStructure.senderMessageKeys[index]! - this.senderKeyStateStructure.senderMessageKeys.splice(index, 1) - return new SenderMessageKey(messageKey.iteration, messageKey.seed) - } - - return null - } - - public getStructure(): SenderKeyStateStructure { - return this.senderKeyStateStructure - } -} diff --git a/src/Signal/Group/sender-message-key.ts b/src/Signal/Group/sender-message-key.ts deleted file mode 100644 index 7336a6e06d9..00000000000 --- a/src/Signal/Group/sender-message-key.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { deriveSecrets } from 'libsignal/src/crypto' - -export class SenderMessageKey { - private readonly iteration: number - private readonly iv: Uint8Array - private readonly cipherKey: Uint8Array - private readonly seed: Uint8Array - - constructor(iteration: number, seed: Uint8Array) { - const derivative = deriveSecrets(seed, Buffer.alloc(32), Buffer.from('WhisperGroup')) - const keys = new Uint8Array(32) - keys.set(new Uint8Array(derivative[0].slice(16))) - keys.set(new Uint8Array(derivative[1].slice(0, 16)), 16) - - this.iv = Buffer.from(derivative[0].slice(0, 16)) - this.cipherKey = Buffer.from(keys.buffer) - this.iteration = iteration - this.seed = seed - } - - public getIteration(): number { - return this.iteration - } - - public getIv(): Uint8Array { - return this.iv - } - - public getCipherKey(): Uint8Array { - return this.cipherKey - } - - public getSeed(): Uint8Array { - return this.seed - } -} diff --git a/src/Signal/libsignal.ts b/src/Signal/libsignal.ts index 228dc06281b..9e868ff056c 100644 --- a/src/Signal/libsignal.ts +++ b/src/Signal/libsignal.ts @@ -1,9 +1,19 @@ -/* @ts-ignore */ -import * as libsignal from 'libsignal' import { LRUCache } from 'lru-cache' +import type { SignalStorage } from 'whatsapp-rust-bridge' +import { + GroupCipher, + GroupSessionBuilder, + hasLogger, + ProtocolAddress, + SenderKeyDistributionMessage, + SenderKeyName, + SessionBuilder, + SessionCipher, + SessionRecord, + setLogger +} from 'whatsapp-rust-bridge' import type { LIDMapping, SignalAuthState, SignalKeyStoreWithTransaction } from '../Types' import type { SignalRepositoryWithLIDStore } from '../Types/Signal' -import { generateSignalPubKey } from '../Utils' import type { ILogger } from '../Utils/logger' import { isHostedLidUser, @@ -14,10 +24,6 @@ import { transferDevice, WAJIDDomains } from '../WABinary' -import type { SenderKeyStore } from './Group/group_cipher' -import { SenderKeyName } from './Group/sender-key-name' -import { SenderKeyRecord } from './Group/sender-key-record' -import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage } from './Group' import { LIDMappingStore } from './lid-mapping' export function makeLibSignalRepository( @@ -25,6 +31,10 @@ export function makeLibSignalRepository( logger: ILogger, pnToLIDFunc?: (jids: string[]) => Promise ): SignalRepositoryWithLIDStore { + if (!hasLogger()) { + setLogger(logger) + } + const lidMapping = new LIDMappingStore(auth.keys as SignalKeyStoreWithTransaction, logger, pnToLIDFunc) const storage = signalStorage(auth, lidMapping) @@ -37,8 +47,8 @@ export function makeLibSignalRepository( const repository: SignalRepositoryWithLIDStore = { decryptGroupMessage({ group, authorJid, msg }) { - const senderName = jidToSignalSenderKeyName(group, authorJid) - const cipher = new GroupCipher(storage, senderName) + const senderAddr = jidToSignalProtocolAddress(authorJid) + const cipher = new GroupCipher(storage, group, senderAddr) // Use transaction to ensure atomicity return parsedKeys.transaction(async () => { @@ -51,36 +61,21 @@ export function makeLibSignalRepository( throw new Error('Group ID is required for sender key distribution message') } - const senderName = jidToSignalSenderKeyName(item.groupId, authorJid) + const senderAddr = jidToSignalProtocolAddress(authorJid) - const senderMsg = new SenderKeyDistributionMessage( - null, - null, - null, - null, - item.axolotlSenderKeyDistributionMessage - ) - const senderNameStr = senderName.toString() - const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]) - if (!senderKey) { - await storage.storeSenderKey(senderName, new SenderKeyRecord()) - } + const senderName = new SenderKeyName(item.groupId, senderAddr) + const senderMsg = SenderKeyDistributionMessage.deserialize(item.axolotlSenderKeyDistributionMessage!) return parsedKeys.transaction(async () => { - const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]) - if (!senderKey) { - await storage.storeSenderKey(senderName, new SenderKeyRecord()) - } - await builder.process(senderName, senderMsg) }, item.groupId) }, async decryptMessage({ jid, type, ciphertext }) { const addr = jidToSignalProtocolAddress(jid) - const session = new libsignal.SessionCipher(storage, addr) + const session = new SessionCipher(storage, addr) async function doDecrypt() { - let result: Buffer + let result: Uint8Array switch (type) { case 'pkmsg': result = await session.decryptPreKeyWhisperMessage(ciphertext) @@ -102,31 +97,27 @@ export function makeLibSignalRepository( async encryptMessage({ jid, data }) { const addr = jidToSignalProtocolAddress(jid) - const cipher = new libsignal.SessionCipher(storage, addr) + const cipher = new SessionCipher(storage, addr) // Use transaction to ensure atomicity return parsedKeys.transaction(async () => { const { type: sigType, body } = await cipher.encrypt(data) const type = sigType === 3 ? 'pkmsg' : 'msg' - return { type, ciphertext: Buffer.from(body, 'binary') } + return { type, ciphertext: body } }, jid) }, async encryptGroupMessage({ group, meId, data }) { - const senderName = jidToSignalSenderKeyName(group, meId) const builder = new GroupSessionBuilder(storage) + const meAddr = jidToSignalProtocolAddress(meId) + const senderName = jidToSignalSenderKeyName(group, meId) - const senderNameStr = senderName.toString() + const senderKeyDistributionMessage = await builder.create(senderName) - return parsedKeys.transaction(async () => { - const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]) - if (!senderKey) { - await storage.storeSenderKey(senderName, new SenderKeyRecord()) - } + const cipher = new GroupCipher(storage, group, meAddr) - const senderKeyDistributionMessage = await builder.create(senderName) - const session = new GroupCipher(storage, senderName) - const ciphertext = await session.encrypt(data) + return parsedKeys.transaction(async () => { + const ciphertext = await cipher.encrypt(data) return { ciphertext, @@ -137,7 +128,7 @@ export function makeLibSignalRepository( async injectE2ESession({ jid, session }) { logger.trace({ jid }, 'injecting E2EE session') - const cipher = new libsignal.SessionBuilder(storage, jidToSignalProtocolAddress(jid)) + const cipher = new SessionBuilder(storage, jidToSignalProtocolAddress(jid)) return parsedKeys.transaction(async () => { await cipher.initOutgoing(session) }, jid) @@ -158,10 +149,6 @@ export function makeLibSignalRepository( return { exists: false, reason: 'no session' } } - if (!session.haveOpenSession()) { - return { exists: false, reason: 'no open session' } - } - return { exists: true } } catch (error) { return { exists: false, reason: 'validation error' } @@ -259,8 +246,8 @@ export function makeLibSignalRepository( pnUser: string lidUser: string deviceId: number - fromAddr: libsignal.ProtocolAddress - toAddr: libsignal.ProtocolAddress + fromAddr: ProtocolAddress + toAddr: ProtocolAddress } const migrationOps: MigrationOp[] = deviceJids.map(jid => { @@ -296,7 +283,7 @@ export function makeLibSignalRepository( const pnSession = pnSessions[pnAddrStr] if (pnSession) { // Session exists (guaranteed from device discovery) - const fromSession = libsignal.SessionRecord.deserialize(pnSession) + const fromSession = SessionRecord.deserialize(pnSession) if (fromSession.haveOpenSession()) { // Queue for bulk update: copy to LID, delete from PN sessionUpdates[lidAddrStr] = fromSession.serialize() @@ -332,7 +319,7 @@ export function makeLibSignalRepository( return repository } -const jidToSignalProtocolAddress = (jid: string): libsignal.ProtocolAddress => { +const jidToSignalProtocolAddress = (jid: string): ProtocolAddress => { const decoded = jidDecode(jid)! const { user, device, server, domainType } = decoded @@ -349,17 +336,14 @@ const jidToSignalProtocolAddress = (jid: string): libsignal.ProtocolAddress => { throw new Error('Unexpected non-hosted device JID with device 99. This ID seems invalid. ID:' + jid) } - return new libsignal.ProtocolAddress(signalUser, finalDevice) + return new ProtocolAddress(signalUser, finalDevice) } const jidToSignalSenderKeyName = (group: string, user: string): SenderKeyName => { return new SenderKeyName(group, jidToSignalProtocolAddress(user)) } -function signalStorage( - { creds, keys }: SignalAuthState, - lidMapping: LIDMappingStore -): SenderKeyStore & libsignal.SignalStorage { +function signalStorage({ creds, keys }: SignalAuthState, lidMapping: LIDMappingStore): SignalStorage { // Shared function to resolve PN signal address to LID if mapping exists const resolveLIDSignalAddress = async (id: string): Promise => { if (id.includes('.')) { @@ -386,61 +370,61 @@ function signalStorage( try { const wireJid = await resolveLIDSignalAddress(id) const { [wireJid]: sess } = await keys.get('session', [wireJid]) - if (sess) { - return libsignal.SessionRecord.deserialize(sess) + return sess } + + return null } catch (e) { return null } - - return null }, - storeSession: async (id: string, session: libsignal.SessionRecord) => { + storeSession: async (id: string, session: SessionRecord) => { const wireJid = await resolveLIDSignalAddress(id) await keys.set({ session: { [wireJid]: session.serialize() } }) }, isTrustedIdentity: () => { return true // todo: implement }, - loadPreKey: async (id: number | string) => { + loadPreKey: async (id: number) => { 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) + privKey: key.private, + pubKey: key.public } } }, removePreKey: (id: number) => keys.set({ 'pre-key': { [id]: null } }), - loadSignedPreKey: () => { + loadSignedPreKey: async (id: number) => { const key = creds.signedPreKey + if (!key || key.keyId !== id) { + return null + } + return { - privKey: Buffer.from(key.keyPair.private), - pubKey: Buffer.from(key.keyPair.public) + keyId: key.keyId, + signature: key.signature, + keyPair: { + pubKey: key.keyPair.public, + privKey: key.keyPair.private + } } }, - loadSenderKey: async (senderKeyName: SenderKeyName) => { - const keyId = senderKeyName.toString() + loadSenderKey: async (keyId: string) => { const { [keyId]: key } = await keys.get('sender-key', [keyId]) - if (key) { - return SenderKeyRecord.deserialize(key) - } - - return new SenderKeyRecord() + return key ?? null }, - storeSenderKey: async (senderKeyName: SenderKeyName, key: SenderKeyRecord) => { - const keyId = senderKeyName.toString() - const serialized = JSON.stringify(key.serialize()) - await keys.set({ 'sender-key': { [keyId]: Buffer.from(serialized, 'utf-8') } }) + storeSenderKey: async (keyId: string, keyBytes: Uint8Array) => { + await keys.set({ 'sender-key': { [keyId]: keyBytes.slice() } }) }, getOurRegistrationId: () => creds.registrationId, getOurIdentity: () => { const { signedIdentityKey } = creds return { - privKey: Buffer.from(signedIdentityKey.private), - pubKey: Buffer.from(generateSignalPubKey(signedIdentityKey.public)) + privKey: signedIdentityKey.private, + pubKey: signedIdentityKey.public } } } diff --git a/src/Utils/auth-utils.ts b/src/Utils/auth-utils.ts index a326c1246d7..d33f4f7b6e4 100644 --- a/src/Utils/auth-utils.ts +++ b/src/Utils/auth-utils.ts @@ -1,7 +1,7 @@ -import NodeCache from '@cacheable/node-cache' import { AsyncLocalStorage } from 'async_hooks' import { Mutex } from 'async-mutex' import { randomBytes } from 'crypto' +import { LRUCache } from 'lru-cache' import PQueue from 'p-queue' import { DEFAULT_CACHE_TTLS } from '../Defaults' import type { @@ -38,13 +38,17 @@ export function makeCacheableSignalKeyStore( logger?: ILogger, _cache?: CacheStore ): SignalKeyStore { - const cache = - _cache || - new NodeCache({ - stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, // 5 minutes - useClones: false, - deleteOnExpire: true - }) + const lruCache = new LRUCache({ + ttl: DEFAULT_CACHE_TTLS.SIGNAL_STORE * 1000, + ttlAutopurge: true + }) + + const cache: CacheStore = _cache ?? { + get: (key: string) => lruCache.get(key) as T | undefined, + set: (key, value) => void lruCache.set(key, value as SignalDataTypeMap[keyof SignalDataTypeMap]), + del: key => void lruCache.delete(key), + flushAll: () => lruCache.clear() + } // Mutex for protecting cache operations const cacheMutex = new Mutex() diff --git a/src/Utils/crypto.ts b/src/Utils/crypto.ts index 0e0dc2a1f45..b9b2cd1d00d 100644 --- a/src/Utils/crypto.ts +++ b/src/Utils/crypto.ts @@ -1,5 +1,5 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto' -import * as curve from 'libsignal/src/curve' +import { calculateAgreement, calculateSignature, generateKeyPair, verifySignature } from 'whatsapp-rust-bridge' import { KEY_BUNDLE_TYPE } from '../Defaults' import type { KeyPair } from '../Types' @@ -12,7 +12,7 @@ export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) => export const Curve = { generateKeyPair: (): KeyPair => { - const { pubKey, privKey } = curve.generateKeyPair() + const { pubKey, privKey } = generateKeyPair() return { private: Buffer.from(privKey), // remove version byte @@ -20,13 +20,13 @@ export const Curve = { } }, sharedKey: (privateKey: Uint8Array, publicKey: Uint8Array) => { - const shared = curve.calculateAgreement(generateSignalPubKey(publicKey), privateKey) + const shared = calculateAgreement(generateSignalPubKey(publicKey), privateKey) return Buffer.from(shared) }, - sign: (privateKey: Uint8Array, buf: Uint8Array) => curve.calculateSignature(privateKey, buf), + sign: (privateKey: Uint8Array, buf: Uint8Array) => calculateSignature(privateKey, buf), verify: (pubKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => { try { - curve.verifySignature(generateSignalPubKey(pubKey), message, signature) + verifySignature(generateSignalPubKey(pubKey), message, signature) return true } catch (error) { return false diff --git a/src/Utils/decode-wa-message.ts b/src/Utils/decode-wa-message.ts index dc13fa4ef0f..f3206dea9f4 100644 --- a/src/Utils/decode-wa-message.ts +++ b/src/Utils/decode-wa-message.ts @@ -319,7 +319,7 @@ export const decryptMessageNode = ( } else { fullMessage.message = msg } - } catch (err: any) { + } catch (err: unknown) { const errorContext = { key: fullMessage.key, err, @@ -332,7 +332,7 @@ export const decryptMessageNode = ( logger.error(errorContext, 'failed to decrypt message') fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT - fullMessage.messageStubParameters = [err.message.toString()] + fullMessage.messageStubParameters = [safeGetErrorMessage(err)] } } } @@ -353,3 +353,15 @@ function isSessionRecordError(error: any): boolean { const errorMessage = error?.message || error?.toString() || '' return DECRYPTION_RETRY_CONFIG.sessionRecordErrors.some(errorPattern => errorMessage.includes(errorPattern)) } + +function safeGetErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message + } + + if (typeof error === 'object' && error !== null && 'message' in error) { + return String((error as any).message) + } + + return String(error) +} diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index d308e62ea92..a3af47b8956 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -2,6 +2,7 @@ import { Boom } from '@hapi/boom' import { createHash, randomBytes } from 'crypto' import { proto } from '../../WAProto/index.js' const baileysVersion = [2, 3000, 1027934701] +import type Long from 'long' import type { BaileysEventEmitter, BaileysEventMap, diff --git a/src/Utils/signal.ts b/src/Utils/signal.ts index fcc9d814320..41ed2e3fde9 100644 --- a/src/Utils/signal.ts +++ b/src/Utils/signal.ts @@ -154,7 +154,8 @@ export const extractDeviceJids = ( ((myUser !== user && myLid !== user) || myDevice !== device) && // either different user or if me user, not this device (device === 0 || !!keyIndex) // ensure that "key-index" is specified for "non-zero" devices, produces a bad req otherwise ) { - if (isHosted) { + // Device 99 must always be on the hosted domain + if (isHosted || device === 99) { domainType = domainType === WAJIDDomains.LID ? WAJIDDomains.HOSTED_LID : WAJIDDomains.HOSTED } diff --git a/src/__tests__/Signal/Group/sender-key-state-regression.test.ts b/src/__tests__/Signal/Group/sender-key-state-regression.test.ts deleted file mode 100644 index 9c49f62613a..00000000000 --- a/src/__tests__/Signal/Group/sender-key-state-regression.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { SenderKeyState } from '../../../Signal/Group/sender-key-state' -import { SenderMessageKey } from '../../../Signal/Group/sender-message-key' - -describe('SenderKeyState regression: missing senderMessageKeys array', () => { - it('should initialize senderMessageKeys when absent in provided structure', () => { - const legacyStructure = { - senderKeyId: 42, - senderChainKey: { iteration: 0, seed: Buffer.from([1, 2, 3]) }, - senderSigningKey: { public: Buffer.from([4, 5, 6]) } - } - - const state = new SenderKeyState(null, null, null, null, null, null, legacyStructure as any) - const msgKey = new SenderMessageKey(0, Buffer.from([7, 8, 9])) - state.addSenderMessageKey(msgKey) - - const structure = state.getStructure() - expect(structure.senderMessageKeys).toBeDefined() - expect(Array.isArray(structure.senderMessageKeys)).toBe(true) - expect(structure.senderMessageKeys.length).toBe(1) - expect(structure.senderMessageKeys[0]?.iteration).toBe(0) - }) -}) diff --git a/src/__tests__/Signal/libsignal.test.ts b/src/__tests__/Signal/libsignal.test.ts new file mode 100644 index 00000000000..5f5c3640fec --- /dev/null +++ b/src/__tests__/Signal/libsignal.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from '@jest/globals' +import P from 'pino' +import { makeLibSignalRepository } from '../../Signal/libsignal' +import type { SignalAuthState } from '../../Types' + +const logger = P({ level: 'silent' }) + +describe('jidToSignalProtocolAddress', () => { + // Create a minimal mock auth state + const mockAuth: SignalAuthState = { + creds: { + registrationId: 1234, + signedIdentityKey: { + public: new Uint8Array(32), + private: new Uint8Array(32) + }, + signedPreKey: { + keyId: 1, + keyPair: { + public: new Uint8Array(32), + private: new Uint8Array(32) + }, + signature: new Uint8Array(64) + } + }, + keys: { + get: async () => ({}), + set: async () => {}, + transaction: async (work: any) => await work(), + isInTransaction: () => false + } + } as any + + const repository = makeLibSignalRepository(mockAuth, logger) + + describe('device 99 validation', () => { + it('should accept :99@hosted for hosted PN devices', () => { + const jid = '5511999887766:99@hosted' + expect(() => repository.jidToSignalProtocolAddress(jid)).not.toThrow() + const result = repository.jidToSignalProtocolAddress(jid) + expect(result).toContain('5511999887766_128.99') + }) + + it('should accept :99@hosted.lid for hosted LID devices', () => { + const jid = '18217575229588:99@hosted.lid' + expect(() => repository.jidToSignalProtocolAddress(jid)).not.toThrow() + const result = repository.jidToSignalProtocolAddress(jid) + expect(result).toContain('18217575229588_129.99') + }) + + it('should reject :99@lid as invalid (must be @hosted.lid, not @lid)', () => { + const jid = '18217575229588:99@lid' + expect(() => repository.jidToSignalProtocolAddress(jid)).toThrow( + 'Unexpected non-hosted device JID with device 99' + ) + }) + + it('should reject :99@s.whatsapp.net as invalid', () => { + const jid = '5511999887766:99@s.whatsapp.net' + expect(() => repository.jidToSignalProtocolAddress(jid)).toThrow( + 'Unexpected non-hosted device JID with device 99' + ) + }) + + it('should reject :99@g.us as invalid', () => { + const jid = '123456789:99@g.us' + expect(() => repository.jidToSignalProtocolAddress(jid)).toThrow( + 'Unexpected non-hosted device JID with device 99' + ) + }) + }) + + describe('standard device validation', () => { + it('should handle regular PN JID without device', () => { + const jid = '5511999887766@s.whatsapp.net' + const result = repository.jidToSignalProtocolAddress(jid) + expect(result).toBe('5511999887766.0') + }) + + it('should handle LID JID without device', () => { + const jid = '18217575229588@lid' + const result = repository.jidToSignalProtocolAddress(jid) + expect(result).toBe('18217575229588_1.0') + }) + + it('should handle PN JID with companion device', () => { + const jid = '5511999887766:1@s.whatsapp.net' + const result = repository.jidToSignalProtocolAddress(jid) + expect(result).toBe('5511999887766.1') + }) + + it('should handle LID JID with companion device', () => { + const jid = '18217575229588:33@lid' + const result = repository.jidToSignalProtocolAddress(jid) + expect(result).toBe('18217575229588_1.33') + }) + }) +}) diff --git a/src/__tests__/Utils/signal-hosted.test.ts b/src/__tests__/Utils/signal-hosted.test.ts new file mode 100644 index 00000000000..3e8c234ca1b --- /dev/null +++ b/src/__tests__/Utils/signal-hosted.test.ts @@ -0,0 +1,106 @@ +import { extractDeviceJids } from '../../Utils/signal' +import { WAJIDDomains } from '../../WABinary' + +describe('extractDeviceJids Hosted Device Logic', () => { + const myJid = '11111111111@s.whatsapp.net' + const myLid = '22222222222@lid' + + it('should correctly convert PN user with device 99 to @hosted domain', () => { + const targetUser = '33333333333@s.whatsapp.net' + // Mock a USync result where isHosted is MISSING/false for device 99 + const mockResult = [ + { + id: targetUser, + devices: { + deviceList: [{ id: 99, keyIndex: 1, isHosted: false }] + } + } + ] + + const result = extractDeviceJids(mockResult as any, myJid, myLid, false) + + expect(result).toHaveLength(1) + expect(result[0]!.device).toBe(99) + // Must be HOSTED (1), not WHATSAPP (0) + expect(result[0]!.domainType).toBe(WAJIDDomains.HOSTED) + expect(result[0]!.server).toBe('hosted') + }) + + it('should correctly convert LID user with device 99 to @hosted.lid domain', () => { + const targetUser = '44444444444@lid' + const mockResult = [ + { + id: targetUser, + devices: { + deviceList: [{ id: 99, keyIndex: 1, isHosted: false }] + } + } + ] + + const result = extractDeviceJids(mockResult as any, myJid, myLid, false) + + expect(result).toHaveLength(1) + expect(result[0]!.device).toBe(99) + // Must be HOSTED_LID (129), not LID (1) + expect(result[0]!.domainType).toBe(WAJIDDomains.HOSTED_LID) + expect(result[0]!.server).toBe('hosted.lid') + }) + + it('should respect explicit isHosted flag for non-99 devices', () => { + const targetUser = '55555555555@s.whatsapp.net' + const mockResult = [ + { + id: targetUser, + devices: { + deviceList: [{ id: 33, keyIndex: 1, isHosted: true }] + } + } + ] + + const result = extractDeviceJids(mockResult as any, myJid, myLid, false) + + expect(result).toHaveLength(1) + expect(result[0]!.device).toBe(33) + expect(result[0]!.domainType).toBe(WAJIDDomains.HOSTED) + expect(result[0]!.server).toBe('hosted') + }) + + it('should NOT force hosted domain for non-99 devices without isHosted flag', () => { + const targetUser = '66666666666@s.whatsapp.net' + const mockResult = [ + { + id: targetUser, + devices: { + deviceList: [{ id: 33, keyIndex: 1, isHosted: false }] + } + } + ] + + const result = extractDeviceJids(mockResult as any, myJid, myLid, false) + + expect(result).toHaveLength(1) + expect(result[0]!.device).toBe(33) + // Should remain WHATSAPP (0) when isHosted=false + expect(result[0]!.domainType).toBe(WAJIDDomains.WHATSAPP) + expect(result[0]!.server).toBe('s.whatsapp.net') + }) + + it('should handle device 99 with explicit isHosted=true (redundant but safe)', () => { + const targetUser = '77777777777@lid' + const mockResult = [ + { + id: targetUser, + devices: { + deviceList: [{ id: 99, keyIndex: 1, isHosted: true }] + } + } + ] + + const result = extractDeviceJids(mockResult as any, myJid, myLid, false) + + expect(result).toHaveLength(1) + expect(result[0]!.device).toBe(99) + expect(result[0]!.domainType).toBe(WAJIDDomains.HOSTED_LID) + expect(result[0]!.server).toBe('hosted.lid') + }) +}) diff --git a/yarn.lock b/yarn.lock index 13b75cc13b5..d088334318b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2102,13 +2102,6 @@ __metadata: languageName: node linkType: hard -"@types/long@npm:^4.0.0": - version: 4.0.2 - resolution: "@types/long@npm:4.0.2" - checksum: 10c0/42ec66ade1f72ff9d143c5a519a65efc7c1c77be7b1ac5455c530ae9acd87baba065542f8847522af2e3ace2cc999f3ad464ef86e6b7352eece34daf88f8c924 - languageName: node - linkType: hard - "@types/markdown-it@npm:^14.1.1": version: 14.1.2 resolution: "@types/markdown-it@npm:14.1.2" @@ -2142,13 +2135,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^10.1.0": - version: 10.17.60 - resolution: "@types/node@npm:10.17.60" - checksum: 10c0/0742294912a6e79786cdee9ed77cff6ee8ff007b55d8e21170fc3e5994ad3a8101fea741898091876f8dc32b0a5ae3d64537b7176799e92da56346028d2cbcd2 - languageName: node - linkType: hard - "@types/node@npm:^20.9.0": version: 20.19.13 resolution: "@types/node@npm:20.19.13" @@ -3007,7 +2993,6 @@ __metadata: jimp: "npm:^1.6.0" jiti: "npm:^2.4.2" json: "npm:^11.0.0" - libsignal: "git+https://github.com/whiskeysockets/libsignal-node" link-preview-js: "npm:^3.0.0" lru-cache: "npm:^11.1.0" music-metadata: "npm:^11.7.0" @@ -3025,6 +3010,7 @@ __metadata: typedoc: "npm:^0.27.9" typedoc-plugin-markdown: "npm:4.4.2" typescript: "npm:^5.8.2" + whatsapp-rust-bridge: "npm:^0.5.0-alpha.1" ws: "npm:^8.13.0" peerDependencies: audio-decode: ^2.1.3 @@ -3730,13 +3716,6 @@ __metadata: languageName: node linkType: hard -"curve25519-js@npm:^0.0.4": - version: 0.0.4 - resolution: "curve25519-js@npm:0.0.4" - checksum: 10c0/5b6c3a0dcaf045588aa78c2d1113310bf93fda9c59bd533b2a06da807024eec92feb39b203d1db9c09eda94bba1252d507fb3901283d32898e43090546785ddd - languageName: node - linkType: hard - "data-uri-to-buffer@npm:^4.0.0": version: 4.0.1 resolution: "data-uri-to-buffer@npm:4.0.1" @@ -6976,16 +6955,6 @@ __metadata: languageName: node linkType: hard -"libsignal@git+https://github.com/whiskeysockets/libsignal-node": - version: 2.0.1 - resolution: "libsignal@https://github.com/whiskeysockets/libsignal-node.git#commit=e81ecfc32eb74951d789ab37f7e341ab66d5fff1" - dependencies: - curve25519-js: "npm:^0.0.4" - protobufjs: "npm:6.8.8" - checksum: 10c0/d1ae7d8a5fadd6bb1c486d1b2ebc388967fee57c13f52b473127c1cbd9cd647b44545ff07c2b9cc49b3dea4e25ccfcfece31c526fdbdbf065837c85d189e97a0 - languageName: node - linkType: hard - "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -7106,13 +7075,6 @@ __metadata: languageName: node linkType: hard -"long@npm:^4.0.0": - version: 4.0.0 - resolution: "long@npm:4.0.0" - checksum: 10c0/50a6417d15b06104dbe4e3d4a667c39b137f130a9108ea8752b352a4cfae047531a3ac351c181792f3f8768fe17cca6b0f406674a541a86fb638aaac560d83ed - languageName: node - linkType: hard - "long@npm:^5.0.0": version: 5.3.2 resolution: "long@npm:5.3.2" @@ -8404,30 +8366,6 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:6.8.8": - version: 6.8.8 - resolution: "protobufjs@npm:6.8.8" - dependencies: - "@protobufjs/aspromise": "npm:^1.1.2" - "@protobufjs/base64": "npm:^1.1.2" - "@protobufjs/codegen": "npm:^2.0.4" - "@protobufjs/eventemitter": "npm:^1.1.0" - "@protobufjs/fetch": "npm:^1.1.0" - "@protobufjs/float": "npm:^1.0.2" - "@protobufjs/inquire": "npm:^1.1.0" - "@protobufjs/path": "npm:^1.1.2" - "@protobufjs/pool": "npm:^1.1.0" - "@protobufjs/utf8": "npm:^1.1.0" - "@types/long": "npm:^4.0.0" - "@types/node": "npm:^10.1.0" - long: "npm:^4.0.0" - bin: - pbjs: bin/pbjs - pbts: bin/pbts - checksum: 10c0/2511ed6089245b2102c333ac56190b104f8d8227972c00f041def8387abf841fded7b2cb7130063666b7bca84597a43005ea05c5f674132a0ddd5eb94a6e7916 - languageName: node - linkType: hard - "protobufjs@npm:^7.2.4": version: 7.5.3 resolution: "protobufjs@npm:7.5.3" @@ -10184,6 +10122,13 @@ __metadata: languageName: node linkType: hard +"whatsapp-rust-bridge@npm:^0.5.0-alpha.1": + version: 0.5.0-alpha.1 + resolution: "whatsapp-rust-bridge@npm:0.5.0-alpha.1" + checksum: 10c0/aa6eec3c95996ede72080ddacabe900424223c17e74db10fca0ffa0f028cfc8d74b483254c2f4e0650f4c6cc1eecee1227f060b1b2a56128a7d2205bbb6f4393 + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0"