@signalapp/mock-server
Version:
Mock Signal Server for writing tests
237 lines (236 loc) • 10.6 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.decodeKyberPreKey = exports.decodeSignedPreKey = exports.decodePreKey = exports.generateAccessKeyVerifier = exports.encryptProfileName = exports.encryptStorageItem = exports.decryptStorageItem = exports.encryptStorageManifest = exports.decryptStorageManifest = exports.deriveStorageItemKey = exports.deriveStorageKey = exports.deriveMasterKey = exports.deriveAccessKey = exports.generateSenderCertificate = exports.generateServerCertificate = exports.encryptAttachment = exports.encryptProvisionMessage = void 0;
const crypto_1 = __importDefault(require("crypto"));
const buffer_1 = require("buffer");
const long_1 = __importDefault(require("long"));
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 hkdf = libsignal_client_1.HKDF.new(3);
const secrets = hkdf.deriveSecrets(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: publicKey.serialize(),
};
}
exports.encryptProvisionMessage = encryptProvisionMessage;
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,
};
}
exports.encryptAttachment = encryptAttachment;
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(),
}).finish());
const signature = rootKey.sign(data);
const certificate = {
certificate: data,
signature,
};
return {
privateKey,
certificate,
};
}
exports.generateServerCertificate = generateServerCertificate;
function generateSenderCertificate(serverCert, sender) {
const data = buffer_1.Buffer.from(compiled_1.signalservice.SenderCertificate.Certificate.encode({
senderE164: sender.number,
senderUuid: sender.aci,
senderDevice: sender.deviceId,
expires: long_1.default.fromNumber(sender.expires || constants_1.NEVER_EXPIRES),
identityKey: sender.identityKey.serialize(),
signer: serverCert.certificate,
}).finish());
const signature = serverCert.privateKey.sign(data);
const certificate = buffer_1.Buffer.from(compiled_1.signalservice.SenderCertificate.encode({
certificate: data,
signature,
}).finish());
return libsignal_client_1.SenderCertificate.deserialize(certificate);
}
exports.generateSenderCertificate = generateSenderCertificate;
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()]);
}
exports.deriveAccessKey = deriveAccessKey;
function deriveMasterKey(accountEntropyPool) {
const hkdf = libsignal_client_1.HKDF.new(3);
return hkdf.deriveSecrets(MASTER_KEY_SIZE, buffer_1.Buffer.from(accountEntropyPool), buffer_1.Buffer.from('20240801_SIGNAL_SVR_MASTER_KEY'), null);
}
exports.deriveMasterKey = deriveMasterKey;
function deriveStorageKey(masterKey) {
const hash = crypto_1.default.createHmac('sha256', masterKey);
hash.update('Storage Service Encryption');
return hash.digest();
}
exports.deriveStorageKey = deriveStorageKey;
function deriveStorageManifestKey(storageKey, version) {
const hash = crypto_1.default.createHmac('sha256', storageKey);
hash.update(`Manifest_${version}`);
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();
}
const hkdf = libsignal_client_1.HKDF.new(3);
return hkdf.deriveSecrets(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));
}
exports.deriveStorageItemKey = deriveStorageItemKey;
function decryptAESGCM(ciphertext, key) {
const iv = ciphertext.slice(0, AESGCM_IV_SIZE);
const tag = ciphertext.slice(ciphertext.length - AUTH_TAG_SIZE);
const rest = ciphertext.slice(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.version) {
throw new Error('Missing manifest.version');
}
if (!manifest.value) {
throw new Error('Missing manifest.value');
}
const manifestKey = deriveStorageManifestKey(storageKey, manifest.version);
const decoded = compiled_1.signalservice.ManifestRecord.decode(decryptAESGCM(buffer_1.Buffer.from(manifest.value), manifestKey));
if (!decoded.version) {
throw new Error('Missing manifestRecord.version');
}
if (!decoded.version.eq(manifest.version)) {
throw new Error('manifestRecord.version != manifest.version');
}
return decoded;
}
exports.decryptStorageManifest = decryptStorageManifest;
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).finish()), manifestKey);
return {
version: manifestRecord.version,
value: encrypted,
};
}
exports.encryptStorageManifest = encryptStorageManifest;
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));
}
exports.decryptStorageItem = decryptStorageItem;
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).finish()), itemKey);
return {
key,
value: encrypted,
};
}
exports.encryptStorageItem = encryptStorageItem;
function encryptProfileName(profileKey, name) {
const encrypted = encryptAESGCM(buffer_1.Buffer.from(name), profileKey);
return encrypted;
}
exports.encryptProfileName = encryptProfileName;
function generateAccessKeyVerifier(accessKey) {
const zeroes = buffer_1.Buffer.alloc(32);
return crypto_1.default.createHmac('sha256', accessKey).update(zeroes).digest();
}
exports.generateAccessKeyVerifier = generateAccessKeyVerifier;
function decodePreKey({ keyId, publicKey }) {
return {
keyId,
publicKey: libsignal_client_1.PublicKey.deserialize(buffer_1.Buffer.from(publicKey, 'base64')),
};
}
exports.decodePreKey = decodePreKey;
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'),
};
}
exports.decodeSignedPreKey = decodeSignedPreKey;
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'),
};
}
exports.decodeKyberPreKey = decodeKyberPreKey;