Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ext/crypto) - support encrypt/decrypt with AES-CTR #13177

Merged
merged 4 commits into from
Jan 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
cryptographix marked this conversation as resolved.
Show resolved Hide resolved
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