naruyaizumi
Version:
A WebSockets library for interacting with WhatsApp Web
179 lines (178 loc) • 6.89 kB
JavaScript
import { KEY_BUNDLE_TYPE } from "../Defaults/index.js";
import {
assertNodeErrorFree,
getBinaryNodeChild,
getBinaryNodeChildBuffer,
getBinaryNodeChildren,
getBinaryNodeChildUInt,
getServerFromDomainType,
jidDecode,
S_WHATSAPP_NET,
WAJIDDomains,
} from "../WABinary/index.js";
import { Curve, generateSignalPubKey } from "./crypto.js";
import { encodeBigEndian } from "./generics.js";
function chunk(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
export const createSignalIdentity = (wid, accountSignatureKey) => {
return {
identifier: { name: wid, deviceId: 0 },
identifierKey: generateSignalPubKey(accountSignatureKey),
};
};
export const getPreKeys = async ({ get }, min, limit) => {
const idList = [];
for (let id = min; id < limit; id++) {
idList.push(id.toString());
}
return get("pre-key", idList);
};
export const generateOrGetPreKeys = (creds, range) => {
const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId;
const remaining = range - avaliable;
const lastPreKeyId = creds.nextPreKeyId + remaining - 1;
const newPreKeys = {};
if (remaining > 0) {
for (let i = creds.nextPreKeyId; i <= lastPreKeyId; i++) {
newPreKeys[i] = Curve.generateKeyPair();
}
}
return {
newPreKeys,
lastPreKeyId,
preKeysRange: [creds.firstUnuploadedPreKeyId, range],
};
};
export const xmppSignedPreKey = (key) => ({
tag: "skey",
attrs: {},
content: [
{ tag: "id", attrs: {}, content: encodeBigEndian(key.keyId, 3) },
{ tag: "value", attrs: {}, content: key.keyPair.public },
{ tag: "signature", attrs: {}, content: key.signature },
],
});
export const xmppPreKey = (pair, id) => ({
tag: "key",
attrs: {},
content: [
{ tag: "id", attrs: {}, content: encodeBigEndian(id, 3) },
{ tag: "value", attrs: {}, content: pair.public },
],
});
export const parseAndInjectE2ESessions = async (node, repository) => {
const extractKey = (key) =>
key
? {
keyId: getBinaryNodeChildUInt(key, "id", 3),
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, "value")),
signature: getBinaryNodeChildBuffer(key, "signature"),
}
: undefined;
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, "list"), "user");
for (const node of nodes) {
assertNodeErrorFree(node);
}
// Most of the work in repository.injectE2ESession is CPU intensive, not IO
// So Promise.all doesn't really help here,
// but blocks even loop if we're using it inside keys.transaction, and it makes it "sync" actually
// This way we chunk it in smaller parts and between those parts we can yield to the event loop
// It's rare case when you need to E2E sessions for so many users, but it's possible
const chunkSize = 100;
const chunks = chunk(nodes, chunkSize);
for (const nodesChunk of chunks) {
for (const node of nodesChunk) {
const signedKey = getBinaryNodeChild(node, "skey");
const key = getBinaryNodeChild(node, "key");
const identity = getBinaryNodeChildBuffer(node, "identity");
const jid = node.attrs.jid;
const registrationId = getBinaryNodeChildUInt(node, "registration", 4);
await repository.injectE2ESession({
jid,
session: {
registrationId: registrationId,
identityKey: generateSignalPubKey(identity),
signedPreKey: extractKey(signedKey),
preKey: extractKey(key),
},
});
}
}
};
export const extractDeviceJids = (result, myJid, myLid, excludeZeroDevices) => {
const { user: myUser, device: myDevice } = jidDecode(myJid);
const extracted = [];
for (const userResult of result) {
const { devices, id } = userResult;
const decoded = jidDecode(id),
{ user, server } = decoded;
let { domainType } = decoded;
const deviceList = devices?.deviceList;
if (!Array.isArray(deviceList)) continue;
for (const { id: device, keyIndex, isHosted } of deviceList) {
if (
(!excludeZeroDevices || device !== 0) && // if zero devices are not-excluded, or device is non zero
((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) {
domainType =
domainType === WAJIDDomains.LID
? WAJIDDomains.HOSTED_LID
: WAJIDDomains.HOSTED;
}
extracted.push({
user,
device,
domainType,
server: getServerFromDomainType(server, domainType),
});
}
}
}
return extracted;
};
/**
* get the next N keys for upload or processing
* @param count number of pre-keys to get or generate
*/
export const getNextPreKeys = async ({ creds, keys }, count) => {
const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(creds, count);
const update = {
nextPreKeyId: Math.max(lastPreKeyId + 1, creds.nextPreKeyId),
firstUnuploadedPreKeyId: Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId + 1),
};
await keys.set({ "pre-key": newPreKeys });
const preKeys = await getPreKeys(keys, preKeysRange[0], preKeysRange[0] + preKeysRange[1]);
return { update, preKeys };
};
export const getNextPreKeysNode = async (state, count) => {
const { creds } = state;
const { update, preKeys } = await getNextPreKeys(state, count);
const node = {
tag: "iq",
attrs: {
xmlns: "encrypt",
type: "set",
to: S_WHATSAPP_NET,
},
content: [
{ tag: "registration", attrs: {}, content: encodeBigEndian(creds.registrationId) },
{ tag: "type", attrs: {}, content: KEY_BUNDLE_TYPE },
{ tag: "identity", attrs: {}, content: creds.signedIdentityKey.public },
{
tag: "list",
attrs: {},
content: Object.keys(preKeys).map((k) => xmppPreKey(preKeys[+k], +k)),
},
xmppSignedPreKey(creds.signedPreKey),
],
};
return { update, node };
};
//# sourceMappingURL=signal.js.map