Skip to content

Commit

Permalink
feat(ext/crypto): support AES-CTR encrypt/decrypt (#13177)
Browse files Browse the repository at this point in the history
Fixes #13201.
  • Loading branch information
cryptographix authored Jan 3, 2022
1 parent a721c34 commit 9a42d65
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 77 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

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

111 changes: 110 additions & 1 deletion cli/tests/unit/webcrypto_test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { assert, assertEquals, assertRejects } from "./test_util.ts";
import {
assert,
assertEquals,
assertNotEquals,
assertRejects,
} from "./test_util.ts";

// https://github.com/denoland/deno/issues/11664
Deno.test(async function testImportArrayBufferKey() {
Expand Down Expand Up @@ -608,6 +613,110 @@ Deno.test(async function testAesCbcEncryptDecrypt() {
assertEquals(new Uint8Array(decrypted), new Uint8Array([1, 2, 3, 4, 5, 6]));
});

Deno.test(async function testAesCtrEncryptDecrypt() {
async function aesCtrRoundTrip(
key: CryptoKey,
counter: Uint8Array,
length: number,
plainText: Uint8Array,
) {
const cipherText = await crypto.subtle.encrypt(
{
name: "AES-CTR",
counter,
length,
},
key,
plainText,
);

assert(cipherText instanceof ArrayBuffer);
assertEquals(cipherText.byteLength, plainText.byteLength);
assertNotEquals(new Uint8Array(cipherText), plainText);

const decryptedText = await crypto.subtle.decrypt(
{
name: "AES-CTR",
counter,
length,
},
key,
cipherText,
);

assert(decryptedText instanceof ArrayBuffer);
assertEquals(decryptedText.byteLength, plainText.byteLength);
assertEquals(new Uint8Array(decryptedText), plainText);
}
for (const keySize of [128, 192, 256]) {
const key = await crypto.subtle.generateKey(
{ name: "AES-CTR", length: keySize },
true,
["encrypt", "decrypt"],
) as CryptoKey;

// test normal operation
for (const length of [128 /*, 64, 128 */]) {
const counter = await crypto.getRandomValues(new Uint8Array(16));

await aesCtrRoundTrip(
key,
counter,
length,
new Uint8Array([1, 2, 3, 4, 5, 6]),
);
}

// test counter-wrapping
for (const length of [32, 64, 128]) {
const plaintext1 = await crypto.getRandomValues(new Uint8Array(32));
const counter = new Uint8Array(16);

// fixed upper part
for (let off = 0; off < 16 - (length / 8); ++off) {
counter[off] = off;
}
const ciphertext1 = await crypto.subtle.encrypt(
{
name: "AES-CTR",
counter,
length,
},
key,
plaintext1,
);

// Set lower [length] counter bits to all '1's
for (let off = 16 - (length / 8); off < 16; ++off) {
counter[off] = 0xff;
}

// = [ 1 block of 0x00 + plaintext1 ]
const plaintext2 = new Uint8Array(48);
plaintext2.set(plaintext1, 16);

const ciphertext2 = await crypto.subtle.encrypt(
{
name: "AES-CTR",
counter,
length,
},
key,
plaintext2,
);

// If counter wrapped, 2nd block of ciphertext2 should be equal to 1st block of ciphertext1
// since ciphertext1 used counter = 0x00...00
// and ciphertext2 used counter = 0xFF..FF which should wrap to 0x00..00 without affecting
// higher bits
assertEquals(
new Uint8Array(ciphertext1),
new Uint8Array(ciphertext2).slice(16),
);
}
}
});

// TODO(@littledivy): Enable WPT when we have importKey support
Deno.test(async function testECDH() {
const namedCurve = "P-256";
Expand Down
68 changes: 68 additions & 0 deletions ext/crypto/00_crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,12 @@
"encrypt": {
"RSA-OAEP": "RsaOaepParams",
"AES-CBC": "AesCbcParams",
"AES-CTR": "AesCtrParams",
},
"decrypt": {
"RSA-OAEP": "RsaOaepParams",
"AES-CBC": "AesCbcParams",
"AES-CTR": "AesCtrParams",
},
"get key length": {
"AES-CBC": "AesDerivedKeyParams",
Expand Down Expand Up @@ -605,6 +607,39 @@
// 6.
return plainText.buffer;
}
case "AES-CTR": {
normalizedAlgorithm.counter = copyBuffer(normalizedAlgorithm.counter);

// 1.
if (normalizedAlgorithm.counter.byteLength !== 16) {
throw new DOMException(
"Counter vector must be 16 bytes",
"OperationError",
);
}

// 2.
if (
normalizedAlgorithm.length === 0 || normalizedAlgorithm.length > 128
) {
throw new DOMException(
"Counter length must not be 0 or greater than 128",
"OperationError",
);
}

// 3.
const cipherText = await core.opAsync("op_crypto_decrypt", {
key: keyData,
algorithm: "AES-CTR",
keyLength: key[_algorithm].length,
counter: normalizedAlgorithm.counter,
ctrLength: normalizedAlgorithm.length,
}, data);

// 4.
return cipherText.buffer;
}
default:
throw new DOMException("Not implemented", "NotSupportedError");
}
Expand Down Expand Up @@ -3431,6 +3466,39 @@
// 4.
return cipherText.buffer;
}
case "AES-CTR": {
normalizedAlgorithm.counter = copyBuffer(normalizedAlgorithm.counter);

// 1.
if (normalizedAlgorithm.counter.byteLength !== 16) {
throw new DOMException(
"Counter vector must be 16 bytes",
"OperationError",
);
}

// 2.
if (
normalizedAlgorithm.length == 0 || normalizedAlgorithm.length > 128
) {
throw new DOMException(
"Counter length must not be 0 or greater than 128",
"OperationError",
);
}

// 3.
const cipherText = await core.opAsync("op_crypto_encrypt", {
key: keyData,
algorithm: "AES-CTR",
keyLength: key[_algorithm].length,
counter: normalizedAlgorithm.counter,
ctrLength: normalizedAlgorithm.length,
}, data);

// 4.
return cipherText.buffer;
}
default:
throw new DOMException("Not implemented", "NotSupportedError");
}
Expand Down
18 changes: 18 additions & 0 deletions ext/crypto/01_webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,24 @@
webidl.converters.AesCbcParams = webidl
.createDictionaryConverter("AesCbcParams", dictAesCbcParams);

const dictAesCtrParams = [
...dictAlgorithm,
{
key: "counter",
converter: webidl.converters["BufferSource"],
required: true,
},
{
key: "length",
converter: (V, opts) =>
webidl.converters["unsigned short"](V, { ...opts, enforceRange: true }),
required: true,
},
];

webidl.converters.AesCtrParams = webidl
.createDictionaryConverter("AesCtrParams", dictAesCtrParams);

webidl.converters.CryptoKey = webidl.createInterfaceConverter(
"CryptoKey",
CryptoKey,
Expand Down
1 change: 1 addition & 0 deletions ext/crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ path = "lib.rs"
aes = "0.7.5"
base64 = "0.13.0"
block-modes = "0.8.1"
ctr = "0.8.0"
deno_core = { version = "0.112.0", path = "../../core" }
deno_web = { version = "0.61.0", path = "../web" }
elliptic-curve = { version = "0.10.6", features = ["std", "pem"] }
Expand Down
75 changes: 75 additions & 0 deletions ext/crypto/decrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@ use std::cell::RefCell;
use std::rc::Rc;

use crate::shared::*;
use aes::BlockEncrypt;
use aes::NewBlockCipher;
use block_modes::BlockMode;
use ctr::cipher::NewCipher;
use ctr::cipher::StreamCipher;
use ctr::flavors::Ctr128BE;
use ctr::flavors::Ctr32BE;
use ctr::flavors::Ctr64BE;
use ctr::flavors::CtrFlavor;
use ctr::Ctr;
use deno_core::error::custom_error;
use deno_core::error::type_error;
use deno_core::error::AnyError;
use deno_core::OpState;
use deno_core::ZeroCopyBuf;
Expand Down Expand Up @@ -39,6 +49,13 @@ pub enum DecryptAlgorithm {
iv: Vec<u8>,
length: usize,
},
#[serde(rename = "AES-CTR", rename_all = "camelCase")]
AesCtr {
#[serde(with = "serde_bytes")]
counter: Vec<u8>,
ctr_length: usize,
key_length: usize,
},
}

pub async fn op_crypto_decrypt(
Expand All @@ -54,6 +71,11 @@ pub async fn op_crypto_decrypt(
DecryptAlgorithm::AesCbc { iv, length } => {
decrypt_aes_cbc(key, length, iv, &data)
}
DecryptAlgorithm::AesCtr {
counter,
ctr_length,
key_length,
} => decrypt_aes_ctr(key, key_length, &counter, ctr_length, &data),
};
let buf = tokio::task::spawn_blocking(fun).await.unwrap()?;
Ok(buf.into())
Expand Down Expand Up @@ -153,3 +175,56 @@ fn decrypt_aes_cbc(
// 6.
Ok(plaintext)
}

fn decrypt_aes_ctr_gen<B, F>(
key: &[u8],
counter: &[u8],
data: &[u8],
) -> Result<Vec<u8>, AnyError>
where
B: BlockEncrypt + NewBlockCipher,
F: CtrFlavor<B::BlockSize>,
{
let mut cipher = Ctr::<B, F>::new(key.into(), counter.into());

let mut plaintext = data.to_vec();
cipher
.try_apply_keystream(&mut plaintext)
.map_err(|_| operation_error("tried to decrypt too much data"))?;

Ok(plaintext)
}

fn decrypt_aes_ctr(
key: RawKeyData,
key_length: usize,
counter: &[u8],
ctr_length: usize,
data: &[u8],
) -> Result<Vec<u8>, deno_core::anyhow::Error> {
let key = key.as_secret_key()?;

match ctr_length {
32 => match key_length {
128 => decrypt_aes_ctr_gen::<aes::Aes128, Ctr32BE>(key, counter, data),
192 => decrypt_aes_ctr_gen::<aes::Aes192, Ctr32BE>(key, counter, data),
256 => decrypt_aes_ctr_gen::<aes::Aes256, Ctr32BE>(key, counter, data),
_ => Err(type_error("invalid length")),
},
64 => match key_length {
128 => decrypt_aes_ctr_gen::<aes::Aes128, Ctr64BE>(key, counter, data),
192 => decrypt_aes_ctr_gen::<aes::Aes192, Ctr64BE>(key, counter, data),
256 => decrypt_aes_ctr_gen::<aes::Aes256, Ctr64BE>(key, counter, data),
_ => Err(type_error("invalid length")),
},
128 => match key_length {
128 => decrypt_aes_ctr_gen::<aes::Aes128, Ctr128BE>(key, counter, data),
192 => decrypt_aes_ctr_gen::<aes::Aes192, Ctr128BE>(key, counter, data),
256 => decrypt_aes_ctr_gen::<aes::Aes256, Ctr128BE>(key, counter, data),
_ => Err(type_error("invalid length")),
},
_ => Err(type_error(
"invalid counter length. Currently supported 32/64/128 bits",
)),
}
}
Loading

0 comments on commit 9a42d65

Please sign in to comment.