114 lines
No EOL
3.2 KiB
TypeScript
114 lines
No EOL
3.2 KiB
TypeScript
//Originally from https://github.com/Frooastside/node-widevine/blob/main/src/cmac.ts
|
|
|
|
import crypto from 'crypto';
|
|
|
|
export class AES_CMAC {
|
|
private readonly BLOCK_SIZE = 16;
|
|
private readonly XOR_RIGHT = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87]);
|
|
private readonly EMPTY_BLOCK_SIZE_BUFFER = Buffer.alloc(this.BLOCK_SIZE);
|
|
|
|
private _key: Buffer;
|
|
private _subkeys: { first: Buffer; second: Buffer };
|
|
|
|
public constructor(key: Buffer) {
|
|
if (![16, 24, 32].includes(key.length)) {
|
|
throw new Error('Key size must be 128, 192, or 256 bits.');
|
|
}
|
|
this._key = key;
|
|
this._subkeys = this._generateSubkeys();
|
|
}
|
|
|
|
public calculate(message: Buffer): Buffer {
|
|
const blockCount = this._getBlockCount(message);
|
|
|
|
let x = this.EMPTY_BLOCK_SIZE_BUFFER;
|
|
let y;
|
|
|
|
for (let i = 0; i < blockCount - 1; i++) {
|
|
const from = i * this.BLOCK_SIZE;
|
|
const block = message.subarray(from, from + this.BLOCK_SIZE);
|
|
y = this._xor(x, block);
|
|
x = this._aes(y);
|
|
}
|
|
|
|
y = this._xor(x, this._getLastBlock(message));
|
|
x = this._aes(y);
|
|
|
|
return x;
|
|
}
|
|
|
|
private _generateSubkeys(): { first: Buffer; second: Buffer } {
|
|
const l = this._aes(this.EMPTY_BLOCK_SIZE_BUFFER);
|
|
|
|
let first = this._bitShiftLeft(l);
|
|
if (l[0] & 0x80) {
|
|
first = this._xor(first, this.XOR_RIGHT);
|
|
}
|
|
|
|
let second = this._bitShiftLeft(first);
|
|
if (first[0] & 0x80) {
|
|
second = this._xor(second, this.XOR_RIGHT);
|
|
}
|
|
|
|
return { first: first, second: second };
|
|
}
|
|
|
|
private _getBlockCount(message: Buffer): number {
|
|
const blockCount = Math.ceil(message.length / this.BLOCK_SIZE);
|
|
return blockCount === 0 ? 1 : blockCount;
|
|
}
|
|
|
|
private _aes(message: Buffer): Buffer {
|
|
const cipher = crypto.createCipheriv(`aes-${this._key.length * 8}-cbc`, this._key, Buffer.alloc(this.BLOCK_SIZE));
|
|
const result = cipher.update(message).subarray(0, 16);
|
|
cipher.destroy();
|
|
return result;
|
|
}
|
|
|
|
private _getLastBlock(message: Buffer): Buffer {
|
|
const blockCount = this._getBlockCount(message);
|
|
const paddedBlock = this._padding(message, blockCount - 1);
|
|
|
|
let complete = false;
|
|
if (message.length > 0) {
|
|
complete = message.length % this.BLOCK_SIZE === 0;
|
|
}
|
|
|
|
const key = complete ? this._subkeys.first : this._subkeys.second;
|
|
return this._xor(paddedBlock, key);
|
|
}
|
|
|
|
private _padding(message: Buffer, blockIndex: number): Buffer {
|
|
const block = Buffer.alloc(this.BLOCK_SIZE);
|
|
|
|
const from = blockIndex * this.BLOCK_SIZE;
|
|
|
|
const slice = message.subarray(from, from + this.BLOCK_SIZE);
|
|
block.set(slice);
|
|
|
|
if (slice.length !== this.BLOCK_SIZE) {
|
|
block[slice.length] = 0x80;
|
|
}
|
|
|
|
return block;
|
|
}
|
|
|
|
private _bitShiftLeft(input: Buffer): Buffer {
|
|
const output = Buffer.alloc(input.length);
|
|
let overflow = 0;
|
|
for (let i = input.length - 1; i >= 0; i--) {
|
|
output[i] = (input[i] << 1) | overflow;
|
|
overflow = input[i] & 0x80 ? 1 : 0;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
private _xor(a: Buffer, b: Buffer): Buffer {
|
|
const length = Math.min(a.length, b.length);
|
|
const output = Buffer.alloc(length);
|
|
for (let i = 0; i < length; i++) {
|
|
output[i] = a[i] ^ b[i];
|
|
}
|
|
return output;
|
|
}
|
|
} |