Skip to content
This repository has been archived by the owner on Feb 5, 2021. It is now read-only.

Implemented optimal CMAC calculation for WebCrypto #156

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export interface IBlockCipher {

/** Encrypt 16-byte block in-place, replacing its contents with ciphertext. */
encryptBlock(block: Block): Promise<this>;

/** Encrypt batched data with CBC for optimal CMAC calculation. */
encryptBlockBatch(block: Block, data: Uint8Array): Promise<this>;
}

/**
Expand Down
71 changes: 34 additions & 37 deletions src/mac/cmac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import { IBlockCipher, ICryptoProvider, IMACLike } from "../interfaces";
import Block from "../internals/block";
import { xor } from "../internals/xor";

/**
* The AES-CMAC message authentication code
Expand All @@ -27,6 +26,7 @@ export class CMAC implements IMACLike {
private _buffer: Block;
private _bufferPos = 0;
private _finished = false;
private _data_accum = new Array();

constructor(
private _cipher: IBlockCipher,
Expand All @@ -40,6 +40,7 @@ export class CMAC implements IMACLike {
this._buffer.clear();
this._bufferPos = 0;
this._finished = false;
this._data_accum = new Array();
return this;
}

Expand All @@ -50,55 +51,51 @@ export class CMAC implements IMACLike {
}

public async update(data: Uint8Array): Promise<this> {
const left = Block.SIZE - this._bufferPos;
let dataPos = 0;
let dataLength = data.length;

if (dataLength > left) {
for (let i = 0; i < left; i++) {
this._buffer.data[this._bufferPos + i] ^= data[i];
}
dataLength -= left;
dataPos += left;
await this._cipher.encryptBlock(this._buffer);
this._bufferPos = 0;
}

// TODO: use AES-CBC with a span of multiple blocks instead of encryptBlock
// to encrypt many blocks in a single call to the WebCrypto API
while (dataLength > Block.SIZE) {
for (let i = 0; i < Block.SIZE; i++) {
this._buffer.data[i] ^= data[dataPos + i];
}
dataLength -= Block.SIZE;
dataPos += Block.SIZE;
await this._cipher.encryptBlock(this._buffer);
}

for (let i = 0; i < dataLength; i++) {
this._buffer.data[this._bufferPos++] ^= data[dataPos + i];
}

this._data_accum.push(data);
return this;
}

public async finish(): Promise<Uint8Array> {
if (!this._finished) {
// calculate total length and padding
let totalLength = this._data_accum.reduce((acc, value) => acc + value.length, 0);
let padding;
if (totalLength === 0) {
totalLength = padding = Block.SIZE;
} else {
padding = totalLength % Block.SIZE;
if (padding > 0) {
padding = Block.SIZE - padding;
totalLength += padding;
}
}

// construct single buffer with all data
const allData = new Uint8Array(totalLength);
let bufPos = 0;
for (const data of this._data_accum) {
allData.set(data, bufPos);
bufPos += data.length;
}

// Select which subkey to use.
const subkey = (this._bufferPos < Block.SIZE) ? this._subkey2 : this._subkey1;
const subkey = (padding > 0) ? this._subkey2 : this._subkey1;

// XOR in the subkey.
xor(this._buffer.data, subkey.data);
for (let i = 0; i < Block.SIZE; ++i) {
allData[totalLength - Block.SIZE + i] ^= subkey.data[i];
}

// Pad if needed.
if (this._bufferPos < Block.SIZE) {
this._buffer.data[this._bufferPos] ^= 0x80;
if (padding > 0) {
allData[totalLength - padding] ^= 0x80;
}

// Encrypt buffer to get the final digest.
await this._cipher.encryptBlock(this._buffer);
// Encrypt the full buffer to get the final digest.
await this._cipher.encryptBlockBatch(this._buffer, allData);

// Set finished flag.
// Free the accumulation buffer and set finished flag.
this._data_accum = new Array();
this._finished = true;
}

Expand Down
16 changes: 16 additions & 0 deletions src/providers/soft/aes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,22 @@ export default class SoftAes implements IBlockCipher {

return this._emptyPromise;
}

/**
* Encrypt batched data with CBC. This is made to be suitable for use in CMAC calculation.
* NOTE: This does not provide better performance compared to using encryptBlock(...) directly like before.
*/
public async encryptBlockBatch(block: Block, data: Uint8Array): Promise<this> {
let dataPos = 0;
while (dataPos < data.length) {
for (let i = 0; i < Block.SIZE; i++) {
block.data[i] ^= data[dataPos + i];
}
dataPos += Block.SIZE;
await this.encryptBlock(block);
}
return this._emptyPromise;
}
}

// Initialize generates encryption and decryption tables.
Expand Down
15 changes: 15 additions & 0 deletions src/providers/webcrypto/aes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,19 @@ export default class WebCryptoAes implements IBlockCipher {
block.data.set(new Uint8Array(ctBlock, 0, Block.SIZE));
return this._emptyPromise;
}

/**
* Encrypt data with CBC in a batch.
* This is made to be suitable and as optimal as possible for use in CMAC calculation.
*
* @param {Block} block - block to use as iv, final block of ciphertext will replace its contents
* @param {Uint8Array} data - data to encrypt
* @returns {Promise<this>}
*/
public async encryptBlockBatch(block: Block, data: Uint8Array): Promise<this> {
const params = { name: "AES-CBC", iv: block.data };
const ctBlock = await this._crypto.subtle.encrypt(params, this._key, data);
block.data.set(new Uint8Array(ctBlock, data.length - Block.SIZE, Block.SIZE));
return this._emptyPromise;
}
}