811 lines
21 KiB
TypeScript
811 lines
21 KiB
TypeScript
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { Buffer } from 'buffer';
|
|
import Long from 'long';
|
|
import { HKDF } from '@signalapp/libsignal-client';
|
|
|
|
import * as Bytes from './Bytes';
|
|
import { Crypto } from './context/Crypto';
|
|
import { calculateAgreement, generateKeyPair } from './Curve';
|
|
import { HashType, CipherType, UUID_BYTE_SIZE } from './types/Crypto';
|
|
import { ProfileDecryptError } from './types/errors';
|
|
import { getBytesSubarray } from './util/uuidToBytes';
|
|
import { logPadSize } from './util/logPadding';
|
|
import { Environment } from './environment';
|
|
|
|
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;
|
|
plaintextHash: string;
|
|
};
|
|
|
|
export function generateRegistrationId(): number {
|
|
return randomInt(1, 16383);
|
|
}
|
|
|
|
export function deriveStickerPackKey(packKey: Uint8Array): Uint8Array {
|
|
const salt = getZeroes(32);
|
|
const info = Bytes.fromString('Sticker Pack');
|
|
|
|
const [part1, part2] = deriveSecrets(packKey, salt, info);
|
|
|
|
return Bytes.concatenate([part1, part2]);
|
|
}
|
|
|
|
export function deriveSecrets(
|
|
input: Uint8Array,
|
|
salt: Uint8Array,
|
|
info: Uint8Array
|
|
): [Uint8Array, Uint8Array, Uint8Array] {
|
|
const hkdf = HKDF.new(3);
|
|
const output = hkdf.deriveSecrets(
|
|
3 * 32,
|
|
Buffer.from(input),
|
|
Buffer.from(info),
|
|
Buffer.from(salt)
|
|
);
|
|
return [output.slice(0, 32), output.slice(32, 64), output.slice(64, 96)];
|
|
}
|
|
|
|
export function deriveMasterKeyFromGroupV1(groupV1Id: Uint8Array): Uint8Array {
|
|
const salt = getZeroes(32);
|
|
const info = Bytes.fromString('GV2 Migration');
|
|
|
|
const [part1] = deriveSecrets(groupV1Id, salt, info);
|
|
|
|
return part1;
|
|
}
|
|
|
|
export function computeHash(data: Uint8Array): string {
|
|
return Bytes.toBase64(hash(HashType.size512, data));
|
|
}
|
|
|
|
// High-level Operations
|
|
|
|
export type EncryptedDeviceName = {
|
|
ephemeralPublic: Uint8Array;
|
|
syntheticIv: Uint8Array;
|
|
ciphertext: Uint8Array;
|
|
};
|
|
|
|
export function encryptDeviceName(
|
|
deviceName: string,
|
|
identityPublic: Uint8Array
|
|
): EncryptedDeviceName {
|
|
const plaintext = Bytes.fromString(deviceName);
|
|
const ephemeralKeyPair = generateKeyPair();
|
|
const masterSecret = calculateAgreement(
|
|
identityPublic,
|
|
ephemeralKeyPair.privKey
|
|
);
|
|
|
|
const key1 = hmacSha256(masterSecret, Bytes.fromString('auth'));
|
|
const syntheticIv = getFirstBytes(hmacSha256(key1, plaintext), 16);
|
|
|
|
const key2 = hmacSha256(masterSecret, Bytes.fromString('cipher'));
|
|
const cipherKey = hmacSha256(key2, syntheticIv);
|
|
|
|
const counter = getZeroes(16);
|
|
const ciphertext = encryptAesCtr(cipherKey, plaintext, counter);
|
|
|
|
return {
|
|
ephemeralPublic: ephemeralKeyPair.pubKey,
|
|
syntheticIv,
|
|
ciphertext,
|
|
};
|
|
}
|
|
|
|
export function decryptDeviceName(
|
|
{ ephemeralPublic, syntheticIv, ciphertext }: EncryptedDeviceName,
|
|
identityPrivate: Uint8Array
|
|
): string {
|
|
const masterSecret = calculateAgreement(ephemeralPublic, identityPrivate);
|
|
|
|
const key2 = hmacSha256(masterSecret, Bytes.fromString('cipher'));
|
|
const cipherKey = hmacSha256(key2, syntheticIv);
|
|
|
|
const counter = getZeroes(16);
|
|
const plaintext = decryptAesCtr(cipherKey, ciphertext, counter);
|
|
|
|
const key1 = hmacSha256(masterSecret, Bytes.fromString('auth'));
|
|
const ourSyntheticIv = getFirstBytes(hmacSha256(key1, plaintext), 16);
|
|
|
|
if (!constantTimeEqual(ourSyntheticIv, syntheticIv)) {
|
|
throw new Error('decryptDeviceName: synthetic IV did not match');
|
|
}
|
|
|
|
return Bytes.toString(plaintext);
|
|
}
|
|
|
|
export function deriveStorageServiceKey(masterKey: Uint8Array): Uint8Array {
|
|
return hmacSha256(masterKey, Bytes.fromString('Storage Service Encryption'));
|
|
}
|
|
|
|
export function deriveStorageManifestKey(
|
|
storageServiceKey: Uint8Array,
|
|
version: Long = Long.fromNumber(0)
|
|
): Uint8Array {
|
|
return hmacSha256(storageServiceKey, Bytes.fromString(`Manifest_${version}`));
|
|
}
|
|
|
|
const BACKUP_KEY_LEN = 32;
|
|
const BACKUP_KEY_INFO = '20231003_Signal_Backups_GenerateBackupKey';
|
|
|
|
export function deriveBackupKey(masterKey: Uint8Array): Uint8Array {
|
|
const hkdf = HKDF.new(3);
|
|
return hkdf.deriveSecrets(
|
|
BACKUP_KEY_LEN,
|
|
Buffer.from(masterKey),
|
|
Buffer.from(BACKUP_KEY_INFO),
|
|
Buffer.alloc(0)
|
|
);
|
|
}
|
|
|
|
const BACKUP_SIGNATURE_KEY_LEN = 32;
|
|
const BACKUP_SIGNATURE_KEY_INFO =
|
|
'20231003_Signal_Backups_GenerateBackupIdKeyPair';
|
|
|
|
export function deriveBackupSignatureKey(
|
|
backupKey: Uint8Array,
|
|
aciBytes: Uint8Array
|
|
): Uint8Array {
|
|
if (backupKey.byteLength !== BACKUP_KEY_LEN) {
|
|
throw new Error('deriveBackupId: invalid backup key length');
|
|
}
|
|
|
|
if (aciBytes.byteLength !== UUID_BYTE_SIZE) {
|
|
throw new Error('deriveBackupId: invalid aci length');
|
|
}
|
|
|
|
const hkdf = HKDF.new(3);
|
|
return hkdf.deriveSecrets(
|
|
BACKUP_SIGNATURE_KEY_LEN,
|
|
Buffer.from(backupKey),
|
|
Buffer.from(BACKUP_SIGNATURE_KEY_INFO),
|
|
Buffer.from(aciBytes)
|
|
);
|
|
}
|
|
|
|
const BACKUP_ID_LEN = 16;
|
|
const BACKUP_ID_INFO = '20231003_Signal_Backups_GenerateBackupId';
|
|
|
|
export function deriveBackupId(
|
|
backupKey: Uint8Array,
|
|
aciBytes: Uint8Array
|
|
): Uint8Array {
|
|
if (backupKey.byteLength !== BACKUP_KEY_LEN) {
|
|
throw new Error('deriveBackupId: invalid backup key length');
|
|
}
|
|
|
|
if (aciBytes.byteLength !== UUID_BYTE_SIZE) {
|
|
throw new Error('deriveBackupId: invalid aci length');
|
|
}
|
|
|
|
const hkdf = HKDF.new(3);
|
|
return hkdf.deriveSecrets(
|
|
BACKUP_ID_LEN,
|
|
Buffer.from(backupKey),
|
|
Buffer.from(BACKUP_ID_INFO),
|
|
Buffer.from(aciBytes)
|
|
);
|
|
}
|
|
|
|
export type BackupKeyMaterialType = Readonly<{
|
|
macKey: Uint8Array;
|
|
aesKey: Uint8Array;
|
|
}>;
|
|
|
|
export type BackupMediaKeyMaterialType = Readonly<{
|
|
macKey: Uint8Array;
|
|
aesKey: Uint8Array;
|
|
iv: Uint8Array;
|
|
}>;
|
|
|
|
const BACKUP_AES_KEY_LEN = 32;
|
|
const BACKUP_MAC_KEY_LEN = 32;
|
|
const BACKUP_MATERIAL_INFO = '20231003_Signal_Backups_EncryptMessageBackup';
|
|
|
|
const BACKUP_MEDIA_ID_INFO = '20231003_Signal_Backups_Media_ID';
|
|
const BACKUP_MEDIA_ID_LEN = 15;
|
|
const BACKUP_MEDIA_ENCRYPT_INFO = '20231003_Signal_Backups_EncryptMedia';
|
|
const BACKUP_MEDIA_THUMBNAIL_ENCRYPT_INFO =
|
|
'20240513_Signal_Backups_EncryptThumbnail';
|
|
const BACKUP_MEDIA_AES_KEY_LEN = 32;
|
|
const BACKUP_MEDIA_MAC_KEY_LEN = 32;
|
|
const BACKUP_MEDIA_IV_LEN = 16;
|
|
|
|
export function deriveBackupKeyMaterial(
|
|
backupKey: Uint8Array,
|
|
backupId: Uint8Array
|
|
): BackupKeyMaterialType {
|
|
if (backupKey.byteLength !== BACKUP_KEY_LEN) {
|
|
throw new Error('deriveBackupId: invalid backup key length');
|
|
}
|
|
|
|
if (backupId.byteLength !== BACKUP_ID_LEN) {
|
|
throw new Error('deriveBackupId: invalid backup id length');
|
|
}
|
|
|
|
const hkdf = HKDF.new(3);
|
|
const material = hkdf.deriveSecrets(
|
|
BACKUP_AES_KEY_LEN + BACKUP_MAC_KEY_LEN,
|
|
Buffer.from(backupKey),
|
|
Buffer.from(BACKUP_MATERIAL_INFO),
|
|
Buffer.from(backupId)
|
|
);
|
|
|
|
return {
|
|
macKey: material.slice(0, BACKUP_MAC_KEY_LEN),
|
|
aesKey: material.slice(BACKUP_MAC_KEY_LEN),
|
|
};
|
|
}
|
|
|
|
export function deriveMediaIdFromMediaName(
|
|
backupKey: Uint8Array,
|
|
mediaName: string
|
|
): Uint8Array {
|
|
if (backupKey.byteLength !== BACKUP_KEY_LEN) {
|
|
throw new Error('deriveMediaIdFromMediaName: invalid backup key length');
|
|
}
|
|
|
|
if (!mediaName) {
|
|
throw new Error('deriveMediaIdFromMediaName: mediaName missing');
|
|
}
|
|
|
|
const hkdf = HKDF.new(3);
|
|
return hkdf.deriveSecrets(
|
|
BACKUP_MEDIA_ID_LEN,
|
|
Buffer.from(backupKey),
|
|
Buffer.from(BACKUP_MEDIA_ID_INFO),
|
|
Buffer.from(mediaName, 'utf8')
|
|
);
|
|
}
|
|
|
|
export function deriveBackupMediaKeyMaterial(
|
|
backupKey: Uint8Array,
|
|
mediaId: Uint8Array
|
|
): BackupMediaKeyMaterialType {
|
|
if (backupKey.byteLength !== BACKUP_KEY_LEN) {
|
|
throw new Error('deriveBackupMediaKeyMaterial: invalid backup key length');
|
|
}
|
|
|
|
if (!mediaId.length) {
|
|
throw new Error('deriveBackupMediaKeyMaterial: mediaId missing');
|
|
}
|
|
|
|
const hkdf = HKDF.new(3);
|
|
const material = hkdf.deriveSecrets(
|
|
BACKUP_MEDIA_MAC_KEY_LEN + BACKUP_MEDIA_AES_KEY_LEN + BACKUP_MEDIA_IV_LEN,
|
|
Buffer.from(backupKey),
|
|
Buffer.from(BACKUP_MEDIA_ENCRYPT_INFO),
|
|
Buffer.from(mediaId)
|
|
);
|
|
|
|
return {
|
|
macKey: material.subarray(0, BACKUP_MEDIA_MAC_KEY_LEN),
|
|
aesKey: material.subarray(
|
|
BACKUP_MEDIA_MAC_KEY_LEN,
|
|
BACKUP_MEDIA_MAC_KEY_LEN + BACKUP_MEDIA_AES_KEY_LEN
|
|
),
|
|
iv: material.subarray(BACKUP_MEDIA_MAC_KEY_LEN + BACKUP_MEDIA_AES_KEY_LEN),
|
|
};
|
|
}
|
|
|
|
export function deriveBackupMediaThumbnailInnerEncryptionKeyMaterial(
|
|
backupKey: Uint8Array,
|
|
mediaId: Uint8Array
|
|
): BackupMediaKeyMaterialType {
|
|
if (backupKey.byteLength !== BACKUP_KEY_LEN) {
|
|
throw new Error(
|
|
'deriveBackupMediaThumbnailKeyMaterial: invalid backup key length'
|
|
);
|
|
}
|
|
|
|
if (!mediaId.length) {
|
|
throw new Error('deriveBackupMediaThumbnailKeyMaterial: mediaId missing');
|
|
}
|
|
|
|
const hkdf = HKDF.new(3);
|
|
const material = hkdf.deriveSecrets(
|
|
BACKUP_MEDIA_MAC_KEY_LEN + BACKUP_MEDIA_AES_KEY_LEN + BACKUP_MEDIA_IV_LEN,
|
|
Buffer.from(backupKey),
|
|
Buffer.from(BACKUP_MEDIA_THUMBNAIL_ENCRYPT_INFO),
|
|
Buffer.from(mediaId)
|
|
);
|
|
|
|
return {
|
|
aesKey: material.subarray(0, BACKUP_MEDIA_AES_KEY_LEN),
|
|
macKey: material.subarray(
|
|
BACKUP_MEDIA_AES_KEY_LEN,
|
|
BACKUP_MEDIA_AES_KEY_LEN + BACKUP_MEDIA_MAC_KEY_LEN
|
|
),
|
|
iv: material.subarray(BACKUP_MEDIA_MAC_KEY_LEN + BACKUP_MEDIA_AES_KEY_LEN),
|
|
};
|
|
}
|
|
|
|
export function deriveStorageItemKey(
|
|
storageServiceKey: Uint8Array,
|
|
itemID: string
|
|
): Uint8Array {
|
|
return hmacSha256(storageServiceKey, Bytes.fromString(`Item_${itemID}`));
|
|
}
|
|
|
|
export function deriveAccessKey(profileKey: Uint8Array): Uint8Array {
|
|
const iv = getZeroes(12);
|
|
const plaintext = getZeroes(16);
|
|
const accessKey = encryptAesGcm(profileKey, iv, plaintext);
|
|
|
|
return getFirstBytes(accessKey, 16);
|
|
}
|
|
|
|
export function getAccessKeyVerifier(accessKey: Uint8Array): Uint8Array {
|
|
const plaintext = getZeroes(32);
|
|
|
|
return hmacSha256(accessKey, plaintext);
|
|
}
|
|
|
|
export function verifyAccessKey(
|
|
accessKey: Uint8Array,
|
|
theirVerifier: Uint8Array
|
|
): boolean {
|
|
const ourVerifier = getAccessKeyVerifier(accessKey);
|
|
|
|
if (constantTimeEqual(ourVerifier, theirVerifier)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const IV_LENGTH = 16;
|
|
const NONCE_LENGTH = 16;
|
|
const SYMMETRIC_MAC_LENGTH = 16;
|
|
|
|
export function encryptSymmetric(
|
|
key: Uint8Array,
|
|
plaintext: Uint8Array
|
|
): Uint8Array {
|
|
const iv = getZeroes(IV_LENGTH);
|
|
const nonce = getRandomBytes(NONCE_LENGTH);
|
|
|
|
const cipherKey = hmacSha256(key, nonce);
|
|
const macKey = hmacSha256(key, cipherKey);
|
|
|
|
const ciphertext = encryptAes256CbcPkcsPadding(cipherKey, plaintext, iv);
|
|
const mac = getFirstBytes(
|
|
hmacSha256(macKey, ciphertext),
|
|
SYMMETRIC_MAC_LENGTH
|
|
);
|
|
|
|
return Bytes.concatenate([nonce, ciphertext, mac]);
|
|
}
|
|
|
|
export function decryptSymmetric(
|
|
key: Uint8Array,
|
|
data: Uint8Array
|
|
): Uint8Array {
|
|
const iv = getZeroes(IV_LENGTH);
|
|
|
|
const nonce = getFirstBytes(data, NONCE_LENGTH);
|
|
const ciphertext = getBytesSubarray(
|
|
data,
|
|
NONCE_LENGTH,
|
|
data.byteLength - NONCE_LENGTH - SYMMETRIC_MAC_LENGTH
|
|
);
|
|
const theirMac = getBytesSubarray(
|
|
data,
|
|
data.byteLength - SYMMETRIC_MAC_LENGTH,
|
|
SYMMETRIC_MAC_LENGTH
|
|
);
|
|
|
|
const cipherKey = hmacSha256(key, nonce);
|
|
const macKey = hmacSha256(key, cipherKey);
|
|
|
|
const ourMac = getFirstBytes(
|
|
hmacSha256(macKey, ciphertext),
|
|
SYMMETRIC_MAC_LENGTH
|
|
);
|
|
if (!constantTimeEqual(theirMac, ourMac)) {
|
|
throw new Error(
|
|
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
|
);
|
|
}
|
|
|
|
return decryptAes256CbcPkcsPadding(cipherKey, ciphertext, iv);
|
|
}
|
|
|
|
// Encryption
|
|
|
|
export function hmacSha256(key: Uint8Array, plaintext: Uint8Array): Uint8Array {
|
|
return sign(key, plaintext);
|
|
}
|
|
|
|
// 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.
|
|
export function verifyHmacSha256(
|
|
plaintext: Uint8Array,
|
|
key: Uint8Array,
|
|
theirMac: Uint8Array,
|
|
length: number
|
|
): void {
|
|
const ourMac = hmacSha256(key, plaintext);
|
|
|
|
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
|
|
result |= ourMac[i] ^ theirMac[i];
|
|
}
|
|
if (result !== 0) {
|
|
throw new Error('Bad MAC');
|
|
}
|
|
}
|
|
|
|
export function encryptAes256CbcPkcsPadding(
|
|
key: Uint8Array,
|
|
plaintext: Uint8Array,
|
|
iv: Uint8Array
|
|
): Uint8Array {
|
|
return encrypt(CipherType.AES256CBC, {
|
|
key,
|
|
plaintext,
|
|
iv,
|
|
});
|
|
}
|
|
|
|
export function decryptAes256CbcPkcsPadding(
|
|
key: Uint8Array,
|
|
ciphertext: Uint8Array,
|
|
iv: Uint8Array
|
|
): Uint8Array {
|
|
return decrypt(CipherType.AES256CBC, {
|
|
key,
|
|
ciphertext,
|
|
iv,
|
|
});
|
|
}
|
|
|
|
export function encryptAesCtr(
|
|
key: Uint8Array,
|
|
plaintext: Uint8Array,
|
|
counter: Uint8Array
|
|
): Uint8Array {
|
|
return encrypt(CipherType.AES256CTR, {
|
|
key,
|
|
plaintext,
|
|
iv: counter,
|
|
});
|
|
}
|
|
|
|
export function decryptAesCtr(
|
|
key: Uint8Array,
|
|
ciphertext: Uint8Array,
|
|
counter: Uint8Array
|
|
): Uint8Array {
|
|
return decrypt(CipherType.AES256CTR, {
|
|
key,
|
|
ciphertext,
|
|
iv: counter,
|
|
});
|
|
}
|
|
|
|
export function encryptAesGcm(
|
|
key: Uint8Array,
|
|
iv: Uint8Array,
|
|
plaintext: Uint8Array,
|
|
aad?: Uint8Array
|
|
): Uint8Array {
|
|
return encrypt(CipherType.AES256GCM, {
|
|
key,
|
|
plaintext,
|
|
iv,
|
|
aad,
|
|
});
|
|
}
|
|
|
|
export function decryptAesGcm(
|
|
key: Uint8Array,
|
|
iv: Uint8Array,
|
|
ciphertext: Uint8Array
|
|
): Uint8Array {
|
|
return decrypt(CipherType.AES256GCM, {
|
|
key,
|
|
ciphertext,
|
|
iv,
|
|
});
|
|
}
|
|
|
|
// Hashing
|
|
|
|
export function sha256(data: Uint8Array): Uint8Array {
|
|
return hash(HashType.size256, data);
|
|
}
|
|
|
|
// Utility
|
|
|
|
export function getZeroes(n: number): Uint8Array {
|
|
return new Uint8Array(n);
|
|
}
|
|
|
|
export function highBitsToInt(byte: number): number {
|
|
// eslint-disable-next-line no-bitwise
|
|
return (byte & 0xff) >> 4;
|
|
}
|
|
|
|
export function intsToByteHighAndLow(
|
|
highValue: number,
|
|
lowValue: number
|
|
): number {
|
|
// eslint-disable-next-line no-bitwise
|
|
return ((highValue << 4) | lowValue) & 0xff;
|
|
}
|
|
|
|
export function getFirstBytes(data: Uint8Array, n: number): Uint8Array {
|
|
return data.subarray(0, n);
|
|
}
|
|
|
|
export function trimForDisplay(padded: Uint8Array): Uint8Array {
|
|
let paddingEnd = 0;
|
|
for (paddingEnd; paddingEnd < padded.length; paddingEnd += 1) {
|
|
if (padded[paddingEnd] === 0x00) {
|
|
break;
|
|
}
|
|
}
|
|
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 decryptAttachmentV1(
|
|
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,
|
|
keys,
|
|
dangerousTestOnlyIv,
|
|
}: {
|
|
plaintext: Readonly<Uint8Array>;
|
|
keys: Readonly<Uint8Array>;
|
|
dangerousTestOnlyIv?: Readonly<Uint8Array>;
|
|
}): Omit<EncryptedAttachment, 'plaintextHash'> {
|
|
const logId = 'encryptAttachment';
|
|
if (!(plaintext instanceof Uint8Array)) {
|
|
throw new TypeError(
|
|
`${logId}: \`plaintext\` must be an \`Uint8Array\`; got: ${typeof plaintext}`
|
|
);
|
|
}
|
|
|
|
if (keys.byteLength !== 64) {
|
|
throw new Error(`${logId}: invalid length attachment keys`);
|
|
}
|
|
|
|
if (dangerousTestOnlyIv && window.getEnvironment() !== Environment.Test) {
|
|
throw new Error(`${logId}: Used dangerousTestOnlyIv outside tests!`);
|
|
}
|
|
|
|
const iv = dangerousTestOnlyIv || getRandomBytes(16);
|
|
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 padAndEncryptAttachment({
|
|
plaintext,
|
|
keys,
|
|
dangerousTestOnlyIv,
|
|
}: {
|
|
plaintext: Readonly<Uint8Array>;
|
|
keys: Readonly<Uint8Array>;
|
|
dangerousTestOnlyIv?: Readonly<Uint8Array>;
|
|
}): EncryptedAttachment {
|
|
const size = plaintext.byteLength;
|
|
const paddedSize = logPadSize(size);
|
|
const padding = getZeroes(paddedSize - size);
|
|
|
|
return {
|
|
...encryptAttachment({
|
|
plaintext: Bytes.concatenate([plaintext, padding]),
|
|
keys,
|
|
dangerousTestOnlyIv,
|
|
}),
|
|
// We generate the plaintext hash here for forwards-compatibility with streaming
|
|
// attachment encryption, which may be the only place that the whole attachment flows
|
|
// through memory
|
|
plaintextHash: Buffer.from(sha256(plaintext)).toString('hex'),
|
|
};
|
|
}
|
|
|
|
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 = globalThis.window?.SignalContext.crypto || new Crypto();
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Generate an integer between `min` and `max`, inclusive.
|
|
*/
|
|
export function randomInt(min: number, max: number): number {
|
|
return crypto.randomInt(min, max + 1);
|
|
}
|
|
|
|
export function getRandomBytes(size: number): Uint8Array {
|
|
return crypto.getRandomBytes(size);
|
|
}
|
|
|
|
export function constantTimeEqual(
|
|
left: Uint8Array,
|
|
right: Uint8Array
|
|
): boolean {
|
|
return crypto.constantTimeEqual(left, right);
|
|
}
|