Skip to content

Commit

Permalink
Merge pull request #6 from kigawas/add-aes-aad
Browse files Browse the repository at this point in the history
Add webcrypto aes-gcm opitonal aad
  • Loading branch information
paulmillr authored Aug 31, 2023
2 parents 1393086 + 5521802 commit 6e133df
Show file tree
Hide file tree
Showing 5 changed files with 916 additions and 928 deletions.
50 changes: 38 additions & 12 deletions src/webcrypto/aes.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,59 @@
import { ensureBytes } from '../utils.js';
import { getWebcryptoSubtle } from './utils.js';

function generate(algo: string, length: number) {
/**
* AAD is only effective on AES-256-GCM or AES-128-GCM. Otherwise it'll be ignored
*/
export type Cipher = (
key: Uint8Array,
nonce: Uint8Array,
AAD?: Uint8Array
) => {
keyLength: number;
encrypt(plaintext: Uint8Array): Promise<Uint8Array>;
decrypt(ciphertext: Uint8Array): Promise<Uint8Array>;
};

type Algo = 'AES-CTR' | 'AES-GCM' | 'AES-CBC';
type BitLength = 128 | 256;

function getCryptParams(
algo: Algo,
nonce: Uint8Array,
AAD?: Uint8Array
): AesCbcParams | AesCtrParams | AesGcmParams {
const params = { name: algo };
if (algo === 'AES-CTR') {
return { ...params, counter: nonce, length: 64 } as AesCtrParams;
} else if (algo === 'AES-GCM') {
return { ...params, iv: nonce, additionalData: AAD } as AesGcmParams;
} else if (algo === 'AES-CBC') {
return { ...params, iv: nonce } as AesCbcParams;
} else {
throw new Error('unknown aes cipher');
}
}

function generate(algo: Algo, length: BitLength): Cipher {
const keyLength = length / 8;
const keyParams = { name: algo, length };
const cryptParams: Record<string, any> = { name: algo };
// const params: Record<string, any> = ({ e: algo, i: { name: algo, length } });

return (key: Uint8Array, nonce: Uint8Array) => {
return (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => {
ensureBytes(key, keyLength);
if (algo === 'AES-CTR') {
cryptParams.counter = nonce;
cryptParams.length = 64;
} else {
cryptParams.iv = nonce;
}
const cryptParams = getCryptParams(algo, nonce, AAD);

return {
keyLength,

async encrypt(plaintext: Uint8Array): Promise<Uint8Array> {
async encrypt(plaintext: Uint8Array) {
ensureBytes(plaintext);
const cr = getWebcryptoSubtle();
const iKey = await cr.importKey('raw', key, keyParams, true, ['encrypt']);
const cipher = await cr.encrypt(cryptParams, iKey, plaintext);
return new Uint8Array(cipher);
},

async decrypt(ciphertext: Uint8Array): Promise<Uint8Array> {
async decrypt(ciphertext: Uint8Array) {
ensureBytes(ciphertext);
const cr = getWebcryptoSubtle();
const iKey = await cr.importKey('raw', key, keyParams, true, ['decrypt']);
Expand Down
74 changes: 74 additions & 0 deletions test/gcm-siv.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const { deepStrictEqual, rejects } = require('assert');
const { should, describe } = require('micro-should');
const { hex } = require('@scure/base');
const utils = require('../utils.js');
const siv = require('../webcrypto/siv.js');
const { polyval } = require('../_polyval.js');

// https://datatracker.ietf.org/doc/html/rfc8452#appendix-C
const VECTORS = require('./vectors/siv.json');
const aes_gcm_siv_test = require('./wycheproof/aes_gcm_siv_test.json');

describe('AES-GCM-SIV', () => {
should('Polyval', () => {
const h = polyval(
hex.decode('25629347589242761d31f826ba4b757b'),
hex.decode('4f4f95668c83dfb6401762bb2d01a262d1a24ddd2721d006bbe45f20d3c9f362')
);
deepStrictEqual(hex.encode(h), 'f7a3b47b846119fae5b7866cf5e5b77e');
});
for (const flavor of ['aes128', 'aes256', 'counterWrap']) {
for (let i = 0; i < VECTORS[flavor].length; i++) {
const v = VECTORS[flavor][i];
should(`${flavor}(${i}): init`, async () => {
const { encKey, authKey } = await siv.deriveKeys(hex.decode(v.key), hex.decode(v.nonce));
deepStrictEqual(encKey, hex.decode(v.encKey));
deepStrictEqual(authKey, hex.decode(v.authKey));
});
should(`${flavor}(${i}): polyval`, async () => {
deepStrictEqual(
polyval(hex.decode(v.authKey), hex.decode(v.polyvalInput)),
hex.decode(v.polyvalResult)
);
});
should(`${flavor}(${i}).encrypt`, async () => {
let a = await siv.aes_256_gcm_siv(
hex.decode(v.key),
hex.decode(v.nonce),
hex.decode(v.AAD)
);
deepStrictEqual(await a.encrypt(hex.decode(v.plaintext)), hex.decode(v.result));
});
should(`${flavor}(${i}).decrypt`, async () => {
let a = await siv.aes_256_gcm_siv(
hex.decode(v.key),
hex.decode(v.nonce),
hex.decode(v.AAD)
);
deepStrictEqual(await a.decrypt(hex.decode(v.result)), hex.decode(v.plaintext));
});
}
}
});

describe('Wycheproof', () => {
for (const g of aes_gcm_siv_test.testGroups) {
const name = `Wycheproof/${g.ivSize}/${g.keySize}/${g.tagSize}/${g.type}`;
for (let i = 0; i < g.tests.length; i++) {
const t = g.tests[i];
should(`${name}: ${i}`, async () => {
const a = await siv.aes_256_gcm_siv(hex.decode(t.key), hex.decode(t.iv), hex.decode(t.aad));
const ct = utils.concatBytes(hex.decode(t.ct), hex.decode(t.tag));
const msg = hex.decode(t.msg);
if (t.result === 'valid') {
deepStrictEqual(await a.decrypt(ct), msg);
deepStrictEqual(await a.encrypt(msg), ct);
} else {
await rejects(async () => await a.decrypt(ct));
}
});
}
}
});

if (require.main === module) should.run();
Loading

0 comments on commit 6e133df

Please sign in to comment.