signal-desktop/ts/Crypto.ts

835 lines
21 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 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';
import Long from 'long';
import { Aci, HKDF } from '@signalapp/libsignal-client';
2021-07-13 18:54:53 +00:00
2021-09-24 00:49:05 +00:00
import * as Bytes from './Bytes';
import { Crypto } from './context/Crypto';
import { calculateAgreement, generateKeyPair } from './Curve';
2024-04-15 20:54:21 +00:00
import { HashType, CipherType, UUID_BYTE_SIZE } from './types/Crypto';
2021-09-24 00:49:05 +00:00
import { ProfileDecryptError } from './types/errors';
2023-08-09 00:53:06 +00:00
import { getBytesSubarray } from './util/uuidToBytes';
2024-04-15 20:54:21 +00:00
import { logPadSize } from './util/logPadding';
2024-09-04 18:12:45 +00:00
import { Environment, getEnvironment } from './environment';
import { toWebSafeBase64 } from './util/webSafeBase64';
import type { AciString } from './types/ServiceId';
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;
plaintextHash: string;
2021-09-24 00:49:05 +00:00
};
export function generateRegistrationId(): number {
2022-10-05 16:35:56 +00:00
return randomInt(1, 16383);
}
2021-09-24 00:49:05 +00:00
export function deriveStickerPackKey(packKey: Uint8Array): Uint8Array {
const salt = getZeroes(32);
2021-09-24 00:49:05 +00:00
const info = Bytes.fromString('Sticker Pack');
2021-09-24 00:49:05 +00:00
const [part1, part2] = deriveSecrets(packKey, salt, info);
2021-09-24 00:49:05 +00:00
return Bytes.concatenate([part1, part2]);
}
2018-12-13 21:41:42 +00:00
export function deriveSecrets(
2021-09-24 00:49:05 +00:00
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)
);
2021-09-24 00:49:05 +00:00
return [output.slice(0, 32), output.slice(32, 64), output.slice(64, 96)];
}
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;
}
export function hashProfileKey(
profileKey: string | undefined,
aci: AciString
): string {
if (!profileKey) {
return 'none';
}
const profileKeyBytes = Bytes.fromBase64(profileKey);
const aciBytes = Aci.parseFromServiceIdString(aci).getRawUuidBytes();
const hashBytes = hmacSha256(
profileKeyBytes,
Bytes.concatenate([Bytes.fromString('profileKeyHash'), aciBytes])
);
const webSafe = toWebSafeBase64(Bytes.toBase64(hashBytes));
return webSafe.slice(-3);
}
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
}
// 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(
deviceName: string,
2021-09-24 00:49:05 +00:00
identityPublic: Uint8Array
): EncryptedDeviceName {
const plaintext = Bytes.fromString(deviceName);
const ephemeralKeyPair = generateKeyPair();
const masterSecret = calculateAgreement(
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);
2021-09-24 00:49:05 +00:00
const key2 = hmacSha256(masterSecret, Bytes.fromString('cipher'));
const cipherKey = hmacSha256(key2, syntheticIv);
const counter = getZeroes(16);
2021-09-24 00:49:05 +00:00
const ciphertext = encryptAesCtr(cipherKey, plaintext, counter);
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 {
const masterSecret = calculateAgreement(ephemeralPublic, identityPrivate);
2021-09-24 00:49:05 +00:00
const key2 = hmacSha256(masterSecret, Bytes.fromString('cipher'));
const cipherKey = hmacSha256(key2, syntheticIv);
const counter = getZeroes(16);
2021-09-24 00:49:05 +00:00
const plaintext = decryptAesCtr(cipherKey, ciphertext, counter);
2021-09-24 00:49:05 +00:00
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');
}
2021-09-24 00:49:05 +00:00
return Bytes.toString(plaintext);
}
export function deriveStorageServiceKey(masterKey: Uint8Array): Uint8Array {
return hmacSha256(masterKey, Bytes.fromString('Storage Service Encryption'));
}
2021-09-24 00:49:05 +00:00
export function deriveStorageManifestKey(
storageServiceKey: Uint8Array,
2022-03-23 20:49:27 +00:00
version: Long = Long.fromNumber(0)
2021-09-24 00:49:05 +00:00
): Uint8Array {
return hmacSha256(storageServiceKey, Bytes.fromString(`Manifest_${version}`));
}
2024-04-15 20:54:21 +00:00
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)
);
}
2024-04-22 14:11:36 +00:00
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)
);
}
2024-04-15 20:54:21 +00:00
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;
}>;
2024-04-15 20:54:21 +00:00
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;
2024-04-15 20:54:21 +00:00
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),
};
}
2021-09-24 00:49:05 +00:00
export function deriveStorageItemKey(
storageServiceKey: Uint8Array,
itemID: string
2021-09-24 00:49:05 +00:00
): Uint8Array {
return hmacSha256(storageServiceKey, Bytes.fromString(`Item_${itemID}`));
}
2021-09-24 00:49:05 +00:00
export function deriveAccessKey(profileKey: Uint8Array): Uint8Array {
const iv = getZeroes(12);
const plaintext = getZeroes(16);
2021-09-24 00:49:05 +00:00
const accessKey = encryptAesGcm(profileKey, iv, plaintext);
return getFirstBytes(accessKey, 16);
}
2021-09-24 00:49:05 +00:00
export function getAccessKeyVerifier(accessKey: Uint8Array): Uint8Array {
const plaintext = getZeroes(32);
return hmacSha256(accessKey, plaintext);
}
2021-09-24 00:49:05 +00:00
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;
2021-09-24 00:49:05 +00:00
export function encryptSymmetric(
key: Uint8Array,
plaintext: Uint8Array
): Uint8Array {
const iv = getZeroes(IV_LENGTH);
const nonce = getRandomBytes(NONCE_LENGTH);
2021-09-24 00:49:05 +00:00
const cipherKey = hmacSha256(key, nonce);
const macKey = hmacSha256(key, cipherKey);
2021-09-24 00:49:05 +00:00
const ciphertext = encryptAes256CbcPkcsPadding(cipherKey, plaintext, iv);
const mac = getFirstBytes(
hmacSha256(macKey, ciphertext),
SYMMETRIC_MAC_LENGTH
);
2021-09-24 00:49:05 +00:00
return Bytes.concatenate([nonce, ciphertext, mac]);
}
2021-09-24 00:49:05 +00:00
export function decryptSymmetric(
key: Uint8Array,
data: Uint8Array
): Uint8Array {
const iv = getZeroes(IV_LENGTH);
const nonce = getFirstBytes(data, NONCE_LENGTH);
2023-08-09 00:53:06 +00:00
const ciphertext = getBytesSubarray(
data,
NONCE_LENGTH,
data.byteLength - NONCE_LENGTH - SYMMETRIC_MAC_LENGTH
);
2023-08-09 00:53:06 +00:00
const theirMac = getBytesSubarray(
data,
data.byteLength - SYMMETRIC_MAC_LENGTH,
SYMMETRIC_MAC_LENGTH
2023-08-09 00:53:06 +00:00
);
2021-09-24 00:49:05 +00:00
const cipherKey = hmacSha256(key, nonce);
const macKey = hmacSha256(key, cipherKey);
const ourMac = getFirstBytes(
hmacSha256(macKey, ciphertext),
SYMMETRIC_MAC_LENGTH
);
if (!constantTimeEqual(theirMac, ourMac)) {
2018-04-27 21:25:04 +00:00
throw new Error(
'decryptSymmetric: Failed to decrypt; MAC verification failed'
);
}
return decryptAes256CbcPkcsPadding(cipherKey, ciphertext, iv);
}
// Encryption
2021-09-24 00:49:05 +00:00
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.
2021-09-24 00:49:05 +00:00
export function verifyHmacSha256(
plaintext: Uint8Array,
key: Uint8Array,
theirMac: Uint8Array,
length: number
2021-09-24 00:49:05 +00:00
): 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
2021-09-24 00:49:05 +00:00
result |= ourMac[i] ^ theirMac[i];
}
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, {
key,
2021-09-24 00:49:05 +00:00
plaintext,
iv,
2021-09-24 00:49:05 +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,
});
}
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,
});
}
2021-09-24 00:49:05 +00:00
export function decryptAesCtr(
key: Uint8Array,
ciphertext: Uint8Array,
counter: Uint8Array
): Uint8Array {
return decrypt(CipherType.AES256CTR, {
key,
2021-09-24 00:49:05 +00:00
ciphertext,
iv: counter,
});
}
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 {
return hash(HashType.size256, data);
2020-09-04 01:25:19 +00:00
}
// Utility
2021-09-24 00:49:05 +00:00
export function getZeroes(n: number): Uint8Array {
return new Uint8Array(n);
}
export function highBitsToInt(byte: number): number {
2020-09-11 19:37:01 +00:00
// eslint-disable-next-line no-bitwise
return (byte & 0xff) >> 4;
}
export function intsToByteHighAndLow(
highValue: number,
lowValue: number
): number {
2020-09-11 19:37:01 +00:00
// eslint-disable-next-line no-bitwise
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);
}
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 decryptAttachmentV1(
2021-09-24 00:49:05 +00:00
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';
2021-09-24 00:49:05 +00:00
if (!(plaintext instanceof Uint8Array)) {
throw new TypeError(
`${logId}: \`plaintext\` must be an \`Uint8Array\`; got: ${typeof plaintext}`
2021-09-24 00:49:05 +00:00
);
}
if (keys.byteLength !== 64) {
throw new Error(`${logId}: invalid length attachment keys`);
2021-09-24 00:49:05 +00:00
}
2024-09-04 18:12:45 +00:00
if (dangerousTestOnlyIv && getEnvironment() !== Environment.Test) {
throw new Error(`${logId}: Used dangerousTestOnlyIv outside tests!`);
}
const iv = dangerousTestOnlyIv || getRandomBytes(16);
2021-09-24 00:49:05 +00:00
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;
2024-04-15 20:54:21 +00:00
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'),
};
}
2021-09-24 00:49:05 +00:00
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,
2024-07-24 00:31:40 +00:00
paddedLengths: (typeof PaddedLengths)[keyof typeof PaddedLengths]
2021-09-24 00:49:05 +00:00
): 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();
2021-09-24 00:49:05 +00:00
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);
}
2022-10-05 16:35:56 +00:00
/**
* Generate an integer between `min` and `max`, inclusive.
*/
export function randomInt(min: number, max: number): number {
return crypto.randomInt(min, max + 1);
}
2021-09-24 00:49:05 +00:00
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
}