0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 09:31:22 -05:00

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

Fixes #13201.
This commit is contained in:
Sean Michael Wykes 2022-01-03 08:27:28 -03:00 committed by GitHub
parent a721c34c19
commit 9a42d65fc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 378 additions and 77 deletions

10
Cargo.lock generated
View file

@ -557,6 +557,15 @@ dependencies = [
"syn 1.0.65",
]
[[package]]
name = "ctr"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
dependencies = [
"cipher",
]
[[package]]
name = "cty"
version = "0.2.2"
@ -775,6 +784,7 @@ dependencies = [
"aes",
"base64 0.13.0",
"block-modes",
"ctr",
"deno_core",
"deno_web",
"elliptic-curve",

View file

@ -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() {
@ -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";

View file

@ -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",
@ -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");
}
@ -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");
}

View file

@ -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,

View file

@ -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"] }

View file

@ -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;
@ -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(
@ -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())
@ -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",
)),
}
}

View file

@ -2,7 +2,19 @@ use std::cell::RefCell;
use std::rc::Rc;
use crate::shared::*;
use aes::cipher::NewCipher;
use aes::BlockEncrypt;
use aes::NewBlockCipher;
use ctr::Ctr;
use block_modes::BlockMode;
use ctr::cipher::StreamCipher;
use ctr::flavors::Ctr128BE;
use ctr::flavors::Ctr32BE;
use ctr::flavors::Ctr64BE;
use ctr::flavors::CtrFlavor;
use deno_core::error::type_error;
use deno_core::error::AnyError;
use deno_core::OpState;
@ -41,6 +53,13 @@ pub enum EncryptAlgorithm {
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_encrypt(
_state: Rc<RefCell<OpState>>,
@ -55,6 +74,11 @@ pub async fn op_crypto_encrypt(
EncryptAlgorithm::AesCbc { iv, length } => {
encrypt_aes_cbc(key, length, iv, &data)
}
EncryptAlgorithm::AesCtr {
counter,
ctr_length,
key_length,
} => encrypt_aes_ctr(key, key_length, &counter, ctr_length, &data),
};
let buf = tokio::task::spawn_blocking(fun).await.unwrap()?;
Ok(buf.into())
@ -136,3 +160,56 @@ fn encrypt_aes_cbc(
};
Ok(ciphertext)
}
fn encrypt_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 ciphertext = data.to_vec();
cipher
.try_apply_keystream(&mut ciphertext)
.map_err(|_| operation_error("tried to encrypt too much data"))?;
Ok(ciphertext)
}
fn encrypt_aes_ctr(
key: RawKeyData,
key_length: usize,
counter: &[u8],
ctr_length: usize,
data: &[u8],
) -> Result<Vec<u8>, AnyError> {
let key = key.as_secret_key()?;
match ctr_length {
32 => match key_length {
128 => encrypt_aes_ctr_gen::<aes::Aes128, Ctr32BE>(key, counter, data),
192 => encrypt_aes_ctr_gen::<aes::Aes192, Ctr32BE>(key, counter, data),
256 => encrypt_aes_ctr_gen::<aes::Aes256, Ctr32BE>(key, counter, data),
_ => Err(type_error("invalid length")),
},
64 => match key_length {
128 => encrypt_aes_ctr_gen::<aes::Aes128, Ctr64BE>(key, counter, data),
192 => encrypt_aes_ctr_gen::<aes::Aes192, Ctr64BE>(key, counter, data),
256 => encrypt_aes_ctr_gen::<aes::Aes256, Ctr64BE>(key, counter, data),
_ => Err(type_error("invalid length")),
},
128 => match key_length {
128 => encrypt_aes_ctr_gen::<aes::Aes128, Ctr128BE>(key, counter, data),
192 => encrypt_aes_ctr_gen::<aes::Aes192, Ctr128BE>(key, counter, data),
256 => encrypt_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",
)),
}
}

View file

