UNPKG

@signalapp/mock-server

Version:
186 lines (185 loc) 7.19 kB
"use strict"; // Copyright 2026 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CALLING_SERVICE_SECRET = exports.CallingErrorCodesToHttpStatus = exports.CallingError = exports.CallingErrorCode = exports.CallingPublicKeySchema = exports.CallLinkRestrictions = exports.CallType = void 0; exports.getRandomHexString = getRandomHexString; exports.getRandomCallingEraId = getRandomCallingEraId; exports.getRandomCallingDemuxId = getRandomCallingDemuxId; exports.decodeCallingPublicKey = decodeCallingPublicKey; exports.encodeCallingPublicKey = encodeCallingPublicKey; exports.parseCallingAuthHeader = parseCallingAuthHeader; exports.verifyCallingAuthToken = verifyCallingAuthToken; exports.generateCallingAuthToken = generateCallingAuthToken; const node_crypto_1 = require("node:crypto"); const zod_1 = __importDefault(require("zod")); const util_1 = require("./util"); const crypto_1 = require("./sfu/crypto"); const UINT32_MAX = 4_294_967_296; // 2 ** 32 − 1 function getRandomUint32() { const minInclusive = 0; const maxExclusive = UINT32_MAX + 1; return (0, node_crypto_1.randomInt)(minInclusive, maxExclusive); } function getRandomHexString(size) { return (0, node_crypto_1.randomBytes)(size).toString('hex'); } function getRandomCallingEraId() { return getRandomHexString(16); } function getRandomCallingDemuxId() { return ((getRandomUint32() & ~0b1111) >>> 0); } var CallType; (function (CallType) { CallType[CallType["Group"] = 0] = "Group"; CallType[CallType["Adhoc"] = 1] = "Adhoc"; })(CallType || (exports.CallType = CallType = {})); var CallLinkRestrictions; (function (CallLinkRestrictions) { CallLinkRestrictions[CallLinkRestrictions["AdminApproval"] = 0] = "AdminApproval"; CallLinkRestrictions[CallLinkRestrictions["None"] = 1] = "None"; })(CallLinkRestrictions || (exports.CallLinkRestrictions = CallLinkRestrictions = {})); /** * Http * ---------------------------------------------------------------------------- */ exports.CallingPublicKeySchema = zod_1.default .string() .nonempty() .transform((input) => { return input; }); function decodeCallingPublicKey(input) { return crypto_1.CallingPublicKey.fromBytes(Buffer.from(input, 'hex')); } function encodeCallingPublicKey(key) { return Buffer.from(key.toBytes()).toString('hex'); } /** * Errors * ---------------------------------------------------------------------------- */ var CallingErrorCode; (function (CallingErrorCode) { CallingErrorCode[CallingErrorCode["AuthError"] = 0] = "AuthError"; CallingErrorCode[CallingErrorCode["CallNotFound"] = 1] = "CallNotFound"; CallingErrorCode[CallingErrorCode["TooManyClients"] = 2] = "TooManyClients"; CallingErrorCode[CallingErrorCode["NoPermissionToCreateCall"] = 3] = "NoPermissionToCreateCall"; CallingErrorCode[CallingErrorCode["DuplicateDemuxIdDetected"] = 4] = "DuplicateDemuxIdDetected"; CallingErrorCode[CallingErrorCode["InternalError"] = 5] = "InternalError"; })(CallingErrorCode || (exports.CallingErrorCode = CallingErrorCode = {})); class CallingError extends Error { #code; constructor(code, message) { super(message); this.#code = code; } get code() { return this.#code; } } exports.CallingError = CallingError; exports.CallingErrorCodesToHttpStatus = { [CallingErrorCode.AuthError]: 401, [CallingErrorCode.CallNotFound]: 404, [CallingErrorCode.TooManyClients]: 413, [CallingErrorCode.NoPermissionToCreateCall]: 500, [CallingErrorCode.DuplicateDemuxIdDetected]: 500, [CallingErrorCode.InternalError]: 500, }; /** * Auth * ---------------------------------------------------------------------------- */ exports.CALLING_SERVICE_SECRET = (0, node_crypto_1.randomBytes)(32); function parseNumberSafe(value) { const trimmed = value.trim(); if (trimmed === '') { return null; } const parsed = Number(value); if (!Number.isFinite(parsed)) { return null; } return parsed; } function parseCallingAuthHeader(authHeader) { const { error, password } = (0, util_1.parseAuthHeader)(authHeader); if (error != null) { throw new CallingError(CallingErrorCode.AuthError, error); } const [version = '', userIdStr = '', groupIdHex = '', timeUnixSecsStr = '', permissionStr = '', macDigestHex = '',] = password.split(':'); if (version !== '2') { throw new CallingError(CallingErrorCode.AuthError, 'Unsupported call auth signature'); } const userId = userIdStr; if (userId.length === 0) { throw new CallingError(CallingErrorCode.AuthError, 'Missing userId'); } const groupId = Buffer.from(groupIdHex, 'hex').toString(); if (groupId.length === 0) { throw new CallingError(CallingErrorCode.AuthError, 'Missing groupId'); } const timeUnixSecs = parseNumberSafe(timeUnixSecsStr); if (timeUnixSecs == null) { throw new CallingError(CallingErrorCode.AuthError, 'Time not encoded correctly'); } const time = timeUnixSecs * 1000; const isAllowedToInitiateGroupCall = permissionStr === '1'; const macDigest = Buffer.from(macDigestHex, 'hex'); if (macDigest.length === 0) { throw new CallingError(CallingErrorCode.AuthError, 'Missing macDigest'); } const macCiphertext = password.slice(0, password.length - macDigestHex.length - 1); const token = { userId, groupId, time, isAllowedToInitiateGroupCall, macCiphertext, macDigest, }; return token; } const GV2_AUTH_MATCH_LIMIT = 10; const GV2_AUTH_MAX_HEADER_AGE = 30 * 60 * 60 * 1000; function verifyCallingAuthToken(token, key) { const expectedDigest = (0, node_crypto_1.createHmac)('sha256', key) .update(token.macCiphertext) .digest() .subarray(0, GV2_AUTH_MATCH_LIMIT); if (!(0, node_crypto_1.timingSafeEqual)(expectedDigest, token.macDigest)) { throw new CallingError(CallingErrorCode.AuthError, 'Incorrect hmac digest'); } if (Date.now() > token.time + GV2_AUTH_MAX_HEADER_AGE) { throw new CallingError(CallingErrorCode.AuthError, 'Expired credentials'); } const auth = { userId: token.userId, roomId: token.groupId, isAllowedToInitiateGroupCall: token.isAllowedToInitiateGroupCall, }; return auth; } function generateCallingAuthToken(options) { let data = ''; data += '2'; data += ':'; data += (0, node_crypto_1.createHash)('sha256').update(options.userId).digest('hex'); data += ':'; data += Buffer.from(options.groupId).toString('hex'); data += ':'; data += `${Math.trunc(Date.now() / 1000)}`; data += ':'; data += options.isAllowedToInitiateGroupCall ? '1' : '0'; const hmac = (0, node_crypto_1.createHmac)('sha256', options.key) .update(data) .digest() .subarray(0, GV2_AUTH_MATCH_LIMIT) .toString('hex'); return `${data}:${hmac}`; }