download
raw
6.9 kB
import AESCrypto from './aes-crypto';
import AESDecryptor, { removePadding } from './aes-decryptor';
import { DecrypterAesMode } from './decrypter-aes-mode';
import FastAESKey from './fast-aes-key';
import { logger } from '../utils/logger';
import { appendUint8Array } from '../utils/mp4-tools';
import type { HlsConfig } from '../config';
const CHUNK_SIZE = 16; // 16 bytes, 128 bits
export default class Decrypter {
private logEnabled: boolean = true;
private removePKCS7Padding: boolean;
private subtle: SubtleCrypto | null = null;
private softwareDecrypter: AESDecryptor | null = null;
private key: ArrayBuffer | null = null;
private fastAesKey: FastAESKey | null = null;
private remainderData: Uint8Array<ArrayBuffer> | null = null;
private currentIV: ArrayBuffer | null = null;
private currentResult: ArrayBuffer | null = null;
private useSoftware: boolean;
private enableSoftwareAES: boolean;
constructor(config: HlsConfig, { removePKCS7Padding = true } = {}) {
this.enableSoftwareAES = config.enableSoftwareAES;
this.removePKCS7Padding = removePKCS7Padding;
// built in decryptor expects PKCS7 padding
if (removePKCS7Padding) {
try {
const browserCrypto = self.crypto;
if (browserCrypto) {
this.subtle =
browserCrypto.subtle ||
((browserCrypto as any).webkitSubtle as SubtleCrypto);
}
} catch (e) {
/* no-op */
}
}
this.useSoftware = !this.subtle;
}
destroy() {
this.subtle = null;
this.softwareDecrypter = null;
this.key = null;
this.fastAesKey = null;
this.remainderData = null;
this.currentIV = null;
this.currentResult = null;
}
public isSync() {
return this.useSoftware;
}
public flush(): Uint8Array<ArrayBuffer> | null {
const { currentResult, remainderData } = this;
if (!currentResult || remainderData) {
this.reset();
return null;
}
const data = new Uint8Array(currentResult);
this.reset();
if (this.removePKCS7Padding) {
return removePadding(data);
}
return data;
}
public reset() {
this.currentResult = null;
this.currentIV = null;
this.remainderData = null;
if (this.softwareDecrypter) {
this.softwareDecrypter = null;
}
}
public decrypt(
data: Uint8Array | ArrayBuffer,
key: ArrayBuffer,
iv: ArrayBuffer,
aesMode: DecrypterAesMode,
): Promise<ArrayBuffer> {
if (this.useSoftware) {
return new Promise((resolve, reject) => {
const dataView = ArrayBuffer.isView(data) ? data : new Uint8Array(data);
this.softwareDecrypt(dataView, key, iv, aesMode);
const decryptResult = this.flush();
if (decryptResult) {
resolve(decryptResult.buffer);
} else {
reject(new Error('[softwareDecrypt] Failed to decrypt data'));
}
});
}
return this.webCryptoDecrypt(new Uint8Array(data), key, iv, aesMode);
}
// Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
// data is handled in the flush() call
public softwareDecrypt(
data: Uint8Array,
key: ArrayBuffer,
iv: ArrayBuffer,
aesMode: DecrypterAesMode,
): ArrayBuffer | null {
const { currentIV, currentResult, remainderData } = this;
if (aesMode !== DecrypterAesMode.cbc || key.byteLength !== 16) {
logger.warn('SoftwareDecrypt: can only handle AES-128-CBC');
return null;
}
this.logOnce('JS AES decrypt');
// The output is staggered during progressive parsing - the current result is cached, and emitted on the next call
// This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached
// the end on flush(), but by that time we have already received all bytes for the segment.
// Progressive decryption does not work with WebCrypto
if (remainderData) {
data = appendUint8Array(remainderData, data);
this.remainderData = null;
}
// Byte length must be a multiple of 16 (AES-128 = 128 bit blocks = 16 bytes)
const currentChunk = this.getValidChunk(data);
if (!currentChunk.length) {
return null;
}
if (currentIV) {
iv = currentIV;
}
let softwareDecrypter = this.softwareDecrypter;
if (!softwareDecrypter) {
softwareDecrypter = this.softwareDecrypter = new AESDecryptor();
}
softwareDecrypter.expandKey(key);
const result = currentResult;
this.currentResult = softwareDecrypter.decrypt(currentChunk.buffer, 0, iv);
this.currentIV = currentChunk.slice(-16).buffer;
if (!result) {
return null;
}
return result;
}
public webCryptoDecrypt(
data: Uint8Array<ArrayBuffer>,
key: ArrayBuffer,
iv: ArrayBuffer,
aesMode: DecrypterAesMode,
): Promise<ArrayBuffer> {
if (this.key !== key || !this.fastAesKey) {
if (!this.subtle) {
return Promise.resolve(this.onWebCryptoError(data, key, iv, aesMode));
}
this.key = key;
this.fastAesKey = new FastAESKey(this.subtle, key, aesMode);
}
return this.fastAesKey
.expandKey()
.then((aesKey: CryptoKey) => {
// decrypt using web crypto
if (!this.subtle) {
return Promise.reject(new Error('web crypto not initialized'));
}
this.logOnce('WebCrypto AES decrypt');
const crypto = new AESCrypto(this.subtle, new Uint8Array(iv), aesMode);
return crypto.decrypt(data.buffer, aesKey);
})
.catch((err) => {
logger.warn(
`[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`,
);
return this.onWebCryptoError(data, key, iv, aesMode);
});
}
private onWebCryptoError(
data: Uint8Array,
key: ArrayBuffer,
iv: ArrayBuffer,
aesMode: DecrypterAesMode,
): ArrayBuffer | never {
const enableSoftwareAES = this.enableSoftwareAES;
if (enableSoftwareAES) {
this.useSoftware = true;
this.logEnabled = true;
this.softwareDecrypt(data, key, iv, aesMode);
const decryptResult = this.flush();
if (decryptResult) {
return decryptResult.buffer;
}
}
throw new Error(
'WebCrypto' +
(enableSoftwareAES ? ' and softwareDecrypt' : '') +
': failed to decrypt data',
);
}
private getValidChunk(data: Uint8Array): Uint8Array {
let currentChunk = data;
const splitPoint = data.length - (data.length % CHUNK_SIZE);
if (splitPoint !== data.length) {
currentChunk = data.slice(0, splitPoint);
this.remainderData = data.slice(splitPoint);
}
return currentChunk;
}
private logOnce(msg: string) {
if (!this.logEnabled) {
return;
}
logger.log(`[decrypter]: ${msg}`);
this.logEnabled = false;
}
}

Xet Storage Details

Size:
6.9 kB
·
Xet hash:
2094c94ff6119a57a5c36631352a443af0c5afb787e11c05b8744ccb66ef6bfc

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.