@ -62,6 +62,11 @@ interface AesCbcParams extends Algorithm {
iv: BufferSource;
}
interface AesCtrParams extends Algorithm {
counter: BufferSource;
length: number;
}
interface HmacKeyGenParams extends Algorithm {
hash: HashAlgorithmIdentifier;
length?: number;
@ -239,12 +244,20 @@ interface SubtleCrypto {
data: BufferSource,
): Promise<ArrayBuffer>;
encrypt(
algorithm: AlgorithmIdentifier | RsaOaepParams | AesCbcParams,
algorithm:
| AlgorithmIdentifier
| RsaOaepParams
| AesCbcParams
| AesCtrParams,
key: CryptoKey,
data: BufferSource,
): Promise<ArrayBuffer>;
decrypt(
algorithm: AlgorithmIdentifier | RsaOaepParams | AesCbcParams,
algorithm:
| AlgorithmIdentifier
| RsaOaepParams
| AesCbcParams
| AesCtrParams,
key: CryptoKey,
data: BufferSource,
): Promise<ArrayBuffer>;

View file

@ -39,80 +39,10 @@
"digest.https.any.worker.html": true
},
"encrypt_decrypt": {
"aes_cbc.https.any.html": [
"AES-CBC 128-bit key with mismatched key and algorithm",
"AES-CBC 192-bit key with mismatched key and algorithm",
"AES-CBC 256-bit key with mismatched key and algorithm"
],
"aes_cbc.https.any.worker.html": [
"AES-CBC 128-bit key with mismatched key and algorithm",
"AES-CBC 192-bit key with mismatched key and algorithm",
"AES-CBC 256-bit key with mismatched key and algorithm"
],
"aes_ctr.https.any.html": [
"AES-CTR 128-bit key",
"AES-CTR 192-bit key",
"AES-CTR 256-bit key",
"AES-CTR 128-bit key with altered plaintext",
"AES-CTR 192-bit key with altered plaintext",
"AES-CTR 256-bit key with altered plaintext",
"AES-CTR 128-bit key decryption",
"AES-CTR 192-bit key decryption",
"AES-CTR 256-bit key decryption",
"AES-CTR 128-bit key decryption with altered ciphertext",
"AES-CTR 192-bit key decryption with altered ciphertext",
"AES-CTR 256-bit key decryption with altered ciphertext",
"AES-CTR 128-bit key without encrypt usage",
"AES-CTR 192-bit key without encrypt usage",
"AES-CTR 256-bit key without encrypt usage",
"AES-CTR 128-bit key without decrypt usage",
"AES-CTR 192-bit key without decrypt usage",
"AES-CTR 256-bit key without decrypt usage",
"AES-CTR 128-bit key, 0-bit counter",
"AES-CTR 128-bit key, 129-bit counter",
"AES-CTR 192-bit key, 0-bit counter",
"AES-CTR 192-bit key, 129-bit counter",
"AES-CTR 256-bit key, 0-bit counter",
"AES-CTR 256-bit key, 129-bit counter",
"AES-CTR 128-bit key, 0-bit counter decryption",
"AES-CTR 128-bit key, 129-bit counter decryption",
"AES-CTR 192-bit key, 0-bit counter decryption",
"AES-CTR 192-bit key, 129-bit counter decryption",
"AES-CTR 256-bit key, 0-bit counter decryption",
"AES-CTR 256-bit key, 129-bit counter decryption"
],
"aes_ctr.https.any.worker.html": [
"AES-CTR 128-bit key",
"AES-CTR 192-bit key",
"AES-CTR 256-bit key",
"AES-CTR 128-bit key with altered plaintext",
"AES-CTR 192-bit key with altered plaintext",
"AES-CTR 256-bit key with altered plaintext",
"AES-CTR 128-bit key decryption",
"AES-CTR 192-bit key decryption",
"AES-CTR 256-bit key decryption",
"AES-CTR 128-bit key decryption with altered ciphertext",
"AES-CTR 192-bit key decryption with altered ciphertext",
"AES-CTR 256-bit key decryption with altered ciphertext",
"AES-CTR 128-bit key without encrypt usage",
"AES-CTR 192-bit key without encrypt usage",
"AES-CTR 256-bit key without encrypt usage",
"AES-CTR 128-bit key without decrypt usage",
"AES-CTR 192-bit key without decrypt usage",
"AES-CTR 256-bit key without decrypt usage",
"AES-CTR 128-bit key, 0-bit counter",
"AES-CTR 128-bit key, 129-bit counter",
"AES-CTR 192-bit key, 0-bit counter",
"AES-CTR 192-bit key, 129-bit counter",
"AES-CTR 256-bit key, 0-bit counter",
"AES-CTR 256-bit key, 129-bit counter",
"AES-CTR 128-bit key, 0-bit counter decryption",
"AES-CTR 128-bit key, 129-bit counter decryption",
"AES-CTR 192-bit key, 0-bit counter decryption",
"AES-CTR 192-bit key, 129-bit counter decryption",
"AES-CTR 256-bit key, 0-bit counter decryption",
"AES-CTR 256-bit key, 129-bit counter decryption"
],
"aes_cbc.https.any.html": true,
"aes_cbc.https.any.worker.html": true,
"aes_ctr.https.any.html": true,
"aes_ctr.https.any.worker.html": true,
"aes_gcm.https.any.html": [
"AES-GCM 128-bit key, 32-bit tag",
"AES-GCM 128-bit key, no additional data, 32-bit tag",