@hackbg/miscreant-esm
Version:
(ESM port) Misuse resistant symmetric encryption library providing AES-SIV (RFC 5297), AES-PMAC-SIV, and STREAM constructions
116 lines (115 loc) • 3.85 kB
JavaScript
import {equal} from "./internals/constant-time.dist.mjs";
import {wipe} from "./internals/wipe.dist.mjs";
import {xor} from "./internals/xor.dist.mjs";
import {IntegrityError, NotImplementedError} from "./exceptions.dist.mjs";
import Block from "./internals/block.dist.mjs";
import {CMAC} from "./mac/cmac.dist.mjs";
import {PMAC} from "./mac/pmac.dist.mjs";
import {WebCryptoProvider} from "./providers/webcrypto.dist.mjs";
export const MAX_ASSOCIATED_DATA = 126;
export class SIV {
static async importKey(keyData, alg, provider = new WebCryptoProvider()) {
if (keyData.length !== 32 && keyData.length !== 64) {
throw new Error(`AES-SIV: key must be 32 or 64-bytes (got ${keyData.length}`);
}
const macKey = keyData.subarray(0, keyData.length / 2 | 0);
const encKey = keyData.subarray(keyData.length / 2 | 0);
let mac;
switch (alg) {
case "AES-SIV":
mac = await CMAC.importKey(provider, macKey);
break;
case "AES-CMAC-SIV":
mac = await CMAC.importKey(provider, macKey);
break;
case "AES-PMAC-SIV":
mac = await PMAC.importKey(provider, macKey);
break;
default:
throw new NotImplementedError(`Miscreant: algorithm not supported: ${alg}`);
}
const ctr = await provider.importCTRKey(encKey);
return new SIV(mac, ctr);
}
_mac;
_ctr;
_tmp1;
_tmp2;
constructor(mac, ctr) {
this._mac = mac;
this._ctr = ctr;
this._tmp1 = new Block();
this._tmp2 = new Block();
}
async seal(plaintext, associatedData) {
if (associatedData.length > MAX_ASSOCIATED_DATA) {
throw new Error("AES-SIV: too many associated data items");
}
const resultLength = Block.SIZE + plaintext.length;
const result = new Uint8Array(resultLength);
const iv = await this._s2v(associatedData, plaintext);
result.set(iv);
zeroIVBits(iv);
result.set(await this._ctr.encryptCtr(iv, plaintext), iv.length);
return result;
}
async open(sealed, associatedData) {
if (associatedData.length > MAX_ASSOCIATED_DATA) {
throw new Error("AES-SIV: too many associated data items");
}
if (sealed.length < Block.SIZE) {
throw new IntegrityError("AES-SIV: ciphertext is truncated");
}
const tag = sealed.subarray(0, Block.SIZE);
const iv = this._tmp1.data;
iv.set(tag);
zeroIVBits(iv);
const result = await this._ctr.encryptCtr(iv, sealed.subarray(Block.SIZE));
const expectedTag = await this._s2v(associatedData, result);
if (!equal(expectedTag, tag)) {
wipe(result);
throw new IntegrityError("AES-SIV: ciphertext verification failure!");
}
return result;
}
clear() {
this._tmp1.clear();
this._tmp2.clear();
this._ctr.clear();
this._mac.clear();
return this;
}
async _s2v(associated_data, plaintext) {
this._mac.reset();
this._tmp1.clear();
await this._mac.update(this._tmp1.data);
this._tmp2.clear();
this._tmp2.data.set(await this._mac.finish());
this._mac.reset();
for (const ad of associated_data) {
await this._mac.update(ad);
this._tmp1.clear();
this._tmp1.data.set(await this._mac.finish());
this._mac.reset();
this._tmp2.dbl();
xor(this._tmp2.data, this._tmp1.data);
}
this._tmp1.clear();
if (plaintext.length >= Block.SIZE) {
const n = plaintext.length - Block.SIZE;
this._tmp1.data.set(plaintext.subarray(n));
await this._mac.update(plaintext.subarray(0, n));
} else {
this._tmp1.data.set(plaintext);
this._tmp1.data[plaintext.length] = 0x80;
this._tmp2.dbl();
}
xor(this._tmp1.data, this._tmp2.data);
await this._mac.update(this._tmp1.data);
return this._mac.finish();
}
}
function zeroIVBits(iv) {
iv[iv.length - 8] &= 0x7f;
iv[iv.length - 4] &= 0x7f;
}