'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 };