@signalapp/mock-server
Version:
Mock Signal Server for writing tests
186 lines (185 loc) • 7.19 kB
JavaScript
;
// 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}`;
}