@signalapp/mock-server
Version:
Mock Signal Server for writing tests
240 lines (239 loc) • 10.3 kB
JavaScript
"use strict";
// Copyright 2022 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.encryptProvisionMessage = encryptProvisionMessage;
exports.encryptAttachment = encryptAttachment;
exports.generateServerCertificate = generateServerCertificate;
exports.generateSenderCertificate = generateSenderCertificate;
exports.deriveAccessKey = deriveAccessKey;
exports.deriveMasterKey = deriveMasterKey;
exports.deriveStorageKey = deriveStorageKey;
exports.deriveStorageItemKey = deriveStorageItemKey;
exports.decryptStorageManifest = decryptStorageManifest;
exports.encryptStorageManifest = encryptStorageManifest;
exports.decryptStorageItem = decryptStorageItem;
exports.encryptStorageItem = encryptStorageItem;
exports.encryptProfileName = encryptProfileName;
exports.generateAccessKeyVerifier = generateAccessKeyVerifier;
exports.decodePreKey = decodePreKey;
exports.decodeSignedPreKey = decodeSignedPreKey;
exports.decodeKyberPreKey = decodeKyberPreKey;
exports.hashRemoteConfig = hashRemoteConfig;
const crypto_1 = __importDefault(require("crypto"));
const buffer_1 = require("buffer");
const libsignal_client_1 = require("@signalapp/libsignal-client");
const compiled_1 = require("../protos/compiled");
const constants_1 = require("./constants");
const AES_KEY_SIZE = 32;
const MAC_KEY_SIZE = 32;
const AESGCM_IV_SIZE = 12;
const AUTH_TAG_SIZE = 16;
const MASTER_KEY_SIZE = 32;
function encryptProvisionMessage(data, remotePubKey) {
const privateKey = libsignal_client_1.PrivateKey.generate();
const publicKey = privateKey.getPublicKey();
const agreement = privateKey.agree(remotePubKey);
const secrets = (0, libsignal_client_1.hkdf)(AES_KEY_SIZE + MAC_KEY_SIZE, agreement, buffer_1.Buffer.from('TextSecure Provisioning Message'), null);
const aesKey = secrets.slice(0, AES_KEY_SIZE);
const macKey = secrets.slice(AES_KEY_SIZE);
const iv = crypto_1.default.randomBytes(16);
const cipher = crypto_1.default.createCipheriv('aes-256-cbc', aesKey, iv);
const encrypted = buffer_1.Buffer.concat([cipher.update(data), cipher.final()]);
const version = buffer_1.Buffer.from([1]);
const ciphertext = buffer_1.Buffer.concat([version, iv, encrypted]);
const mac = crypto_1.default.createHmac('sha256', macKey).update(ciphertext).digest();
const body = buffer_1.Buffer.concat([ciphertext, mac]);
return {
body,
ephemeralKey: buffer_1.Buffer.from(publicKey.serialize()),
};
}
function encryptAttachment(cleartext, { aesKey, macKey, iv } = {
aesKey: crypto_1.default.randomBytes(32),
macKey: crypto_1.default.randomBytes(32),
iv: crypto_1.default.randomBytes(16),
}) {
const cipher = crypto_1.default.createCipheriv('aes-256-cbc', aesKey, iv);
const ciphertext = buffer_1.Buffer.concat([cipher.update(cleartext), cipher.final()]);
const mac = crypto_1.default
.createHmac('sha256', macKey)
.update(iv)
.update(ciphertext)
.digest();
const key = buffer_1.Buffer.concat([aesKey, macKey]);
const blob = buffer_1.Buffer.concat([iv, ciphertext, mac]);
const digest = crypto_1.default.createHash('sha256').update(blob).digest();
return {
key,
blob,
digest,
size: cleartext.length,
};
}
function generateServerCertificate(rootKey) {
const privateKey = libsignal_client_1.PrivateKey.generate();
const data = buffer_1.Buffer.from(compiled_1.signalservice.ServerCertificate.Certificate.encode({
id: constants_1.SERVER_CERTIFICATE_ID,
key: privateKey.getPublicKey().serialize(),
}));
const signature = rootKey.sign(data);
const certificate = {
certificate: data,
signature,
};
return {
privateKey,
certificate,
};
}
function generateSenderCertificate(serverCert, sender) {
const data = buffer_1.Buffer.from(compiled_1.signalservice.SenderCertificate.Certificate.encode({
senderE164: sender.number ?? null,
senderUuid: sender.aci,
senderDevice: sender.deviceId,
expires: BigInt(sender.expires ?? constants_1.NEVER_EXPIRES),
identityKey: sender.identityKey.serialize(),
signer: serverCert.certificate,
}));
const signature = serverCert.privateKey.sign(data);
const certificate = buffer_1.Buffer.from(compiled_1.signalservice.SenderCertificate.encode({
certificate: data,
signature,
}));
return libsignal_client_1.SenderCertificate.deserialize(certificate);
}
function deriveAccessKey(profileKey) {
const cipher = crypto_1.default.createCipheriv('aes-256-gcm', profileKey, buffer_1.Buffer.alloc(12));
return buffer_1.Buffer.concat([cipher.update(buffer_1.Buffer.alloc(16)), cipher.final()]);
}
function deriveMasterKey(accountEntropyPool) {
return buffer_1.Buffer.from((0, libsignal_client_1.hkdf)(MASTER_KEY_SIZE, buffer_1.Buffer.from(accountEntropyPool), buffer_1.Buffer.from('20240801_SIGNAL_SVR_MASTER_KEY'), null));
}
function deriveStorageKey(masterKey) {
const hash = crypto_1.default.createHmac('sha256', masterKey);
hash.update('Storage Service Encryption');
return hash.digest();
}
function deriveStorageManifestKey(storageKey, version) {
const hash = crypto_1.default.createHmac('sha256', storageKey);
hash.update(`Manifest_${version.toString()}`);
return hash.digest();
}
const STORAGE_SERVICE_ITEM_KEY_INFO_PREFIX = '20240801_SIGNAL_STORAGE_SERVICE_ITEM_';
const STORAGE_SERVICE_ITEM_KEY_LEN = 32;
function deriveStorageItemKey({ storageKey, recordIkm, key, }) {
if (recordIkm === undefined) {
const hash = crypto_1.default.createHmac('sha256', storageKey);
hash.update(`Item_${key.toString('base64')}`);
return hash.digest();
}
return buffer_1.Buffer.from((0, libsignal_client_1.hkdf)(STORAGE_SERVICE_ITEM_KEY_LEN, recordIkm, buffer_1.Buffer.concat([buffer_1.Buffer.from(STORAGE_SERVICE_ITEM_KEY_INFO_PREFIX), key]), buffer_1.Buffer.alloc(0)));
}
function decryptAESGCM(ciphertext, key) {
const iv = ciphertext.subarray(0, AESGCM_IV_SIZE);
const tag = ciphertext.subarray(ciphertext.length - AUTH_TAG_SIZE);
const rest = ciphertext.subarray(iv.length, ciphertext.length - tag.length);
const decipher = crypto_1.default.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
return buffer_1.Buffer.concat([decipher.update(rest), decipher.final()]);
}
function encryptAESGCM(plaintext, key) {
const iv = crypto_1.default.randomBytes(AESGCM_IV_SIZE);
const cipher = crypto_1.default.createCipheriv('aes-256-gcm', key, iv);
const ciphertext = [cipher.update(plaintext), cipher.final()];
const tag = cipher.getAuthTag();
return buffer_1.Buffer.concat([iv, ...ciphertext, tag]);
}
function decryptStorageManifest(storageKey, manifest) {
if (!manifest.value?.length) {
throw new Error('Missing manifest.value');
}
const manifestKey = deriveStorageManifestKey(storageKey, manifest.version ?? 0n);
const decoded = compiled_1.signalservice.ManifestRecord.decode(decryptAESGCM(buffer_1.Buffer.from(manifest.value), manifestKey));
if (decoded.version !== manifest.version) {
throw new Error('manifestRecord.version != manifest.version');
}
return decoded;
}
function encryptStorageManifest(storageKey, manifestRecord) {
if (!manifestRecord.version) {
throw new Error('Missing manifest.version');
}
const manifestKey = deriveStorageManifestKey(storageKey, manifestRecord.version);
const encrypted = encryptAESGCM(buffer_1.Buffer.from(compiled_1.signalservice.ManifestRecord.encode(manifestRecord)), manifestKey);
return {
version: manifestRecord.version,
value: encrypted,
};
}
function decryptStorageItem({ storageKey, recordIkm, item, }) {
if (!item.key) {
throw new Error('Missing item.key');
}
if (!item.value) {
throw new Error('Missing item.value');
}
const itemKey = deriveStorageItemKey({
storageKey,
recordIkm,
key: buffer_1.Buffer.from(item.key),
});
return compiled_1.signalservice.StorageRecord.decode(decryptAESGCM(buffer_1.Buffer.from(item.value), itemKey));
}
function encryptStorageItem({ storageKey, key, recordIkm, record, }) {
const itemKey = deriveStorageItemKey({
storageKey,
recordIkm,
key,
});
const encrypted = encryptAESGCM(buffer_1.Buffer.from(compiled_1.signalservice.StorageRecord.encode(record)), itemKey);
return {
key,
value: encrypted,
};
}
function encryptProfileName(profileKey, name) {
const encrypted = encryptAESGCM(buffer_1.Buffer.from(name), profileKey);
return encrypted;
}
function generateAccessKeyVerifier(accessKey) {
const zeroes = buffer_1.Buffer.alloc(32);
return crypto_1.default.createHmac('sha256', accessKey).update(zeroes).digest();
}
function decodePreKey({ keyId, publicKey }) {
return {
keyId,
publicKey: libsignal_client_1.PublicKey.deserialize(buffer_1.Buffer.from(publicKey, 'base64')),
};
}
function decodeSignedPreKey({ keyId, publicKey, signature, }) {
return {
keyId,
publicKey: libsignal_client_1.PublicKey.deserialize(buffer_1.Buffer.from(publicKey, 'base64')),
signature: buffer_1.Buffer.from(signature, 'base64'),
};
}
function decodeKyberPreKey({ keyId, publicKey, signature, }) {
return {
keyId,
publicKey: libsignal_client_1.KEMPublicKey.deserialize(buffer_1.Buffer.from(publicKey, 'base64')),
signature: buffer_1.Buffer.from(signature, 'base64'),
};
}
function hashRemoteConfig(config) {
// Not necessarily secure, but this will let us detect changes. The exact
// format isn't important so long as it's deterministic.
const mac = crypto_1.default.createHmac('sha256', 'remoteConfig');
return config
.reduce((mac, [name, value], index) => mac
.update(index.toString())
.update(name.length.toString())
.update(name)
.update(value.length.toString())
.update(value), mac)
.digest();
}