blob: ce249eeafc32df00e7a63d8c92e692122f78e1f4 [file] [log] [blame]
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {Aead} from '../aead/internal/aead';
import {SecurityException} from '../exception/security_exception';
import {Mac} from '../mac/internal/mac';
import * as aesCtr from './aes_ctr';
import * as Bytes from './bytes';
import * as hmac from './hmac';
import {IndCpaCipher} from './ind_cpa_cipher';
import * as Validators from './validators';
/**
* This primitive performs an encrypt-then-Mac operation on plaintext and
* additional authenticated data (aad).
*
* The Mac is computed over `aad || ciphertext || size of aad`, thus it
* doesn't violate https://en.wikipedia.org/wiki/Horton_Principle.
*
* This implementation is based on
* http://tools.ietf.org/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05.
*
* @final
*/
export class EncryptThenAuthenticate extends Aead {
/**
* @param ivSize the IV size in bytes
* @param tagSize the MAC tag size in bytes
* @throws {InvalidArgumentsException}
*/
constructor(
private readonly cipher: IndCpaCipher, private readonly ivSize: number,
private readonly mac: Mac, private readonly tagSize: number) {
super();
}
/**
* The plaintext is encrypted with an {@link IndCpaCipher}, then MAC
* is computed over `aad || ciphertext || t` where t is aad's length in bits
* represented as 64-bit bigendian unsigned integer. The final ciphertext
* format is `ind-cpa ciphertext || mac`.
*
* @override
*/
async encrypt(plaintext: Uint8Array, associatedData = new Uint8Array(0)):
Promise<Uint8Array> {
Validators.requireUint8Array(plaintext);
const payload = await this.cipher.encrypt(plaintext);
Validators.requireUint8Array(associatedData);
const aadLength = Bytes.fromNumber(associatedData.length * 8);
const mac = await this.mac.computeMac(
Bytes.concat(associatedData, payload, aadLength));
if (this.tagSize != mac.length) {
throw new SecurityException(
'invalid tag size, expected ' + this.tagSize + ' but got ' +
mac.length);
}
return Bytes.concat(payload, mac);
}
/**
* @override
*/
async decrypt(ciphertext: Uint8Array, associatedData = new Uint8Array(0)):
Promise<Uint8Array> {
Validators.requireUint8Array(ciphertext);
if (ciphertext.length < this.ivSize + this.tagSize) {
throw new SecurityException('ciphertext too short');
}
const payload = new Uint8Array(
ciphertext.subarray(0, ciphertext.length - this.tagSize));
Validators.requireUint8Array(associatedData);
const aadLength = Bytes.fromNumber(associatedData.length * 8);
const input = Bytes.concat(associatedData, payload, aadLength);
const tag = new Uint8Array(ciphertext.subarray(payload.length));
const isValidMac = await this.mac.verifyMac(tag, input);
if (!isValidMac) {
throw new SecurityException('invalid MAC');
}
return this.cipher.decrypt(payload);
}
}
/**
* @param ivSize the size of the IV
* @param hmacHashAlgo accepted names are SHA-1, SHA-256 and SHA-512
* @param tagSize the size of the tag
* @throws {InvalidArgumentsException}
* @static
*/
export async function aesCtrHmacFromRawKeys(
aesKey: Uint8Array, ivSize: number, hmacHashAlgo: string,
hmacKey: Uint8Array, tagSize: number): Promise<EncryptThenAuthenticate> {
Validators.requireUint8Array(aesKey);
Validators.requireUint8Array(hmacKey);
const cipher = await aesCtr.fromRawKey(aesKey, ivSize);
const mac = await hmac.fromRawKey(hmacHashAlgo, hmacKey, tagSize);
return new EncryptThenAuthenticate(cipher, ivSize, mac, tagSize);
}