Source: session/ChannelSessionPrimitives.js

'use strict';

/**
 * Serialized into the ORIGINAL transport realm. These are opaque, generic
 * channel-session operations, not a VoIP implementation. In particular this
 * module does not know about call keys, offers, SDP, media or call state.
 * Private identity keys, prekeys and Signal ratchets never leave WhatsApp's
 * existing store. Encryption uses its own per-user lock and session jobs.
 */
function attachOriginalChannelSession(transport) {
    const fail = () => new Error('wwebjs_original_session_unavailable');
    const req = globalThis.require;
    const limit = 1024 * 1024;
    const pending = new Map();
    let retired = false;
    try {
        transport.check();
        const prefs = req('WAWebUserPrefsMeUser');
        const factory = req('WAWebWidFactory');
        const storeApi = req('WAWebSignalProtocolStore');
        const cipher = req('WAWebSignalCipherApi');
        const adv = req('WAWebAdvSignatureApi');
        const sessions = req('WAWebManageE2ESessionsJob');
        const scope = req('WAWebSessionScope').SessionScope.DEFAULT;
        const store = storeApi.getSignalProtocolStore();
        if (
            scope !== 'default' ||
            typeof store?.flushBufferToDiskIfNotMemOnlyMode !== 'function' ||
            typeof cipher.encryptSignalProto !== 'function' ||
            typeof cipher.decryptSignalProto !== 'function' ||
            typeof adv.getADVEncodedIdentity !== 'function' ||
            typeof adv.validateADVwithEncs !== 'function' ||
            typeof sessions.ensureE2ESessions !== 'function'
        )
            throw fail();

        const address = (value, group = false) => {
            const pattern = group
                ? /^[0-9-]{1,192}@g\.us$/
                : /^[0-9]{1,32}(?::[1-9][0-9]{0,2})?@(s\.whatsapp\.net|lid|hosted|hosted\.lid)$/;
            if (typeof value !== 'string' || !pattern.test(value)) throw fail();
            const device = Number(value.split('@')[0].split(':')[1] ?? 0);
            if (device > 255) throw fail();
            return value;
        };
        const nativeAddress = (wid, group = false) => {
            if (!wid || typeof wid.toString !== 'function') throw fail();
            return address(
                wid
                    .toString({ legacy: true })
                    .replace(/@c\.us$/, '@s.whatsapp.net')
                    .replace(/:0@/, '@'),
                group,
            );
        };
        const wid = (value, group = false) => {
            address(value, group);
            const result = factory.createWid(value);
            if (nativeAddress(result, group) !== value) throw fail();
            return result;
        };
        const userWid = (value) => {
            const result = wid(value);
            if (value.includes(':')) throw fail();
            return result;
        };
        const readIdentity = () => {
            const phone = nativeAddress(prefs.getMaybeMeDevicePn());
            const lid = nativeAddress(prefs.getMaybeMeDeviceLid());
            if (!phone.endsWith('@s.whatsapp.net') || !lid.endsWith('@lid'))
                throw fail();
            return { phone_jid: phone, lid, generation: transport.generation };
        };
        const original = readIdentity();
        const close = () => {
            if (retired) return;
            retired = true;
            transport.close();
        };
        const check = () => {
            try {
                if (retired) throw fail();
                transport.check();
                const current = readIdentity();
                if (
                    current.phone_jid !== original.phone_jid ||
                    current.lid !== original.lid ||
                    storeApi.getSignalProtocolStore() !== store
                )
                    throw fail();
            } catch {
                close();
                throw fail();
            }
        };
        const decode = (value, allowEmpty = false) => {
            if (
                typeof value !== 'string' ||
                (!allowEmpty && !value.length) ||
                value.length > Math.ceil(limit / 3) * 4 ||
                !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
                    value,
                )
            )
                throw fail();
            const binary = globalThis.atob(value);
            if (binary.length > limit || globalThis.btoa(binary) !== value)
                throw fail();
            return Uint8Array.from(binary, (byte) => byte.charCodeAt(0));
        };
        const encode = (value, allowEmpty = false) => {
            // Copy borrowed provider buffers; never zero or transfer its store.
            if (
                !(value instanceof Uint8Array) &&
                !(value instanceof ArrayBuffer)
            )
                throw fail();
            const bytes = new Uint8Array(
                value instanceof ArrayBuffer ? value : value.buffer,
                value instanceof ArrayBuffer ? 0 : value.byteOffset,
                value.byteLength,
            );
            if ((!allowEmpty && !bytes.length) || bytes.length > limit)
                throw fail();
            let binary = '';
            for (let offset = 0; offset < bytes.length; offset += 8192)
                binary += String.fromCharCode(
                    ...bytes.subarray(offset, offset + 8192),
                );
            return globalThis.btoa(binary);
        };
        const run = async (id, operation) => {
            check();
            if (
                typeof id !== 'string' ||
                !/^[a-zA-Z0-9_.:-]{1,128}$/.test(id) ||
                pending.has(id) ||
                pending.size >= 128
            )
                throw fail();
            const state = { cancelled: false };
            pending.set(id, state);
            const guard = () => {
                check();
                if (state.cancelled) throw fail();
            };
            try {
                const result = await operation(guard);
                guard();
                return result;
            } catch {
                throw fail();
            } finally {
                pending.delete(id);
            }
        };
        const durable = async (guard, operation) => {
            // Once a native crypto operation has mutated the original ratchet,
            // finish its durability boundary even if the caller cancelled. Do
            // not report success, retry it, or reuse its result in a new epoch.
            let result;
            try {
                result = await operation();
            } finally {
                await store.flushBufferToDiskIfNotMemOnlyMode();
            }
            guard();
            return result;
        };
        const encryptSignal = (id, target, input) =>
            run(id, async (guard) => {
                const peer = wid(target);
                const bytes = decode(input);
                let result;
                try {
                    guard();
                    // Use the SAME deduplicated prekey job as ordinary messages,
                    // not a second prekey installer competing with their ratchet.
                    await durable(guard, () =>
                        sessions.ensureE2ESessions({
                            identityChanged: false,
                            sessionScope: scope,
                            wids: [peer],
                        }),
                    );
                    guard();
                    await durable(guard, async () => {
                        result = await cipher.encryptSignalProto(
                            peer,
                            bytes,
                            scope,
                            false,
                        );
                    });
                    if (result?.type !== 'msg' && result?.type !== 'pkmsg')
                        throw fail();
                    return {
                        type: result.type,
                        ciphertext: encode(result.ciphertext),
                    };
                } finally {
                    bytes.fill(0);
                    if (result?.ciphertext instanceof ArrayBuffer)
                        new Uint8Array(result.ciphertext).fill(0);
                }
            });
        const decryptSignal = (id, target, type, input, signedIdentity) =>
            run(id, async (guard) => {
                const peer = wid(target);
                if (type !== 'msg' && type !== 'pkmsg') throw fail();
                const bytes = decode(input);
                let identity, result;
                try {
                    guard();
                    if (
                        type === 'pkmsg' &&
                        peer.device != null &&
                        peer.device !== 0
                    ) {
                        identity = decode(signedIdentity);
                        const valid = await durable(guard, () =>
                            adv.validateADVwithEncs({
                                author: peer,
                                deviceIdentity: identity,
                                encs: [{ e2eType: type, ciphertext: bytes }],
                                offline: false,
                            }),
                        );
                        guard();
                        if (valid !== true) throw fail();
                    }
                    await durable(guard, async () => {
                        result = await cipher.decryptSignalProto(
                            peer,
                            type,
                            bytes,
                            false,
                            scope,
                        );
                    });
                    return encode(result);
                } finally {
                    bytes.fill(0);
                    identity?.fill(0);
                    if (result instanceof ArrayBuffer)
                        new Uint8Array(result).fill(0);
                }
            });
        const resolvePeerLid = (id, target) =>
            run(id, async (guard) => {
                const canonical =
                    typeof target === 'string' &&
                    /^\+?[0-9]{1,32}$/.test(target)
                        ? `${target.replace(/^\+/, '')}@s.whatsapp.net`
                        : target;
                const peer = userWid(canonical);
                if (canonical.endsWith('@lid')) return canonical;
                if (!canonical.endsWith('@s.whatsapp.net')) throw fail();
                const known = req('WAWebApiContact').getCurrentLid(peer);
                if (known) {
                    const value = nativeAddress(known);
                    if (!value.endsWith('@lid') || value.includes(':'))
                        throw fail();
                    return value;
                }
                const { USyncQuery } = req('WAWebUsync');
                const { USyncUser } = req('WAWebUsyncUser');
                guard();
                const result = await new USyncQuery()
                    .withLidProtocol()
                    .withUser(new USyncUser().withId(peer))
                    .execute();
                guard();
                if (
                    !result ||
                    Object.keys(result.error ?? {}).length ||
                    !Array.isArray(result.list) ||
                    result.list.length !== 1
                )
                    throw fail();
                const entry = result.list[0];
                if (
                    nativeAddress(entry.id) !== canonical ||
                    typeof entry.lid !== 'string' ||
                    !entry.lid.endsWith('@lid') ||
                    entry.lid.includes(':')
                )
                    throw fail();
                return address(entry.lid);
            });
        const getUserDevices = (id, users) =>
            run(id, async (guard) => {
                if (
                    !Array.isArray(users) ||
                    !users.length ||
                    users.length > 512
                )
                    throw fail();
                const peers = [...new Set(users)].map(userWid);
                guard();
                // Native ADV verification and cache persistence remain owned by
                // the authenticated channel. Never trust an unverified USync list.
                const lists = await req(
                    'WAWebAdvSyncDeviceListApi',
                ).syncAndGetDeviceList(peers);
                guard();
                if (!Array.isArray(lists) || lists.length !== peers.length)
                    throw fail();
                const devices = new Set();
                for (let i = 0; i < lists.length; i++) {
                    const list = lists[i];
                    if (
                        !list ||
                        !Array.isArray(list.devices) ||
                        !list.devices.length ||
                        nativeAddress(
                            factory.createUserWidFromDeviceListPk(list.id),
                        ) !== nativeAddress(peers[i])
                    )
                        throw fail();
                    for (const item of list.devices) {
                        if (
                            !Number.isInteger(item.id) ||
                            item.id < 0 ||
                            item.id > 255 ||
                            (item.isHosted !== undefined &&
                                typeof item.isHosted !== 'boolean')
                        )
                            throw fail();
                        devices.add(
                            nativeAddress(
                                factory.createDeviceWidFromDeviceListPk(
                                    list.id,
                                    item.id,
                                    item.isHosted,
                                ),
                            ),
                        );
                        if (devices.size > 512) throw fail();
                    }
                }
                return [...devices];
            });
        const deviceIdentity = (id) =>
            run(id, async (guard) => {
                const publicIdentity = await adv.getADVEncodedIdentity();
                guard();
                return encode(publicIdentity);
            });
        const privacyToken = (id, target) =>
            run(id, async (guard) => {
                const peer = userWid(target);
                const chat =
                    await req('WAWebApiChatCommon').getChatRecord(peer);
                guard();
                const orphan = await req(
                    'WAWebApiOrphanTcToken',
                ).getOrphanTcToken(peer);
                guard();
                const utils = req('WAWebTrustedContactsUtils');
                const candidate = [chat, orphan]
                    .filter(
                        (value) =>
                            value &&
                            Number.isFinite(value.tcTokenTimestamp) &&
                            value.tcTokenTimestamp > 0 &&
                            value.tcToken &&
                            !utils.isTokenExpired(
                                value.tcTokenTimestamp,
                                utils.TcTokenMode.Receiver,
                            ),
                    )
                    .sort((a, b) => b.tcTokenTimestamp - a.tcTokenTimestamp)[0];
                // Reading an absent/expired token must not issue a new one.
                return candidate ? encode(candidate.tcToken) : '';
            });
        const getGroupInfo = (id, target) =>
            run(id, async (guard) => {
                wid(target, true);
                const result = await req(
                    'WAWebMexFetchGroupInfoJob',
                ).mexGetGroupInfo({
                    groupId: target,
                    queryContext: 'interactive',
                });
                guard();
                const info = result?.groupInfo;
                if (
                    !info ||
                    nativeAddress(info.id, true) !== target ||
                    info.terminated ||
                    info.suspended ||
                    !Array.isArray(info.participants) ||
                    info.participants.length > 512
                )
                    throw fail();
                return {
                    participants: info.participants.map((item) => {
                        const jid = nativeAddress(item.id);
                        const phone = item.phoneNumber
                            ? nativeAddress(item.phoneNumber)
                            : undefined;
                        const lid = item.lid
                            ? nativeAddress(item.lid)
                            : undefined;
                        if (
                            jid.includes(':') ||
                            (phone &&
                                (!phone.endsWith('@s.whatsapp.net') ||
                                    phone.includes(':'))) ||
                            (lid &&
                                (!lid.endsWith('@lid') || lid.includes(':')))
                        )
                            throw fail();
                        return {
                            jid,
                            ...(phone ? { phone_jid: phone } : {}),
                            ...(lid ? { lid } : {}),
                        };
                    }),
                };
            });
        return Object.freeze({
            ...transport,
            close,
            check,
            identity: () => {
                check();
                return { ...original };
            },
            cancel: (id) => {
                const operation = pending.get(id);
                if (operation) operation.cancelled = true;
                else transport.cancel(id);
            },
            encryptSignal,
            decryptSignal,
            resolvePeerLid,
            getUserDevices,
            deviceIdentity,
            privacyToken,
            getGroupInfo,
        });
    } catch {
        retired = true;
        transport?.close();
        throw fail();
    }
}

module.exports = { attachOriginalChannelSession };