Skip to content

Commit

Permalink
aes: switch from native async to noble sync
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Sep 9, 2024
1 parent 28e4a10 commit 3c1865a
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 112 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"esm"
],
"dependencies": {
"@noble/ciphers": "0.6.0",
"@noble/curves": "1.6.0",
"@noble/hashes": "1.5.0",
"@scure/bip32": "1.5.0",
Expand Down
123 changes: 23 additions & 100 deletions src/aes.ts
Original file line number Diff line number Diff line change
@@ -1,124 +1,47 @@
import { crypto as cr } from "@noble/hashes/crypto";
import { concatBytes, equalsBytes } from "./utils.js";
import { ctr, cbc } from "@noble/ciphers/aes";
import type { CipherWithOutput } from "@noble/ciphers/utils";

const crypto: any = { web: cr };

function validateOpt(key: Uint8Array, iv: Uint8Array, mode: string) {
function getCipher(
key: Uint8Array,
iv: Uint8Array,
mode: string,
pkcs7PaddingEnabled = true
): CipherWithOutput {
if (!mode.startsWith("aes-")) {
throw new Error(`AES submodule doesn't support mode ${mode}`);
}
if (iv.length !== 16) {
throw new Error("AES: wrong IV length");
throw new Error("AES: unsupported mode");
}
if (
(mode.startsWith("aes-128") && key.length !== 16) ||
(mode.startsWith("aes-256") && key.length !== 32)
) {
const len = key.length;
if ((mode.startsWith("aes-128") && len !== 16) || (mode.startsWith("aes-256") && len !== 32)) {
throw new Error("AES: wrong key length");
}
}

async function getBrowserKey(
mode: string,
key: Uint8Array,
iv: Uint8Array
): Promise<[CryptoKey, AesCbcParams | AesCtrParams]> {
if (!crypto.web) {
throw new Error("Browser crypto not available.");
if (iv.length !== 16) {
throw new Error("AES: wrong IV length");
}
let keyMode: string | undefined;
if (["aes-128-cbc", "aes-256-cbc"].includes(mode)) {
keyMode = "cbc";
return cbc(key, iv, { disablePadding: !pkcs7PaddingEnabled });
}
if (["aes-128-ctr", "aes-256-ctr"].includes(mode)) {
keyMode = "ctr";
return ctr(key, iv);
}
if (!keyMode) {
throw new Error("AES: unsupported mode");
}
const wKey = await crypto.web.subtle.importKey(
"raw",
key,
{ name: `AES-${keyMode.toUpperCase()}`, length: key.length * 8 },
true,
["encrypt", "decrypt"]
);
// node.js uses whole 128 bit as a counter, without nonce, instead of 64 bit
// recommended by NIST SP800-38A
return [wKey, { name: `aes-${keyMode}`, iv, counter: iv, length: 128 }];
throw new Error("AES: unsupported mode");
}

export async function encrypt(
export function encrypt(
msg: Uint8Array,
key: Uint8Array,
iv: Uint8Array,
mode = "aes-128-ctr",
pkcs7PaddingEnabled = true
): Promise<Uint8Array> {
validateOpt(key, iv, mode);
if (crypto.web) {
const [wKey, wOpt] = await getBrowserKey(mode, key, iv);
const cipher = await crypto.web.subtle.encrypt(wOpt, wKey, msg);
// Remove PKCS7 padding on cbc mode by stripping end of message
let res = new Uint8Array(cipher);
if (!pkcs7PaddingEnabled && wOpt.name === "aes-cbc" && !(msg.length % 16)) {
res = res.slice(0, -16);
}
return res;
} else if (crypto.node) {
const cipher = crypto.node.createCipheriv(mode, key, iv);
cipher.setAutoPadding(pkcs7PaddingEnabled);
return concatBytes(cipher.update(msg), cipher.final());
} else {
throw new Error("The environment doesn't have AES module");
}
): Uint8Array {
return getCipher(key, iv, mode, pkcs7PaddingEnabled).encrypt(msg);
}

async function getPadding(
cypherText: Uint8Array,
key: Uint8Array,
iv: Uint8Array,
mode: string
) {
const lastBlock = cypherText.slice(-16);
for (let i = 0; i < 16; i++) {
// Undo xor of iv and fill with lastBlock ^ padding (16)
lastBlock[i] ^= iv[i] ^ 16;
}
const res = await encrypt(lastBlock, key, iv, mode);
return res.slice(0, 16);
}

export async function decrypt(
cypherText: Uint8Array,
export function decrypt(
ciphertext: Uint8Array,
key: Uint8Array,
iv: Uint8Array,
mode = "aes-128-ctr",
pkcs7PaddingEnabled = true
): Promise<Uint8Array> {
validateOpt(key, iv, mode);
if (crypto.web) {
const [wKey, wOpt] = await getBrowserKey(mode, key, iv);
// Add empty padding so Chrome will correctly decrypt message
if (!pkcs7PaddingEnabled && wOpt.name === "aes-cbc") {
const padding = await getPadding(cypherText, key, iv, mode);
cypherText = concatBytes(cypherText, padding);
}
const msg = await crypto.web.subtle.decrypt(wOpt, wKey, cypherText);
const msgBytes = new Uint8Array(msg);
// Safari always ignores padding (if no padding -> broken message)
if (wOpt.name === "aes-cbc") {
const encrypted = await encrypt(msgBytes, key, iv, mode);
if (!equalsBytes(encrypted, cypherText)) {
throw new Error("AES: wrong padding");
}
}
return msgBytes;
} else if (crypto.node) {
const decipher = crypto.node.createDecipheriv(mode, key, iv);
decipher.setAutoPadding(pkcs7PaddingEnabled);
return concatBytes(decipher.update(cypherText), decipher.final());
} else {
throw new Error("The environment doesn't have AES module");
}
): Uint8Array {
return getCipher(key, iv, mode, pkcs7PaddingEnabled).decrypt(ciphertext);
}
1 change: 1 addition & 0 deletions test/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 12 additions & 12 deletions test/test-vectors/aes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { decrypt, encrypt } from "ethereum-cryptography/aes";
import { hexToBytes, toHex } from "ethereum-cryptography/utils";
import { deepStrictEqual, rejects } from "./assert";
import { deepStrictEqual, throws } from "./assert";
// Test vectors taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf
const TEST_VECTORS = [
{
Expand Down Expand Up @@ -94,8 +94,8 @@ const TEST_VECTORS = [

describe("aes", () => {
for (const [i, vector] of TEST_VECTORS.entries()) {
it(`Should encrypt the test ${i} correctly`, async () => {
const encrypted = await encrypt(
it(`Should encrypt the test ${i} correctly`, () => {
const encrypted = encrypt(
hexToBytes(vector.msg),
hexToBytes(vector.key),
hexToBytes(vector.iv),
Expand All @@ -106,8 +106,8 @@ describe("aes", () => {
deepStrictEqual(toHex(encrypted), vector.cypherText);
});

it(`Should decrypt the test ${i} correctly`, async () => {
const decrypted = await decrypt(
it(`Should decrypt the test ${i} correctly`, () => {
const decrypted = decrypt(
hexToBytes(vector.cypherText),
hexToBytes(vector.key),
hexToBytes(vector.iv),
Expand All @@ -119,8 +119,8 @@ describe("aes", () => {
});
}

it("Should throw when not padding automatically and the message isn't the right size", async () => {
rejects(() =>
it("Should throw when not padding automatically and the message isn't the right size", () => {
throws(() =>
encrypt(
hexToBytes("abcd"),
hexToBytes("2b7e151628aed2a6abf7158809cf4f3c"),
Expand All @@ -131,8 +131,8 @@ describe("aes", () => {
);
});

it("Should throw when trying to use non-aes modes", async () => {
rejects(() =>
it("Should throw when trying to use non-aes modes", () => {
throws(() =>
encrypt(
hexToBytes("abcd"),
hexToBytes("2b7e151628aed2a6abf7158809cf4f3c"),
Expand All @@ -143,7 +143,7 @@ describe("aes", () => {
);
});

it("aes-ctr bug (browser/node result mismatch)", async () => {
it("aes-ctr bug (browser/node result mismatch)", () => {
// NOTE: full 0xff iv causes difference on counter overflow in CTR mode
const iv = "ffffffffffffffffffffffffffffffff";
const vectors = [
Expand Down Expand Up @@ -184,9 +184,9 @@ describe("aes", () => {
const msg = hexToBytes(v.msg);
const key = hexToBytes(v.key);
const iv = hexToBytes(v.iv);
const res = await encrypt(msg, key, iv, v.mode);
const res = encrypt(msg, key, iv, v.mode);
deepStrictEqual(toHex(res), v.result);
const clearText = await decrypt(res, key, iv, v.mode);
const clearText = decrypt(res, key, iv, v.mode);
deepStrictEqual(clearText, msg);
}
});
Expand Down

0 comments on commit 3c1865a

Please sign in to comment.