Deno's AES GCM authentication tags are not verified
Description
Deno is a JavaScript, TypeScript, and WebAssembly runtime. Versions 1.46.0 through 2.1.6 have an issue that affects AES-256-GCM and AES-128-GCM in Deno in which the authentication tag is not being validated. This means tampered ciphertexts or incorrect keys might not be detected, which breaks the guarantees expected from AES-GCM. Older versions of Deno correctly threw errors in such cases, as does Node.js. Without authentication tag verification, AES-GCM degrades to essentially CTR mode, removing integrity protection. Authenticated data set with set_aad is also affected, as it is incorporated into the GCM hash (ghash) but this too is not validated, rendering AAD checks ineffective. Version 2.1.7 includes a patch that addresses this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
denocrates.io | >= 1.46.0, < 2.1.7 | 2.1.7 |
deno_nodecrates.io | >= 0.102.0, < 0.125.0 | 0.125.0 |
Affected products
1Patches
4a4003a5292bdfix(ext/node): GCM auth tag check on DechiperIv#final (#27733)
5 files changed · +19 −24
ext/node/lib.rs+0 −1 modified@@ -226,7 +226,6 @@ deno_core::extension!(deno_node, ops::crypto::op_node_decipheriv_decrypt, ops::crypto::op_node_decipheriv_final, ops::crypto::op_node_decipheriv_set_aad, - ops::crypto::op_node_decipheriv_take, ops::crypto::op_node_dh_compute_secret, ops::crypto::op_node_diffie_hellman, ops::crypto::op_node_ecdh_compute_public_key,
ext/node/ops/crypto/cipher.rs+5 −0 modified@@ -500,6 +500,11 @@ impl Decipher { auth_tag: &[u8], ) -> Result<(), DecipherError> { use Decipher::*; + + if input.is_empty() && !matches!(self, Aes128Gcm(_) | Aes256Gcm(_)) { + return Ok(()); + } + match (self, auto_pad) { (Aes128Cbc(decryptor), true) => { assert!(input.len() == 16);
ext/node/ops/crypto/mod.rs+0 −11 modified@@ -332,17 +332,6 @@ pub fn op_node_decipheriv_decrypt( true } -#[op2(fast)] -pub fn op_node_decipheriv_take( - state: &mut OpState, - #[smi] rid: u32, -) -> Result<(), cipher::DecipherContextError> { - let context = state.resource_table.take::<cipher::DecipherContext>(rid)?; - Rc::try_unwrap(context) - .map_err(|_| cipher::DecipherContextError::ContextInUse)?; - Ok(()) -} - #[op2] pub fn op_node_decipheriv_final( state: &mut OpState,
ext/node/polyfills/internal/crypto/cipher.ts+7 −9 modified@@ -18,7 +18,6 @@ import { op_node_decipheriv_decrypt, op_node_decipheriv_final, op_node_decipheriv_set_aad, - op_node_decipheriv_take, op_node_private_decrypt, op_node_private_encrypt, op_node_public_encrypt, @@ -352,14 +351,6 @@ export class Decipheriv extends Transform implements Cipher { } final(encoding: string = getDefaultEncoding()): Buffer | string { - if (!this.#needsBlockCache || this.#cache.cache.byteLength === 0) { - op_node_decipheriv_take(this.#context); - return encoding === "buffer" ? Buffer.from([]) : ""; - } - if (this.#cache.cache.byteLength != 16) { - throw new Error("Invalid final block size"); - } - let buf = new Buffer(16); op_node_decipheriv_final( this.#context, @@ -369,6 +360,13 @@ export class Decipheriv extends Transform implements Cipher { this.#authTag || NO_TAG, ); + if (!this.#needsBlockCache || this.#cache.cache.byteLength === 0) { + return encoding === "buffer" ? Buffer.from([]) : ""; + } + if (this.#cache.cache.byteLength != 16) { + throw new Error("Invalid final block size"); + } + buf = buf.subarray(0, 16 - buf.at(-1)); // Padded in Pkcs7 mode return encoding === "buffer" ? buf : buf.toString(encoding); }
tests/unit_node/crypto/crypto_cipher_gcm_test.ts+7 −3 modified@@ -4,7 +4,7 @@ import crypto from "node:crypto"; import { Buffer } from "node:buffer"; import testVectors128 from "./gcmEncryptExtIV128.json" with { type: "json" }; import testVectors256 from "./gcmEncryptExtIV256.json" with { type: "json" }; -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; const aesGcm = (bits: string, key: Uint8Array) => { const ALGO = bits == "128" ? `aes-128-gcm` : `aes-256-gcm`; @@ -123,7 +123,7 @@ Deno.test({ // Issue #27441 // https://github.com/denoland/deno/issues/27441 Deno.test({ - name: "aes-256-gcm supports IV of non standard length", + name: "aes-256-gcm supports IV of non standard length and auth tag check", fn() { const decipher = crypto.createDecipheriv( "aes-256-gcm", @@ -136,6 +136,10 @@ Deno.test({ "utf-8", ); assertEquals(decrypted, "this is a secret"); - decipher.final(); + assertThrows( + () => decipher.final(), + TypeError, + "Failed to authenticate data", + ); }, });
4f27d7cdc02efix(ext/node): GCM auth tag check on DechiperIv#final (#27733)
5 files changed · +19 −24
ext/node/lib.rs+0 −1 modified@@ -226,7 +226,6 @@ deno_core::extension!(deno_node, ops::crypto::op_node_decipheriv_decrypt, ops::crypto::op_node_decipheriv_final, ops::crypto::op_node_decipheriv_set_aad, - ops::crypto::op_node_decipheriv_take, ops::crypto::op_node_dh_compute_secret, ops::crypto::op_node_diffie_hellman, ops::crypto::op_node_ecdh_compute_public_key,
ext/node/ops/crypto/cipher.rs+5 −0 modified@@ -500,6 +500,11 @@ impl Decipher { auth_tag: &[u8], ) -> Result<(), DecipherError> { use Decipher::*; + + if input.is_empty() && !matches!(self, Aes128Gcm(_) | Aes256Gcm(_)) { + return Ok(()); + } + match (self, auto_pad) { (Aes128Cbc(decryptor), true) => { assert!(input.len() == 16);
ext/node/ops/crypto/mod.rs+0 −11 modified@@ -332,17 +332,6 @@ pub fn op_node_decipheriv_decrypt( true } -#[op2(fast)] -pub fn op_node_decipheriv_take( - state: &mut OpState, - #[smi] rid: u32, -) -> Result<(), cipher::DecipherContextError> { - let context = state.resource_table.take::<cipher::DecipherContext>(rid)?; - Rc::try_unwrap(context) - .map_err(|_| cipher::DecipherContextError::ContextInUse)?; - Ok(()) -} - #[op2] pub fn op_node_decipheriv_final( state: &mut OpState,
ext/node/polyfills/internal/crypto/cipher.ts+7 −9 modified@@ -18,7 +18,6 @@ import { op_node_decipheriv_decrypt, op_node_decipheriv_final, op_node_decipheriv_set_aad, - op_node_decipheriv_take, op_node_private_decrypt, op_node_private_encrypt, op_node_public_encrypt, @@ -352,14 +351,6 @@ export class Decipheriv extends Transform implements Cipher { } final(encoding: string = getDefaultEncoding()): Buffer | string { - if (!this.#needsBlockCache || this.#cache.cache.byteLength === 0) { - op_node_decipheriv_take(this.#context); - return encoding === "buffer" ? Buffer.from([]) : ""; - } - if (this.#cache.cache.byteLength != 16) { - throw new Error("Invalid final block size"); - } - let buf = new Buffer(16); op_node_decipheriv_final( this.#context, @@ -369,6 +360,13 @@ export class Decipheriv extends Transform implements Cipher { this.#authTag || NO_TAG, ); + if (!this.#needsBlockCache || this.#cache.cache.byteLength === 0) { + return encoding === "buffer" ? Buffer.from([]) : ""; + } + if (this.#cache.cache.byteLength != 16) { + throw new Error("Invalid final block size"); + } + buf = buf.subarray(0, 16 - buf.at(-1)); // Padded in Pkcs7 mode return encoding === "buffer" ? buf : buf.toString(encoding); }
tests/unit_node/crypto/crypto_cipher_gcm_test.ts+7 −3 modified@@ -4,7 +4,7 @@ import crypto from "node:crypto"; import { Buffer } from "node:buffer"; import testVectors128 from "./gcmEncryptExtIV128.json" with { type: "json" }; import testVectors256 from "./gcmEncryptExtIV256.json" with { type: "json" }; -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; const aesGcm = (bits: string, key: Uint8Array) => { const ALGO = bits == "128" ? `aes-128-gcm` : `aes-256-gcm`; @@ -123,7 +123,7 @@ Deno.test({ // Issue #27441 // https://github.com/denoland/deno/issues/27441 Deno.test({ - name: "aes-256-gcm supports IV of non standard length", + name: "aes-256-gcm supports IV of non standard length and auth tag check", fn() { const decipher = crypto.createDecipheriv( "aes-256-gcm", @@ -136,6 +136,10 @@ Deno.test({ "utf-8", ); assertEquals(decrypted, "this is a secret"); - decipher.final(); + assertThrows( + () => decipher.final(), + TypeError, + "Failed to authenticate data", + ); }, });
0d1beedfix(ext/node): add `CipherIv.setAutoPadding()` (#24940)
4 files changed · +146 −31
ext/node/lib.rs+1 −0 modified@@ -232,6 +232,7 @@ deno_core::extension!(deno_node, ops::crypto::op_node_decipheriv_decrypt, ops::crypto::op_node_decipheriv_final, ops::crypto::op_node_decipheriv_set_aad, + ops::crypto::op_node_decipheriv_take, ops::crypto::op_node_dh_compute_secret, ops::crypto::op_node_diffie_hellman, ops::crypto::op_node_ecdh_compute_public_key,
ext/node/ops/crypto/cipher.rs+104 −19 modified@@ -7,6 +7,7 @@ use aes::cipher::KeyIvInit; use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::Resource; +use digest::generic_array::GenericArray; use digest::KeyInit; use std::borrow::Cow; @@ -65,13 +66,14 @@ impl CipherContext { pub fn r#final( self, + auto_pad: bool, input: &[u8], output: &mut [u8], ) -> Result<Tag, AnyError> { Rc::try_unwrap(self.cipher) .map_err(|_| type_error("Cipher context is already in use"))? .into_inner() - .r#final(input, output) + .r#final(auto_pad, input, output) } } @@ -92,14 +94,15 @@ impl DecipherContext { pub fn r#final( self, + auto_pad: bool, input: &[u8], output: &mut [u8], auth_tag: &[u8], ) -> Result<(), AnyError> { Rc::try_unwrap(self.decipher) .map_err(|_| type_error("Decipher context is already in use"))? .into_inner() - .r#final(input, output, auth_tag) + .r#final(auto_pad, input, output, auth_tag) } } @@ -209,42 +212,82 @@ impl Cipher { } /// r#final encrypts the last block of the input data. - fn r#final(self, input: &[u8], output: &mut [u8]) -> Result<Tag, AnyError> { + fn r#final( + self, + auto_pad: bool, + input: &[u8], + output: &mut [u8], + ) -> Result<Tag, AnyError> { assert!(input.len() < 16); use Cipher::*; - match self { - Aes128Cbc(encryptor) => { + match (self, auto_pad) { + (Aes128Cbc(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; Ok(None) } - Aes128Ecb(encryptor) => { + (Aes128Cbc(mut encryptor), false) => { + encryptor.encrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(None) + } + (Aes128Ecb(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; Ok(None) } - Aes192Ecb(encryptor) => { + (Aes128Ecb(mut encryptor), false) => { + encryptor.encrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(None) + } + (Aes192Ecb(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; Ok(None) } - Aes256Ecb(encryptor) => { + (Aes192Ecb(mut encryptor), false) => { + encryptor.encrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(None) + } + (Aes256Ecb(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; Ok(None) } - Aes128Gcm(cipher) => Ok(Some(cipher.finish().to_vec())), - Aes256Gcm(cipher) => Ok(Some(cipher.finish().to_vec())), - Aes256Cbc(encryptor) => { + (Aes256Ecb(mut encryptor), false) => { + encryptor.encrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(None) + } + (Aes128Gcm(cipher), _) => Ok(Some(cipher.finish().to_vec())), + (Aes256Gcm(cipher), _) => Ok(Some(cipher.finish().to_vec())), + (Aes256Cbc(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; Ok(None) } + (Aes256Cbc(mut encryptor), false) => { + encryptor.encrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(None) + } } } } @@ -345,63 +388,105 @@ impl Decipher { /// r#final decrypts the last block of the input data. fn r#final( self, + auto_pad: bool, input: &[u8], output: &mut [u8], auth_tag: &[u8], ) -> Result<(), AnyError> { use Decipher::*; - match self { - Aes128Cbc(decryptor) => { + match (self, auto_pad) { + (Aes128Cbc(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } - Aes128Ecb(decryptor) => { + (Aes128Cbc(mut decryptor), false) => { + decryptor.decrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(()) + } + (Aes128Ecb(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } - Aes192Ecb(decryptor) => { + (Aes128Ecb(mut decryptor), false) => { + decryptor.decrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(()) + } + (Aes192Ecb(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } - Aes256Ecb(decryptor) => { + (Aes192Ecb(mut decryptor), false) => { + decryptor.decrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(()) + } + (Aes256Ecb(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } - Aes128Gcm(decipher) => { + (Aes256Ecb(mut decryptor), false) => { + decryptor.decrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(()) + } + (Aes128Gcm(decipher), true) => { let tag = decipher.finish(); if tag.as_slice() == auth_tag { Ok(()) } else { Err(type_error("Failed to authenticate data")) } } - Aes256Gcm(decipher) => { + (Aes128Gcm(_), false) => Err(type_error( + "setAutoPadding(false) not supported for Aes256Gcm yet", + )), + (Aes256Gcm(decipher), true) => { let tag = decipher.finish(); if tag.as_slice() == auth_tag { Ok(()) } else { Err(type_error("Failed to authenticate data")) } } - Aes256Cbc(decryptor) => { + (Aes256Gcm(_), false) => Err(type_error( + "setAutoPadding(false) not supported for Aes256Gcm yet", + )), + (Aes256Cbc(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } + (Aes256Cbc(mut decryptor), false) => { + decryptor.decrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(()) + } } } }
ext/node/ops/crypto/mod.rs+17 −4 modified@@ -262,13 +262,14 @@ pub fn op_node_cipheriv_encrypt( pub fn op_node_cipheriv_final( state: &mut OpState, #[smi] rid: u32, + auto_pad: bool, #[buffer] input: &[u8], - #[buffer] output: &mut [u8], + #[anybuffer] output: &mut [u8], ) -> Result<Option<Vec<u8>>, AnyError> { let context = state.resource_table.take::<cipher::CipherContext>(rid)?; let context = Rc::try_unwrap(context) .map_err(|_| type_error("Cipher context is already in use"))?; - context.r#final(input, output) + context.r#final(auto_pad, input, output) } #[op2(fast)] @@ -317,17 +318,29 @@ pub fn op_node_decipheriv_decrypt( } #[op2(fast)] +pub fn op_node_decipheriv_take( + state: &mut OpState, + #[smi] rid: u32, +) -> Result<(), AnyError> { + let context = state.resource_table.take::<cipher::DecipherContext>(rid)?; + Rc::try_unwrap(context) + .map_err(|_| type_error("Cipher context is already in use"))?; + Ok(()) +} + +#[op2] pub fn op_node_decipheriv_final( state: &mut OpState, #[smi] rid: u32, + auto_pad: bool, #[buffer] input: &[u8], - #[buffer] output: &mut [u8], + #[anybuffer] output: &mut [u8], #[buffer] auth_tag: &[u8], ) -> Result<(), AnyError> { let context = state.resource_table.take::<cipher::DecipherContext>(rid)?; let context = Rc::try_unwrap(context) .map_err(|_| type_error("Cipher context is already in use"))?; - context.r#final(input, output, auth_tag) + context.r#final(auto_pad, input, output, auth_tag) } #[op2]
ext/node/polyfills/internal/crypto/cipher.ts+24 −8 modified@@ -17,6 +17,7 @@ import { op_node_decipheriv_decrypt, op_node_decipheriv_final, op_node_decipheriv_set_aad, + op_node_decipheriv_take, op_node_private_decrypt, op_node_private_encrypt, op_node_public_encrypt, @@ -163,6 +164,8 @@ export class Cipheriv extends Transform implements Cipher { #authTag?: Buffer; + #autoPadding = true; + constructor( cipher: string, key: CipherKey, @@ -191,8 +194,13 @@ export class Cipheriv extends Transform implements Cipher { final(encoding: string = getDefaultEncoding()): Buffer | string { const buf = new Buffer(16); + + if (!this.#autoPadding && this.#cache.cache.byteLength != 16) { + throw new Error("Invalid final block size"); + } const maybeTag = op_node_cipheriv_final( this.#context, + this.#autoPadding, this.#cache.cache, buf, ); @@ -217,8 +225,8 @@ export class Cipheriv extends Transform implements Cipher { return this; } - setAutoPadding(_autoPadding?: boolean): this { - notImplemented("crypto.Cipheriv.prototype.setAutoPadding"); + setAutoPadding(autoPadding?: boolean): this { + this.#autoPadding = !!autoPadding; return this; } @@ -299,6 +307,8 @@ export class Decipheriv extends Transform implements Cipher { /** DecipherContext resource id */ #context: number; + #autoPadding = true; + /** ciphertext data cache */ #cache: BlockModeCache; @@ -333,18 +343,23 @@ export class Decipheriv extends Transform implements Cipher { } final(encoding: string = getDefaultEncoding()): Buffer | string { + if (!this.#needsBlockCache || this.#cache.cache.byteLength === 0) { + op_node_decipheriv_take(this.#context); + return encoding === "buffer" ? Buffer.from([]) : ""; + } + if (this.#cache.cache.byteLength != 16) { + throw new Error("Invalid final block size"); + } + let buf = new Buffer(16); op_node_decipheriv_final( this.#context, + this.#autoPadding, this.#cache.cache, buf, this.#authTag || NO_TAG, ); - if (!this.#needsBlockCache) { - return encoding === "buffer" ? Buffer.from([]) : ""; - } - buf = buf.subarray(0, 16 - buf.at(-1)); // Padded in Pkcs7 mode return encoding === "buffer" ? buf : buf.toString(encoding); } @@ -364,8 +379,9 @@ export class Decipheriv extends Transform implements Cipher { return this; } - setAutoPadding(_autoPadding?: boolean): this { - notImplemented("crypto.Decipheriv.prototype.setAutoPadding"); + setAutoPadding(autoPadding?: boolean): this { + this.#autoPadding = Boolean(autoPadding); + return this; } update(
0d1beed2e363fix(ext/node): add `CipherIv.setAutoPadding()` (#24940)
4 files changed · +146 −31
ext/node/lib.rs+1 −0 modified@@ -232,6 +232,7 @@ deno_core::extension!(deno_node, ops::crypto::op_node_decipheriv_decrypt, ops::crypto::op_node_decipheriv_final, ops::crypto::op_node_decipheriv_set_aad, + ops::crypto::op_node_decipheriv_take, ops::crypto::op_node_dh_compute_secret, ops::crypto::op_node_diffie_hellman, ops::crypto::op_node_ecdh_compute_public_key,
ext/node/ops/crypto/cipher.rs+104 −19 modified@@ -7,6 +7,7 @@ use aes::cipher::KeyIvInit; use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::Resource; +use digest::generic_array::GenericArray; use digest::KeyInit; use std::borrow::Cow; @@ -65,13 +66,14 @@ impl CipherContext { pub fn r#final( self, + auto_pad: bool, input: &[u8], output: &mut [u8], ) -> Result<Tag, AnyError> { Rc::try_unwrap(self.cipher) .map_err(|_| type_error("Cipher context is already in use"))? .into_inner() - .r#final(input, output) + .r#final(auto_pad, input, output) } } @@ -92,14 +94,15 @@ impl DecipherContext { pub fn r#final( self, + auto_pad: bool, input: &[u8], output: &mut [u8], auth_tag: &[u8], ) -> Result<(), AnyError> { Rc::try_unwrap(self.decipher) .map_err(|_| type_error("Decipher context is already in use"))? .into_inner() - .r#final(input, output, auth_tag) + .r#final(auto_pad, input, output, auth_tag) } } @@ -209,42 +212,82 @@ impl Cipher { } /// r#final encrypts the last block of the input data. - fn r#final(self, input: &[u8], output: &mut [u8]) -> Result<Tag, AnyError> { + fn r#final( + self, + auto_pad: bool, + input: &[u8], + output: &mut [u8], + ) -> Result<Tag, AnyError> { assert!(input.len() < 16); use Cipher::*; - match self { - Aes128Cbc(encryptor) => { + match (self, auto_pad) { + (Aes128Cbc(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; Ok(None) } - Aes128Ecb(encryptor) => { + (Aes128Cbc(mut encryptor), false) => { + encryptor.encrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(None) + } + (Aes128Ecb(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; Ok(None) } - Aes192Ecb(encryptor) => { + (Aes128Ecb(mut encryptor), false) => { + encryptor.encrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(None) + } + (Aes192Ecb(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; Ok(None) } - Aes256Ecb(encryptor) => { + (Aes192Ecb(mut encryptor), false) => { + encryptor.encrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(None) + } + (Aes256Ecb(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; Ok(None) } - Aes128Gcm(cipher) => Ok(Some(cipher.finish().to_vec())), - Aes256Gcm(cipher) => Ok(Some(cipher.finish().to_vec())), - Aes256Cbc(encryptor) => { + (Aes256Ecb(mut encryptor), false) => { + encryptor.encrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(None) + } + (Aes128Gcm(cipher), _) => Ok(Some(cipher.finish().to_vec())), + (Aes256Gcm(cipher), _) => Ok(Some(cipher.finish().to_vec())), + (Aes256Cbc(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; Ok(None) } + (Aes256Cbc(mut encryptor), false) => { + encryptor.encrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(None) + } } } } @@ -345,63 +388,105 @@ impl Decipher { /// r#final decrypts the last block of the input data. fn r#final( self, + auto_pad: bool, input: &[u8], output: &mut [u8], auth_tag: &[u8], ) -> Result<(), AnyError> { use Decipher::*; - match self { - Aes128Cbc(decryptor) => { + match (self, auto_pad) { + (Aes128Cbc(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } - Aes128Ecb(decryptor) => { + (Aes128Cbc(mut decryptor), false) => { + decryptor.decrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(()) + } + (Aes128Ecb(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } - Aes192Ecb(decryptor) => { + (Aes128Ecb(mut decryptor), false) => { + decryptor.decrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(()) + } + (Aes192Ecb(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } - Aes256Ecb(decryptor) => { + (Aes192Ecb(mut decryptor), false) => { + decryptor.decrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(()) + } + (Aes256Ecb(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } - Aes128Gcm(decipher) => { + (Aes256Ecb(mut decryptor), false) => { + decryptor.decrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(()) + } + (Aes128Gcm(decipher), true) => { let tag = decipher.finish(); if tag.as_slice() == auth_tag { Ok(()) } else { Err(type_error("Failed to authenticate data")) } } - Aes256Gcm(decipher) => { + (Aes128Gcm(_), false) => Err(type_error( + "setAutoPadding(false) not supported for Aes256Gcm yet", + )), + (Aes256Gcm(decipher), true) => { let tag = decipher.finish(); if tag.as_slice() == auth_tag { Ok(()) } else { Err(type_error("Failed to authenticate data")) } } - Aes256Cbc(decryptor) => { + (Aes256Gcm(_), false) => Err(type_error( + "setAutoPadding(false) not supported for Aes256Gcm yet", + )), + (Aes256Cbc(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } + (Aes256Cbc(mut decryptor), false) => { + decryptor.decrypt_block_b2b_mut( + GenericArray::from_slice(input), + GenericArray::from_mut_slice(output), + ); + Ok(()) + } } } }
ext/node/ops/crypto/mod.rs+17 −4 modified@@ -262,13 +262,14 @@ pub fn op_node_cipheriv_encrypt( pub fn op_node_cipheriv_final( state: &mut OpState, #[smi] rid: u32, + auto_pad: bool, #[buffer] input: &[u8], - #[buffer] output: &mut [u8], + #[anybuffer] output: &mut [u8], ) -> Result<Option<Vec<u8>>, AnyError> { let context = state.resource_table.take::<cipher::CipherContext>(rid)?; let context = Rc::try_unwrap(context) .map_err(|_| type_error("Cipher context is already in use"))?; - context.r#final(input, output) + context.r#final(auto_pad, input, output) } #[op2(fast)] @@ -317,17 +318,29 @@ pub fn op_node_decipheriv_decrypt( } #[op2(fast)] +pub fn op_node_decipheriv_take( + state: &mut OpState, + #[smi] rid: u32, +) -> Result<(), AnyError> { + let context = state.resource_table.take::<cipher::DecipherContext>(rid)?; + Rc::try_unwrap(context) + .map_err(|_| type_error("Cipher context is already in use"))?; + Ok(()) +} + +#[op2] pub fn op_node_decipheriv_final( state: &mut OpState, #[smi] rid: u32, + auto_pad: bool, #[buffer] input: &[u8], - #[buffer] output: &mut [u8], + #[anybuffer] output: &mut [u8], #[buffer] auth_tag: &[u8], ) -> Result<(), AnyError> { let context = state.resource_table.take::<cipher::DecipherContext>(rid)?; let context = Rc::try_unwrap(context) .map_err(|_| type_error("Cipher context is already in use"))?; - context.r#final(input, output, auth_tag) + context.r#final(auto_pad, input, output, auth_tag) } #[op2]
ext/node/polyfills/internal/crypto/cipher.ts+24 −8 modified@@ -17,6 +17,7 @@ import { op_node_decipheriv_decrypt, op_node_decipheriv_final, op_node_decipheriv_set_aad, + op_node_decipheriv_take, op_node_private_decrypt, op_node_private_encrypt, op_node_public_encrypt, @@ -163,6 +164,8 @@ export class Cipheriv extends Transform implements Cipher { #authTag?: Buffer; + #autoPadding = true; + constructor( cipher: string, key: CipherKey, @@ -191,8 +194,13 @@ export class Cipheriv extends Transform implements Cipher { final(encoding: string = getDefaultEncoding()): Buffer | string { const buf = new Buffer(16); + + if (!this.#autoPadding && this.#cache.cache.byteLength != 16) { + throw new Error("Invalid final block size"); + } const maybeTag = op_node_cipheriv_final( this.#context, + this.#autoPadding, this.#cache.cache, buf, ); @@ -217,8 +225,8 @@ export class Cipheriv extends Transform implements Cipher { return this; } - setAutoPadding(_autoPadding?: boolean): this { - notImplemented("crypto.Cipheriv.prototype.setAutoPadding"); + setAutoPadding(autoPadding?: boolean): this { + this.#autoPadding = !!autoPadding; return this; } @@ -299,6 +307,8 @@ export class Decipheriv extends Transform implements Cipher { /** DecipherContext resource id */ #context: number; + #autoPadding = true; + /** ciphertext data cache */ #cache: BlockModeCache; @@ -333,18 +343,23 @@ export class Decipheriv extends Transform implements Cipher { } final(encoding: string = getDefaultEncoding()): Buffer | string { + if (!this.#needsBlockCache || this.#cache.cache.byteLength === 0) { + op_node_decipheriv_take(this.#context); + return encoding === "buffer" ? Buffer.from([]) : ""; + } + if (this.#cache.cache.byteLength != 16) { + throw new Error("Invalid final block size"); + } + let buf = new Buffer(16); op_node_decipheriv_final( this.#context, + this.#autoPadding, this.#cache.cache, buf, this.#authTag || NO_TAG, ); - if (!this.#needsBlockCache) { - return encoding === "buffer" ? Buffer.from([]) : ""; - } - buf = buf.subarray(0, 16 - buf.at(-1)); // Padded in Pkcs7 mode return encoding === "buffer" ? buf : buf.toString(encoding); } @@ -364,8 +379,9 @@ export class Decipheriv extends Transform implements Cipher { return this; } - setAutoPadding(_autoPadding?: boolean): this { - notImplemented("crypto.Decipheriv.prototype.setAutoPadding"); + setAutoPadding(autoPadding?: boolean): this { + this.#autoPadding = Boolean(autoPadding); + return this; } update(
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-2x3r-hwv5-p32xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-24015ghsaADVISORY
- github.com/denoland/deno/commit/0d1beedghsax_refsource_MISCWEB
- github.com/denoland/deno/commit/0d1beed2e3633d71d5e288e0382b85be361ec13dghsaWEB
- github.com/denoland/deno/commit/4f27d7cdc02e3edfb9d36275341fb8185d6e99edghsax_refsource_MISCWEB
- github.com/denoland/deno/commit/a4003a5292bd0affefad3ecb24a8732886900f67ghsax_refsource_MISCWEB
- github.com/denoland/deno/security/advisories/GHSA-2x3r-hwv5-p32xghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.