2021-09-16 16:28:29 +00:00
|
|
|
// Copyright 2020-2021 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-07-13 18:54:53 +00:00
|
|
|
import { Buffer } from 'buffer';
|
2020-09-04 01:25:19 +00:00
|
|
|
import pProps from 'p-props';
|
2020-11-13 19:57:55 +00:00
|
|
|
import { chunk } from 'lodash';
|
2021-07-13 18:54:53 +00:00
|
|
|
import Long from 'long';
|
2021-05-14 01:18:43 +00:00
|
|
|
import { HKDF } from '@signalapp/signal-client';
|
2021-07-13 18:54:53 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
import * as Bytes from './Bytes';
|
2021-04-16 23:13:13 +00:00
|
|
|
import { calculateAgreement, generateKeyPair } from './Curve';
|
2021-09-17 18:27:53 +00:00
|
|
|
import * as log from './logging/log';
|
2021-09-24 00:49:05 +00:00
|
|
|
import { HashType, CipherType } from './types/Crypto';
|
|
|
|
import { ProfileDecryptError } from './types/errors';
|
2020-09-04 01:25:19 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export { HashType, CipherType };
|
|
|
|
|
|
|
|
const PROFILE_IV_LENGTH = 12; // bytes
|
|
|
|
const PROFILE_KEY_LENGTH = 32; // bytes
|
|
|
|
|
|
|
|
// bytes
|
|
|
|
export const PaddedLengths = {
|
|
|
|
Name: [53, 257],
|
|
|
|
About: [128, 254, 512],
|
|
|
|
AboutEmoji: [32],
|
|
|
|
PaymentAddress: [554],
|
|
|
|
};
|
|
|
|
|
|
|
|
export type EncryptedAttachment = {
|
|
|
|
ciphertext: Uint8Array;
|
|
|
|
digest: Uint8Array;
|
|
|
|
};
|
2021-04-02 22:33:07 +00:00
|
|
|
|
2021-04-16 23:13:13 +00:00
|
|
|
// Generate a number between zero and 16383
|
|
|
|
export function generateRegistrationId(): number {
|
|
|
|
const id = new Uint16Array(getRandomBytes(2))[0];
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-bitwise
|
|
|
|
return id & 0x3fff;
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function deriveStickerPackKey(packKey: Uint8Array): Uint8Array {
|
2019-05-16 22:32:11 +00:00
|
|
|
const salt = getZeroes(32);
|
2021-09-24 00:49:05 +00:00
|
|
|
const info = Bytes.fromString('Sticker Pack');
|
2019-05-16 22:32:11 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const [part1, part2] = deriveSecrets(packKey, salt, info);
|
2019-05-16 22:32:11 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
return Bytes.concatenate([part1, part2]);
|
2019-05-16 22:32:11 +00:00
|
|
|
}
|
2018-12-13 21:41:42 +00:00
|
|
|
|
2021-04-16 23:13:13 +00:00
|
|
|
export function deriveSecrets(
|
2021-09-24 00:49:05 +00:00
|
|
|
input: Uint8Array,
|
|
|
|
salt: Uint8Array,
|
|
|
|
info: Uint8Array
|
|
|
|
): [Uint8Array, Uint8Array, Uint8Array] {
|
2021-04-16 23:13:13 +00:00
|
|
|
const hkdf = HKDF.new(3);
|
|
|
|
const output = hkdf.deriveSecrets(
|
|
|
|
3 * 32,
|
|
|
|
Buffer.from(input),
|
|
|
|
Buffer.from(info),
|
|
|
|
Buffer.from(salt)
|
|
|
|
);
|
2021-09-24 00:49:05 +00:00
|
|
|
return [output.slice(0, 32), output.slice(32, 64), output.slice(64, 96)];
|
2021-04-16 23:13:13 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function deriveMasterKeyFromGroupV1(groupV1Id: Uint8Array): Uint8Array {
|
2020-11-20 17:30:45 +00:00
|
|
|
const salt = getZeroes(32);
|
2021-09-24 00:49:05 +00:00
|
|
|
const info = Bytes.fromString('GV2 Migration');
|
2020-11-20 17:30:45 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const [part1] = deriveSecrets(groupV1Id, salt, info);
|
2020-11-20 17:30:45 +00:00
|
|
|
|
|
|
|
return part1;
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function computeHash(data: Uint8Array): string {
|
|
|
|
return Bytes.toBase64(hash(HashType.size512, data));
|
2020-09-09 02:25:05 +00:00
|
|
|
}
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
// High-level Operations
|
|
|
|
|
2021-07-13 18:54:53 +00:00
|
|
|
export type EncryptedDeviceName = {
|
2021-09-24 00:49:05 +00:00
|
|
|
ephemeralPublic: Uint8Array;
|
|
|
|
syntheticIv: Uint8Array;
|
|
|
|
ciphertext: Uint8Array;
|
2021-07-13 18:54:53 +00:00
|
|
|
};
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function encryptDeviceName(
|
2020-03-31 20:03:38 +00:00
|
|
|
deviceName: string,
|
2021-09-24 00:49:05 +00:00
|
|
|
identityPublic: Uint8Array
|
|
|
|
): EncryptedDeviceName {
|
|
|
|
const plaintext = Bytes.fromString(deviceName);
|
2021-04-16 23:13:13 +00:00
|
|
|
const ephemeralKeyPair = generateKeyPair();
|
|
|
|
const masterSecret = calculateAgreement(
|
2018-12-13 19:12:33 +00:00
|
|
|
identityPublic,
|
|
|
|
ephemeralKeyPair.privKey
|
|
|
|
);
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const key1 = hmacSha256(masterSecret, Bytes.fromString('auth'));
|
|
|
|
const syntheticIv = getFirstBytes(hmacSha256(key1, plaintext), 16);
|
2018-12-13 19:12:33 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const key2 = hmacSha256(masterSecret, Bytes.fromString('cipher'));
|
|
|
|
const cipherKey = hmacSha256(key2, syntheticIv);
|
2018-12-13 19:12:33 +00:00
|
|
|
|
|
|
|
const counter = getZeroes(16);
|
2021-09-24 00:49:05 +00:00
|
|
|
const ciphertext = encryptAesCtr(cipherKey, plaintext, counter);
|
2018-12-13 19:12:33 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
ephemeralPublic: ephemeralKeyPair.pubKey,
|
|
|
|
syntheticIv,
|
|
|
|
ciphertext,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function decryptDeviceName(
|
2021-07-13 18:54:53 +00:00
|
|
|
{ ephemeralPublic, syntheticIv, ciphertext }: EncryptedDeviceName,
|
2021-09-24 00:49:05 +00:00
|
|
|
identityPrivate: Uint8Array
|
|
|
|
): string {
|
2021-04-16 23:13:13 +00:00
|
|
|
const masterSecret = calculateAgreement(ephemeralPublic, identityPrivate);
|
2018-12-13 19:12:33 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const key2 = hmacSha256(masterSecret, Bytes.fromString('cipher'));
|
|
|
|
const cipherKey = hmacSha256(key2, syntheticIv);
|
2018-12-13 19:12:33 +00:00
|
|
|
|
|
|
|
const counter = getZeroes(16);
|
2021-09-24 00:49:05 +00:00
|
|
|
const plaintext = decryptAesCtr(cipherKey, ciphertext, counter);
|
2018-12-13 19:12:33 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const key1 = hmacSha256(masterSecret, Bytes.fromString('auth'));
|
|
|
|
const ourSyntheticIv = getFirstBytes(hmacSha256(key1, plaintext), 16);
|
2018-12-13 19:12:33 +00:00
|
|
|
|
|
|
|
if (!constantTimeEqual(ourSyntheticIv, syntheticIv)) {
|
|
|
|
throw new Error('decryptDeviceName: synthetic IV did not match');
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
return Bytes.toString(plaintext);
|
2018-12-13 19:12:33 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function deriveStorageManifestKey(
|
|
|
|
storageServiceKey: Uint8Array,
|
2020-07-07 00:56:56 +00:00
|
|
|
version: number
|
2021-09-24 00:49:05 +00:00
|
|
|
): Uint8Array {
|
|
|
|
return hmacSha256(storageServiceKey, Bytes.fromString(`Manifest_${version}`));
|
2020-07-07 00:56:56 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function deriveStorageItemKey(
|
|
|
|
storageServiceKey: Uint8Array,
|
2020-07-07 00:56:56 +00:00
|
|
|
itemID: string
|
2021-09-24 00:49:05 +00:00
|
|
|
): Uint8Array {
|
|
|
|
return hmacSha256(storageServiceKey, Bytes.fromString(`Item_${itemID}`));
|
2020-07-07 00:56:56 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function deriveAccessKey(profileKey: Uint8Array): Uint8Array {
|
2018-10-18 01:01:21 +00:00
|
|
|
const iv = getZeroes(12);
|
|
|
|
const plaintext = getZeroes(16);
|
2021-09-24 00:49:05 +00:00
|
|
|
const accessKey = encryptAesGcm(profileKey, iv, plaintext);
|
2020-03-31 20:03:38 +00:00
|
|
|
|
2019-05-08 20:14:52 +00:00
|
|
|
return getFirstBytes(accessKey, 16);
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function getAccessKeyVerifier(accessKey: Uint8Array): Uint8Array {
|
2018-10-18 01:01:21 +00:00
|
|
|
const plaintext = getZeroes(32);
|
|
|
|
|
2020-03-31 20:03:38 +00:00
|
|
|
return hmacSha256(accessKey, plaintext);
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function verifyAccessKey(
|
|
|
|
accessKey: Uint8Array,
|
|
|
|
theirVerifier: Uint8Array
|
|
|
|
): boolean {
|
|
|
|
const ourVerifier = getAccessKeyVerifier(accessKey);
|
2018-10-18 01:01:21 +00:00
|
|
|
|
|
|
|
if (constantTimeEqual(ourVerifier, theirVerifier)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-03-19 19:42:12 +00:00
|
|
|
const IV_LENGTH = 16;
|
|
|
|
const MAC_LENGTH = 16;
|
|
|
|
const NONCE_LENGTH = 16;
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function encryptSymmetric(
|
|
|
|
key: Uint8Array,
|
|
|
|
plaintext: Uint8Array
|
|
|
|
): Uint8Array {
|
2018-10-18 01:01:21 +00:00
|
|
|
const iv = getZeroes(IV_LENGTH);
|
|
|
|
const nonce = getRandomBytes(NONCE_LENGTH);
|
2018-03-19 19:42:12 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const cipherKey = hmacSha256(key, nonce);
|
|
|
|
const macKey = hmacSha256(key, cipherKey);
|
2018-03-19 19:42:12 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const ciphertext = encryptAes256CbcPkcsPadding(cipherKey, plaintext, iv);
|
|
|
|
const mac = getFirstBytes(hmacSha256(macKey, ciphertext), MAC_LENGTH);
|
2018-03-19 19:42:12 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
return Bytes.concatenate([nonce, ciphertext, mac]);
|
2018-03-19 19:42:12 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function decryptSymmetric(
|
|
|
|
key: Uint8Array,
|
|
|
|
data: Uint8Array
|
|
|
|
): Uint8Array {
|
2018-10-18 01:01:21 +00:00
|
|
|
const iv = getZeroes(IV_LENGTH);
|
2018-03-19 19:42:12 +00:00
|
|
|
|
2019-05-08 20:14:52 +00:00
|
|
|
const nonce = getFirstBytes(data, NONCE_LENGTH);
|
2021-04-16 23:13:13 +00:00
|
|
|
const ciphertext = getBytes(
|
2018-03-19 19:42:12 +00:00
|
|
|
data,
|
|
|
|
NONCE_LENGTH,
|
|
|
|
data.byteLength - NONCE_LENGTH - MAC_LENGTH
|
|
|
|
);
|
2020-09-04 01:25:19 +00:00
|
|
|
const theirMac = getBytes(data, data.byteLength - MAC_LENGTH, MAC_LENGTH);
|
2018-03-19 19:42:12 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const cipherKey = hmacSha256(key, nonce);
|
|
|
|
const macKey = hmacSha256(key, cipherKey);
|
2018-03-19 19:42:12 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const ourMac = getFirstBytes(hmacSha256(macKey, ciphertext), MAC_LENGTH);
|
2018-03-19 19:42:12 +00:00
|
|
|
if (!constantTimeEqual(theirMac, ourMac)) {
|
2018-04-27 21:25:04 +00:00
|
|
|
throw new Error(
|
|
|
|
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
|
|
|
);
|
2018-03-19 19:42:12 +00:00
|
|
|
}
|
|
|
|
|
2021-04-16 23:13:13 +00:00
|
|
|
return decryptAes256CbcPkcsPadding(cipherKey, ciphertext, iv);
|
2018-03-19 19:42:12 +00:00
|
|
|
}
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
// Encryption
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function hmacSha256(key: Uint8Array, plaintext: Uint8Array): Uint8Array {
|
2021-04-02 22:33:07 +00:00
|
|
|
return sign(key, plaintext);
|
2018-03-19 19:42:12 +00:00
|
|
|
}
|
|
|
|
|
2021-04-16 23:13:13 +00:00
|
|
|
// We use part of the constantTimeEqual algorithm from below here, but we allow ourMac
|
|
|
|
// to be longer than the passed-in length. This allows easy comparisons against
|
|
|
|
// arbitrary MAC lengths.
|
2021-09-24 00:49:05 +00:00
|
|
|
export function verifyHmacSha256(
|
|
|
|
plaintext: Uint8Array,
|
|
|
|
key: Uint8Array,
|
|
|
|
theirMac: Uint8Array,
|
2021-04-16 23:13:13 +00:00
|
|
|
length: number
|
2021-09-24 00:49:05 +00:00
|
|
|
): void {
|
|
|
|
const ourMac = hmacSha256(key, plaintext);
|
2021-04-16 23:13:13 +00:00
|
|
|
|
|
|
|
if (theirMac.byteLength !== length || ourMac.byteLength < length) {
|
|
|
|
throw new Error('Bad MAC length');
|
|
|
|
}
|
|
|
|
let result = 0;
|
|
|
|
|
|
|
|
for (let i = 0; i < theirMac.byteLength; i += 1) {
|
|
|
|
// eslint-disable-next-line no-bitwise
|
2021-09-24 00:49:05 +00:00
|
|
|
result |= ourMac[i] ^ theirMac[i];
|
2021-04-16 23:13:13 +00:00
|
|
|
}
|
|
|
|
if (result !== 0) {
|
|
|
|
throw new Error('Bad MAC');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function encryptAes256CbcPkcsPadding(
|
|
|
|
key: Uint8Array,
|
|
|
|
plaintext: Uint8Array,
|
|
|
|
iv: Uint8Array
|
|
|
|
): Uint8Array {
|
|
|
|
return encrypt(CipherType.AES256CBC, {
|
2018-03-19 19:42:12 +00:00
|
|
|
key,
|
2021-09-24 00:49:05 +00:00
|
|
|
plaintext,
|
2018-10-18 01:01:21 +00:00
|
|
|
iv,
|
2021-09-24 00:49:05 +00:00
|
|
|
});
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function decryptAes256CbcPkcsPadding(
|
|
|
|
key: Uint8Array,
|
|
|
|
ciphertext: Uint8Array,
|
|
|
|
iv: Uint8Array
|
|
|
|
): Uint8Array {
|
|
|
|
return decrypt(CipherType.AES256CBC, {
|
|
|
|
key,
|
|
|
|
ciphertext,
|
|
|
|
iv,
|
|
|
|
});
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function encryptAesCtr(
|
|
|
|
key: Uint8Array,
|
|
|
|
plaintext: Uint8Array,
|
|
|
|
counter: Uint8Array
|
|
|
|
): Uint8Array {
|
|
|
|
return encrypt(CipherType.AES256CTR, {
|
|
|
|
key,
|
|
|
|
plaintext,
|
|
|
|
iv: counter,
|
|
|
|
});
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
2018-03-19 19:42:12 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function decryptAesCtr(
|
|
|
|
key: Uint8Array,
|
|
|
|
ciphertext: Uint8Array,
|
|
|
|
counter: Uint8Array
|
|
|
|
): Uint8Array {
|
|
|
|
return decrypt(CipherType.AES256CTR, {
|
2018-10-18 01:01:21 +00:00
|
|
|
key,
|
2021-09-24 00:49:05 +00:00
|
|
|
ciphertext,
|
|
|
|
iv: counter,
|
|
|
|
});
|
2018-03-19 19:42:12 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function encryptAesGcm(
|
|
|
|
key: Uint8Array,
|
|
|
|
iv: Uint8Array,
|
|
|
|
plaintext: Uint8Array,
|
|
|
|
aad?: Uint8Array
|
|
|
|
): Uint8Array {
|
|
|
|
return encrypt(CipherType.AES256GCM, {
|
|
|
|
key,
|
|
|
|
plaintext,
|
2020-09-04 01:25:19 +00:00
|
|
|
iv,
|
2021-09-24 00:49:05 +00:00
|
|
|
aad,
|
|
|
|
});
|
|
|
|
}
|
2020-09-04 01:25:19 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function decryptAesGcm(
|
|
|
|
key: Uint8Array,
|
|
|
|
iv: Uint8Array,
|
|
|
|
ciphertext: Uint8Array
|
|
|
|
): Uint8Array {
|
|
|
|
return decrypt(CipherType.AES256GCM, {
|
2020-09-04 01:25:19 +00:00
|
|
|
key,
|
2021-09-24 00:49:05 +00:00
|
|
|
ciphertext,
|
|
|
|
iv,
|
|
|
|
});
|
2020-09-04 01:25:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Hashing
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function sha256(data: Uint8Array): Uint8Array {
|
2021-04-07 21:27:40 +00:00
|
|
|
return hash(HashType.size256, data);
|
2020-09-04 01:25:19 +00:00
|
|
|
}
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
// Utility
|
|
|
|
|
2020-03-31 20:03:38 +00:00
|
|
|
export function getRandomValue(low: number, high: number): number {
|
2019-05-16 22:32:11 +00:00
|
|
|
const diff = high - low;
|
2021-09-24 00:49:05 +00:00
|
|
|
const bytes = getRandomBytes(1);
|
2019-05-16 22:32:11 +00:00
|
|
|
|
|
|
|
// Because high and low are inclusive
|
|
|
|
const mod = diff + 1;
|
2020-03-31 20:03:38 +00:00
|
|
|
|
2020-01-08 17:44:54 +00:00
|
|
|
return (bytes[0] % mod) + low;
|
2019-05-16 22:32:11 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function getZeroes(n: number): Uint8Array {
|
|
|
|
return new Uint8Array(n);
|
2018-03-19 19:42:12 +00:00
|
|
|
}
|
|
|
|
|
2020-03-31 20:03:38 +00:00
|
|
|
export function highBitsToInt(byte: number): number {
|
2020-09-11 19:37:01 +00:00
|
|
|
// eslint-disable-next-line no-bitwise
|
2018-10-18 01:01:21 +00:00
|
|
|
return (byte & 0xff) >> 4;
|
2018-03-19 19:42:12 +00:00
|
|
|
}
|
|
|
|
|
2020-03-31 20:03:38 +00:00
|
|
|
export function intsToByteHighAndLow(
|
|
|
|
highValue: number,
|
|
|
|
lowValue: number
|
|
|
|
): number {
|
2020-09-11 19:37:01 +00:00
|
|
|
// eslint-disable-next-line no-bitwise
|
2018-10-18 01:01:21 +00:00
|
|
|
return ((highValue << 4) | lowValue) & 0xff;
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function getFirstBytes(data: Uint8Array, n: number): Uint8Array {
|
|
|
|
return data.subarray(0, n);
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
|
2020-09-04 01:25:19 +00:00
|
|
|
export function getBytes(
|
2021-09-24 00:49:05 +00:00
|
|
|
data: Uint8Array,
|
2020-03-31 20:03:38 +00:00
|
|
|
start: number,
|
|
|
|
n: number
|
2021-09-24 00:49:05 +00:00
|
|
|
): Uint8Array {
|
|
|
|
return data.subarray(start, start + n);
|
2018-03-19 19:42:12 +00:00
|
|
|
}
|
2020-09-04 01:25:19 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
function _getMacAndData(ciphertext: Uint8Array) {
|
2020-09-04 01:25:19 +00:00
|
|
|
const dataLength = ciphertext.byteLength - MAC_LENGTH;
|
|
|
|
const data = getBytes(ciphertext, 0, dataLength);
|
|
|
|
const mac = getBytes(ciphertext, dataLength, MAC_LENGTH);
|
|
|
|
|
|
|
|
return { data, mac };
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function encryptCdsDiscoveryRequest(
|
|
|
|
attestations: {
|
2021-09-24 00:49:05 +00:00
|
|
|
[key: string]: { clientKey: Uint8Array; requestId: Uint8Array };
|
2020-09-04 01:25:19 +00:00
|
|
|
},
|
|
|
|
phoneNumbers: ReadonlyArray<string>
|
2020-09-11 19:37:01 +00:00
|
|
|
): Promise<Record<string, unknown>> {
|
2020-09-04 01:25:19 +00:00
|
|
|
const nonce = getRandomBytes(32);
|
2021-07-13 18:54:53 +00:00
|
|
|
const numbersArray = Buffer.concat(
|
|
|
|
phoneNumbers.map(number => {
|
|
|
|
// Long.fromString handles numbers with or without a leading '+'
|
|
|
|
return new Uint8Array(Long.fromString(number).toBytesBE());
|
|
|
|
})
|
2020-09-04 01:25:19 +00:00
|
|
|
);
|
2021-04-26 23:52:20 +00:00
|
|
|
|
|
|
|
// We've written to the array, so offset === byteLength; we need to reset it. Then we'll
|
2021-09-24 00:49:05 +00:00
|
|
|
// have access to everything in the array when we generate an Uint8Array from it.
|
|
|
|
const queryDataPlaintext = Bytes.concatenate([nonce, numbersArray]);
|
2021-04-26 23:52:20 +00:00
|
|
|
|
2020-09-04 01:25:19 +00:00
|
|
|
const queryDataKey = getRandomBytes(32);
|
2021-04-07 21:27:40 +00:00
|
|
|
const commitment = sha256(queryDataPlaintext);
|
2020-09-04 01:25:19 +00:00
|
|
|
const iv = getRandomBytes(12);
|
2021-09-24 00:49:05 +00:00
|
|
|
const queryDataCiphertext = encryptAesGcm(
|
2020-09-04 01:25:19 +00:00
|
|
|
queryDataKey,
|
|
|
|
iv,
|
|
|
|
queryDataPlaintext
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
data: queryDataCiphertextData,
|
|
|
|
mac: queryDataCiphertextMac,
|
|
|
|
} = _getMacAndData(queryDataCiphertext);
|
|
|
|
|
|
|
|
const envelopes = await pProps(
|
|
|
|
attestations,
|
|
|
|
async ({ clientKey, requestId }) => {
|
|
|
|
const envelopeIv = getRandomBytes(12);
|
2021-09-24 00:49:05 +00:00
|
|
|
const ciphertext = encryptAesGcm(
|
2020-09-04 01:25:19 +00:00
|
|
|
clientKey,
|
|
|
|
envelopeIv,
|
|
|
|
queryDataKey,
|
|
|
|
requestId
|
|
|
|
);
|
|
|
|
const { data, mac } = _getMacAndData(ciphertext);
|
|
|
|
|
|
|
|
return {
|
2021-09-24 00:49:05 +00:00
|
|
|
requestId: Bytes.toBase64(requestId),
|
|
|
|
data: Bytes.toBase64(data),
|
|
|
|
iv: Bytes.toBase64(envelopeIv),
|
|
|
|
mac: Bytes.toBase64(mac),
|
2020-09-04 01:25:19 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
addressCount: phoneNumbers.length,
|
2021-09-24 00:49:05 +00:00
|
|
|
commitment: Bytes.toBase64(commitment),
|
|
|
|
data: Bytes.toBase64(queryDataCiphertextData),
|
|
|
|
iv: Bytes.toBase64(iv),
|
|
|
|
mac: Bytes.toBase64(queryDataCiphertextMac),
|
2020-09-04 01:25:19 +00:00
|
|
|
envelopes,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function uuidToBytes(uuid: string): Uint8Array {
|
2020-11-13 19:57:55 +00:00
|
|
|
if (uuid.length !== 36) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.warn(
|
2021-09-24 00:49:05 +00:00
|
|
|
'uuidToBytes: received a string of invalid length. ' +
|
|
|
|
'Returning an empty Uint8Array'
|
2020-11-13 19:57:55 +00:00
|
|
|
);
|
2021-09-24 00:49:05 +00:00
|
|
|
return new Uint8Array(0);
|
2020-11-13 19:57:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
return Uint8Array.from(
|
|
|
|
chunk(uuid.replace(/-/g, ''), 2).map(pair => parseInt(pair.join(''), 16))
|
2021-04-16 23:13:13 +00:00
|
|
|
);
|
2020-11-13 19:57:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function bytesToUuid(bytes: Uint8Array): undefined | string {
|
|
|
|
if (bytes.byteLength !== 16) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.warn(
|
2021-09-24 00:49:05 +00:00
|
|
|
'bytesToUuid: received an Uint8Array of invalid length. ' +
|
|
|
|
'Returning undefined'
|
2020-11-13 19:57:55 +00:00
|
|
|
);
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const uuids = splitUuids(bytes);
|
2020-11-13 19:57:55 +00:00
|
|
|
if (uuids.length === 1) {
|
|
|
|
return uuids[0] || undefined;
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function splitUuids(buffer: Uint8Array): Array<string | null> {
|
2020-09-04 01:25:19 +00:00
|
|
|
const uuids = [];
|
2021-09-24 00:49:05 +00:00
|
|
|
for (let i = 0; i < buffer.byteLength; i += 16) {
|
|
|
|
const bytes = getBytes(buffer, i, 16);
|
|
|
|
const hex = Bytes.toHex(bytes);
|
2020-09-04 01:25:19 +00:00
|
|
|
const chunks = [
|
|
|
|
hex.substring(0, 8),
|
|
|
|
hex.substring(8, 12),
|
|
|
|
hex.substring(12, 16),
|
|
|
|
hex.substring(16, 20),
|
|
|
|
hex.substring(20),
|
|
|
|
];
|
|
|
|
const uuid = chunks.join('-');
|
|
|
|
if (uuid !== '00000000-0000-0000-0000-000000000000') {
|
|
|
|
uuids.push(uuid);
|
|
|
|
} else {
|
|
|
|
uuids.push(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return uuids;
|
|
|
|
}
|
2021-01-28 00:18:50 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function trimForDisplay(padded: Uint8Array): Uint8Array {
|
2021-01-28 00:18:50 +00:00
|
|
|
let paddingEnd = 0;
|
|
|
|
for (paddingEnd; paddingEnd < padded.length; paddingEnd += 1) {
|
|
|
|
if (padded[paddingEnd] === 0x00) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-09-24 00:49:05 +00:00
|
|
|
return padded.slice(0, paddingEnd);
|
|
|
|
}
|
|
|
|
|
|
|
|
function verifyDigest(data: Uint8Array, theirDigest: Uint8Array): void {
|
|
|
|
const ourDigest = sha256(data);
|
|
|
|
let result = 0;
|
|
|
|
for (let i = 0; i < theirDigest.byteLength; i += 1) {
|
|
|
|
// eslint-disable-next-line no-bitwise
|
|
|
|
result |= ourDigest[i] ^ theirDigest[i];
|
|
|
|
}
|
|
|
|
if (result !== 0) {
|
|
|
|
throw new Error('Bad digest');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function decryptAttachment(
|
|
|
|
encryptedBin: Uint8Array,
|
|
|
|
keys: Uint8Array,
|
|
|
|
theirDigest?: Uint8Array
|
|
|
|
): Uint8Array {
|
|
|
|
if (keys.byteLength !== 64) {
|
|
|
|
throw new Error('Got invalid length attachment keys');
|
|
|
|
}
|
|
|
|
if (encryptedBin.byteLength < 16 + 32) {
|
|
|
|
throw new Error('Got invalid length attachment');
|
|
|
|
}
|
|
|
|
|
|
|
|
const aesKey = keys.slice(0, 32);
|
|
|
|
const macKey = keys.slice(32, 64);
|
|
|
|
|
|
|
|
const iv = encryptedBin.slice(0, 16);
|
|
|
|
const ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32);
|
|
|
|
const ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32);
|
|
|
|
const mac = encryptedBin.slice(
|
|
|
|
encryptedBin.byteLength - 32,
|
|
|
|
encryptedBin.byteLength
|
|
|
|
);
|
|
|
|
|
|
|
|
verifyHmacSha256(ivAndCiphertext, macKey, mac, 32);
|
|
|
|
|
|
|
|
if (theirDigest) {
|
|
|
|
verifyDigest(encryptedBin, theirDigest);
|
|
|
|
}
|
|
|
|
|
|
|
|
return decryptAes256CbcPkcsPadding(aesKey, ciphertext, iv);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function encryptAttachment(
|
|
|
|
plaintext: Uint8Array,
|
|
|
|
keys: Uint8Array,
|
|
|
|
iv: Uint8Array
|
|
|
|
): EncryptedAttachment {
|
|
|
|
if (!(plaintext instanceof Uint8Array)) {
|
|
|
|
throw new TypeError(
|
|
|
|
`\`plaintext\` must be an \`Uint8Array\`; got: ${typeof plaintext}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (keys.byteLength !== 64) {
|
|
|
|
throw new Error('Got invalid length attachment keys');
|
|
|
|
}
|
|
|
|
if (iv.byteLength !== 16) {
|
|
|
|
throw new Error('Got invalid length attachment iv');
|
|
|
|
}
|
|
|
|
const aesKey = keys.slice(0, 32);
|
|
|
|
const macKey = keys.slice(32, 64);
|
|
|
|
|
|
|
|
const ciphertext = encryptAes256CbcPkcsPadding(aesKey, plaintext, iv);
|
|
|
|
|
|
|
|
const ivAndCiphertext = Bytes.concatenate([iv, ciphertext]);
|
|
|
|
|
|
|
|
const mac = hmacSha256(macKey, ivAndCiphertext);
|
|
|
|
|
|
|
|
const encryptedBin = Bytes.concatenate([ivAndCiphertext, mac]);
|
|
|
|
const digest = sha256(encryptedBin);
|
|
|
|
|
|
|
|
return {
|
|
|
|
ciphertext: encryptedBin,
|
|
|
|
digest,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function encryptProfile(data: Uint8Array, key: Uint8Array): Uint8Array {
|
|
|
|
const iv = getRandomBytes(PROFILE_IV_LENGTH);
|
|
|
|
if (key.byteLength !== PROFILE_KEY_LENGTH) {
|
|
|
|
throw new Error('Got invalid length profile key');
|
|
|
|
}
|
|
|
|
if (iv.byteLength !== PROFILE_IV_LENGTH) {
|
|
|
|
throw new Error('Got invalid length profile iv');
|
|
|
|
}
|
|
|
|
const ciphertext = encryptAesGcm(key, iv, data);
|
|
|
|
return Bytes.concatenate([iv, ciphertext]);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function decryptProfile(data: Uint8Array, key: Uint8Array): Uint8Array {
|
|
|
|
if (data.byteLength < 12 + 16 + 1) {
|
|
|
|
throw new Error(`Got too short input: ${data.byteLength}`);
|
|
|
|
}
|
|
|
|
const iv = data.slice(0, PROFILE_IV_LENGTH);
|
|
|
|
const ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength);
|
|
|
|
if (key.byteLength !== PROFILE_KEY_LENGTH) {
|
|
|
|
throw new Error('Got invalid length profile key');
|
|
|
|
}
|
|
|
|
if (iv.byteLength !== PROFILE_IV_LENGTH) {
|
|
|
|
throw new Error('Got invalid length profile iv');
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
return decryptAesGcm(key, iv, ciphertext);
|
|
|
|
} catch (_) {
|
|
|
|
throw new ProfileDecryptError(
|
|
|
|
'Failed to decrypt profile data. ' +
|
|
|
|
'Most likely the profile key has changed.'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function encryptProfileItemWithPadding(
|
|
|
|
item: Uint8Array,
|
|
|
|
profileKey: Uint8Array,
|
|
|
|
paddedLengths: typeof PaddedLengths[keyof typeof PaddedLengths]
|
|
|
|
): Uint8Array {
|
|
|
|
const paddedLength = paddedLengths.find(
|
|
|
|
(length: number) => item.byteLength <= length
|
|
|
|
);
|
|
|
|
if (!paddedLength) {
|
|
|
|
throw new Error('Oversized value');
|
|
|
|
}
|
|
|
|
const padded = new Uint8Array(paddedLength);
|
|
|
|
padded.set(new Uint8Array(item));
|
|
|
|
return encryptProfile(padded, profileKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function decryptProfileName(
|
|
|
|
encryptedProfileName: string,
|
|
|
|
key: Uint8Array
|
|
|
|
): { given: Uint8Array; family: Uint8Array | null } {
|
|
|
|
const data = Bytes.fromBase64(encryptedProfileName);
|
|
|
|
const padded = decryptProfile(data, key);
|
|
|
|
|
|
|
|
// Given name is the start of the string to the first null character
|
|
|
|
let givenEnd;
|
|
|
|
for (givenEnd = 0; givenEnd < padded.length; givenEnd += 1) {
|
|
|
|
if (padded[givenEnd] === 0x00) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Family name is the next chunk of non-null characters after that first null
|
|
|
|
let familyEnd;
|
|
|
|
for (familyEnd = givenEnd + 1; familyEnd < padded.length; familyEnd += 1) {
|
|
|
|
if (padded[familyEnd] === 0x00) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const foundFamilyName = familyEnd > givenEnd + 1;
|
|
|
|
|
|
|
|
return {
|
|
|
|
given: padded.slice(0, givenEnd),
|
|
|
|
family: foundFamilyName ? padded.slice(givenEnd + 1, familyEnd) : null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// SignalContext APIs
|
|
|
|
//
|
|
|
|
|
|
|
|
const { crypto } = window.SignalContext;
|
|
|
|
|
|
|
|
export function sign(key: Uint8Array, data: Uint8Array): Uint8Array {
|
|
|
|
return crypto.sign(key, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function hash(type: HashType, data: Uint8Array): Uint8Array {
|
|
|
|
return crypto.hash(type, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function encrypt(
|
|
|
|
...args: Parameters<typeof crypto.encrypt>
|
|
|
|
): Uint8Array {
|
|
|
|
return crypto.encrypt(...args);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function decrypt(
|
|
|
|
...args: Parameters<typeof crypto.decrypt>
|
|
|
|
): Uint8Array {
|
|
|
|
return crypto.decrypt(...args);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getRandomBytes(size: number): Uint8Array {
|
|
|
|
return crypto.getRandomBytes(size);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function constantTimeEqual(
|
|
|
|
left: Uint8Array,
|
|
|
|
right: Uint8Array
|
|
|
|
): boolean {
|
|
|
|
return crypto.constantTimeEqual(left, right);
|
2021-01-28 00:18:50 +00:00
|
|
|
}
|