diff --git a/Cargo.lock b/Cargo.lock index 15b7509ec0..96909a83ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/cli/tests/unit/webcrypto_test.ts b/cli/tests/unit/webcrypto_test.ts index fec412937b..c4958fbdf1 100644 --- a/cli/tests/unit/webcrypto_test.ts +++ b/cli/tests/unit/webcrypto_test.ts @@ -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"; diff --git a/ext/crypto/00_crypto.js b/ext/crypto/00_crypto.js index 0a78bff2f9..5d216dbf45 100644 --- a/ext/crypto/00_crypto.js +++ b/ext/crypto/00_crypto.js @@ -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"); } diff --git a/ext/crypto/01_webidl.js b/ext/crypto/01_webidl.js index a6470ce9e5..04315204f3 100644 --- a/ext/crypto/01_webidl.js +++ b/ext/crypto/01_webidl.js @@ -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, diff --git a/ext/crypto/Cargo.toml b/ext/crypto/Cargo.toml index 3ab1cc5e8d..2ab3ff1713 100644 --- a/ext/crypto/Cargo.toml +++ b/ext/crypto/Cargo.toml @@ -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"] } diff --git a/ext/crypto/decrypt.rs b/ext/crypto/decrypt.rs index f487d7e344..90916f9c38 100644 --- a/ext/crypto/decrypt.rs +++ b/ext/crypto/decrypt.rs @@ -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, length: usize, }, + #[serde(rename = "AES-CTR", rename_all = "camelCase")] + AesCtr { + #[serde(with = "serde_bytes")] + counter: Vec, + 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( + key: &[u8], + counter: &[u8], + data: &[u8], +) -> Result, AnyError> +where + B: BlockEncrypt + NewBlockCipher, + F: CtrFlavor, +{ + let mut cipher = Ctr::::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, deno_core::anyhow::Error> { + let key = key.as_secret_key()?; + + match ctr_length { + 32 => match key_length { + 128 => decrypt_aes_ctr_gen::(key, counter, data), + 192 => decrypt_aes_ctr_gen::(key, counter, data), + 256 => decrypt_aes_ctr_gen::(key, counter, data), + _ => Err(type_error("invalid length")), + }, + 64 => match key_length { + 128 => decrypt_aes_ctr_gen::(key, counter, data), + 192 => decrypt_aes_ctr_gen::(key, counter, data), + 256 => decrypt_aes_ctr_gen::(key, counter, data), + _ => Err(type_error("invalid length")), + }, + 128 => match key_length { + 128 => decrypt_aes_ctr_gen::(key, counter, data), + 192 => decrypt_aes_ctr_gen::(key, counter, data), + 256 => decrypt_aes_ctr_gen::(key, counter, data), + _ => Err(type_error("invalid length")), + }, + _ => Err(type_error( + "invalid counter length. Currently supported 32/64/128 bits", + )), + } +} diff --git a/ext/crypto/encrypt.rs b/ext/crypto/encrypt.rs index 87e3fd2e08..99f4762d09 100644 --- a/ext/crypto/encrypt.rs +++ b/ext/crypto/encrypt.rs @@ -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, length: usize, }, + #[serde(rename = "AES-CTR", rename_all = "camelCase")] + AesCtr { + #[serde(with = "serde_bytes")] + counter: Vec, + ctr_length: usize, + key_length: usize, + }, } pub async fn op_crypto_encrypt( _state: Rc>, @@ -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( + key: &[u8], + counter: &[u8], + data: &[u8], +) -> Result, AnyError> +where + B: BlockEncrypt + NewBlockCipher, + F: CtrFlavor, +{ + let mut cipher = Ctr::::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, AnyError> { + let key = key.as_secret_key()?; + + match ctr_length { + 32 => match key_length { + 128 => encrypt_aes_ctr_gen::(key, counter, data), + 192 => encrypt_aes_ctr_gen::(key, counter, data), + 256 => encrypt_aes_ctr_gen::(key, counter, data), + _ => Err(type_error("invalid length")), + }, + 64 => match key_length { + 128 => encrypt_aes_ctr_gen::(key, counter, data), + 192 => encrypt_aes_ctr_gen::(key, counter, data), + 256 => encrypt_aes_ctr_gen::(key, counter, data), + _ => Err(type_error("invalid length")), + }, + 128 => match key_length { + 128 => encrypt_aes_ctr_gen::(key, counter, data), + 192 => encrypt_aes_ctr_gen::(key, counter, data), + 256 => encrypt_aes_ctr_gen::(key, counter, data), + _ => Err(type_error("invalid length")), + }, + _ => Err(type_error( + "invalid counter length. Currently supported 32/64/128 bits", + )), + } +} diff --git a/ext/crypto/lib.deno_crypto.d.ts b/ext/crypto/lib.deno_crypto.d.ts index 6a32557454..f7d735721c 100644 --- a/ext/crypto/lib.deno_crypto.d.ts +++ b/ext/crypto/lib.deno_crypto.d.ts @@ -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; encrypt( - algorithm: AlgorithmIdentifier | RsaOaepParams | AesCbcParams, + algorithm: + | AlgorithmIdentifier + | RsaOaepParams + | AesCbcParams + | AesCtrParams, key: CryptoKey, data: BufferSource, ): Promise; decrypt( - algorithm: AlgorithmIdentifier | RsaOaepParams | AesCbcParams, + algorithm: + | AlgorithmIdentifier + | RsaOaepParams + | AesCbcParams + | AesCtrParams, key: CryptoKey, data: BufferSource, ): Promise; diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json index f4d2869f4e..5d6128cb38 100644 --- a/tools/wpt/expectation.json +++ b/tools/wpt/expectation.json @@ -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",