Signal-Desktop/ts/textsecure/ProvisioningCipher.ts

167 lines
5.1 KiB
TypeScript

// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import { PublicKey, Aci, Pni } from '@signalapp/libsignal-client';
import type { KeyPairType } from './Types.d';
import * as Bytes from '../Bytes';
import {
decryptAes256CbcPkcsPadding,
deriveSecrets,
verifyHmacSha256,
} from '../Crypto';
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
import { SignalService as Proto } from '../protobuf';
import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull';
import { normalizeAci } from '../util/normalizeAci';
import {
type AciString,
type PniString,
normalizePni,
toTaggedPni,
isUntaggedPniString,
fromAciObject,
fromPniObject,
} from '../types/ServiceId';
export type ProvisionDecryptResult = Readonly<{
aciKeyPair: KeyPairType;
pniKeyPair?: KeyPairType;
number?: string;
aci: AciString;
pni: PniString;
provisioningCode?: string;
userAgent?: string;
readReceipts?: boolean;
profileKey?: Uint8Array;
masterKey?: Uint8Array;
accountEntropyPool: string | undefined;
mediaRootBackupKey: Uint8Array | undefined;
ephemeralBackupKey: Uint8Array | undefined;
}>;
class ProvisioningCipherInner {
keyPair?: KeyPairType;
decrypt(provisionEnvelope: Proto.ProvisionEnvelope): ProvisionDecryptResult {
strictAssert(
provisionEnvelope.publicKey,
'Missing publicKey in ProvisionEnvelope'
);
strictAssert(provisionEnvelope.body, 'Missing body in ProvisionEnvelope');
const masterEphemeral = provisionEnvelope.publicKey;
const message = provisionEnvelope.body;
if (new Uint8Array(message)[0] !== 1) {
throw new Error('Bad version number on ProvisioningMessage');
}
const iv = message.subarray(1, 16 + 1);
const mac = message.subarray(message.byteLength - 32, message.byteLength);
const ivAndCiphertext = message.subarray(0, message.byteLength - 32);
const ciphertext = message.subarray(16 + 1, message.byteLength - 32);
if (!this.keyPair) {
throw new Error('ProvisioningCipher.decrypt: No keypair!');
}
const ecRes = calculateAgreement(
PublicKey.deserialize(Buffer.from(masterEphemeral)),
this.keyPair.privateKey
);
const keys = deriveSecrets(
ecRes,
new Uint8Array(32),
Bytes.fromString('TextSecure Provisioning Message')
);
verifyHmacSha256(ivAndCiphertext, keys[1], mac, 32);
const plaintext = decryptAes256CbcPkcsPadding(keys[0], ciphertext, iv);
const provisionMessage = Proto.ProvisionMessage.decode(plaintext);
const aciPrivKey = provisionMessage.aciIdentityKeyPrivate;
const pniPrivKey = provisionMessage.pniIdentityKeyPrivate;
strictAssert(aciPrivKey, 'Missing aciKeyPrivate in ProvisionMessage');
const aciKeyPair = createKeyPair(aciPrivKey);
const pniKeyPair = pniPrivKey?.length
? createKeyPair(pniPrivKey)
: undefined;
const {
aci: rawAci,
pni: rawUntaggedPni,
aciBinary,
pniBinary,
} = provisionMessage;
let aci: AciString;
let pni: PniString;
if (Bytes.isNotEmpty(aciBinary) && Bytes.isNotEmpty(pniBinary)) {
aci = fromAciObject(Aci.fromUuidBytes(aciBinary));
pni = fromPniObject(Pni.fromUuidBytes(pniBinary));
} else if (rawAci && rawUntaggedPni) {
strictAssert(
isUntaggedPniString(rawUntaggedPni),
'ProvisioningCipher: invalid untaggedPni'
);
aci = normalizeAci(rawAci, 'provisionMessage.aci');
pni = normalizePni(toTaggedPni(rawUntaggedPni), 'provisionMessage.pni');
} else {
throw new Error('Missing aci/pni in provisioning message');
}
return {
aciKeyPair,
pniKeyPair,
number: dropNull(provisionMessage.number),
aci,
pni,
provisioningCode: dropNull(provisionMessage.provisioningCode),
userAgent: dropNull(provisionMessage.userAgent),
readReceipts: provisionMessage.readReceipts ?? false,
profileKey: Bytes.isNotEmpty(provisionMessage.profileKey)
? provisionMessage.profileKey
: undefined,
masterKey: Bytes.isNotEmpty(provisionMessage.masterKey)
? provisionMessage.masterKey
: undefined,
ephemeralBackupKey: Bytes.isNotEmpty(provisionMessage.ephemeralBackupKey)
? provisionMessage.ephemeralBackupKey
: undefined,
mediaRootBackupKey: Bytes.isNotEmpty(provisionMessage.mediaRootBackupKey)
? provisionMessage.mediaRootBackupKey
: undefined,
accountEntropyPool: provisionMessage.accountEntropyPool || undefined,
};
}
getPublicKey(): PublicKey {
if (!this.keyPair) {
this.keyPair = generateKeyPair();
}
if (!this.keyPair) {
throw new Error('ProvisioningCipher.decrypt: No keypair!');
}
return this.keyPair.publicKey;
}
}
export default class ProvisioningCipher {
constructor() {
const inner = new ProvisioningCipherInner();
this.decrypt = inner.decrypt.bind(inner);
this.getPublicKey = inner.getPublicKey.bind(inner);
}
decrypt: (
provisionEnvelope: Proto.ProvisionEnvelope
) => ProvisionDecryptResult;
getPublicKey: () => PublicKey;
}