Skip to content
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
1 change: 1 addition & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,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_auth_tag,
ops::crypto::op_node_dh_compute_secret,
ops::crypto::op_node_diffie_hellman,
ops::crypto::op_node_ecdh_compute_public_key,
Expand Down
94 changes: 80 additions & 14 deletions ext/node/ops/crypto/cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ enum Decipher {
Aes128Ecb(Box<ecb::Decryptor<aes::Aes128>>),
Aes192Ecb(Box<ecb::Decryptor<aes::Aes192>>),
Aes256Ecb(Box<ecb::Decryptor<aes::Aes256>>),
Aes128Gcm(Box<Aes128Gcm>),
Aes256Gcm(Box<Aes256Gcm>),
Aes128Gcm(Box<Aes128Gcm>, Option<usize>),
Aes256Gcm(Box<Aes256Gcm>, Option<usize>),
Aes256Cbc(Box<cbc::Decryptor<aes::Aes256>>),
Aes128Ctr(Box<ctr::Ctr128BE<aes::Aes128>>),
Aes192Ctr(Box<ctr::Ctr128BE<aes::Aes192>>),
Expand Down Expand Up @@ -123,12 +123,27 @@ impl DecipherContext {
algorithm: &str,
key: &[u8],
iv: &[u8],
auth_tag_length: Option<usize>,
) -> Result<Self, DecipherContextError> {
Ok(Self {
decipher: Rc::new(RefCell::new(Decipher::new(algorithm, key, iv)?)),
decipher: Rc::new(RefCell::new(Decipher::new(
algorithm,
key,
iv,
auth_tag_length,
)?)),
})
}

pub fn validate_auth_tag(
&self,
length: usize,
) -> Result<(), DecipherContextError> {
self.decipher.borrow().validate_auth_tag(length)?;

Ok(())
}

pub fn set_aad(&self, aad: &[u8]) {
self.decipher.borrow_mut().set_aad(aad);
}
Expand Down Expand Up @@ -419,13 +434,17 @@ impl Cipher {
#[derive(Debug, thiserror::Error, deno_error::JsError)]
#[property("library" = "Provider routines")]
#[property("reason" = self.get_message().to_string())]
#[property("code" = self.code())]
pub enum DecipherError {
#[class(type)]
#[error("IV length must be 12 bytes")]
InvalidIvLength,
#[class(range)]
#[error("Invalid key length")]
InvalidKeyLength,
#[class(type)]
#[error("Invalid authentication tag length: {0}")]
InvalidAuthTag(usize),
#[class(range)]
#[error("Wrong final block length")]
InvalidFinalBlockLength,
Expand All @@ -449,6 +468,23 @@ pub enum DecipherError {
UnknownCipher(String),
}

impl DecipherError {
fn code(&self) -> deno_error::PropertyValue {
match self {
Self::InvalidIvLength => {
deno_error::PropertyValue::String("ERR_CRYPTO_INVALID_IV_LENGTH".into())
}
Self::InvalidKeyLength => deno_error::PropertyValue::String(
"ERR_CRYPTO_INVALID_KEY_LENGTH".into(),
),
Self::InvalidAuthTag(_) => {
deno_error::PropertyValue::String("ERR_CRYPTO_INVALID_AUTH_TAG".into())
}
_ => deno_error::PropertyValue::String("ERR_CRYPTO_DECIPHER".into()),
}
}
}

macro_rules! assert_block_len {
($input:expr, $len:expr) => {
if $input != $len {
Expand All @@ -457,11 +493,16 @@ macro_rules! assert_block_len {
};
}

fn is_valid_gcm_tag_length(tag_len: usize) -> bool {
tag_len == 4 || tag_len == 8 || (12..=16).contains(&tag_len)
}

impl Decipher {
fn new(
algorithm_name: &str,
key: &[u8],
iv: &[u8],
auth_tag_length: Option<usize>,
) -> Result<Self, DecipherError> {
use Decipher::*;
Ok(match algorithm_name {
Expand All @@ -476,20 +517,32 @@ impl Decipher {
return Err(DecipherError::InvalidKeyLength);
}

if let Some(tag_len) = auth_tag_length {
if !is_valid_gcm_tag_length(tag_len) {
return Err(DecipherError::InvalidAuthTag(tag_len));
}
}

let decipher =
aead_gcm_stream::AesGcm::<aes::Aes128>::new(key.into(), iv);

Aes128Gcm(Box::new(decipher))
Aes128Gcm(Box::new(decipher), auth_tag_length)
}
"aes-256-gcm" => {
if key.len() != aes::Aes256::key_size() {
return Err(DecipherError::InvalidKeyLength);
}

if let Some(tag_len) = auth_tag_length {
if !is_valid_gcm_tag_length(tag_len) {
return Err(DecipherError::InvalidAuthTag(tag_len));
}
}

let decipher =
aead_gcm_stream::AesGcm::<aes::Aes256>::new(key.into(), iv);

Aes256Gcm(Box::new(decipher))
Aes256Gcm(Box::new(decipher), auth_tag_length)
}
"aes256" | "aes-256-cbc" => {
if key.len() != 32 {
Expand Down Expand Up @@ -534,13 +587,26 @@ impl Decipher {
})
}

fn validate_auth_tag(&self, length: usize) -> Result<(), DecipherError> {
match self {
Decipher::Aes128Gcm(_, Some(tag_len))
| Decipher::Aes256Gcm(_, Some(tag_len)) => {
if *tag_len != length {
return Err(DecipherError::InvalidAuthTag(length));
}
}
_ => {}
}
Ok(())
}

fn set_aad(&mut self, aad: &[u8]) {
use Decipher::*;
match self {
Aes128Gcm(decipher) => {
Aes128Gcm(decipher, _) => {
decipher.set_aad(aad);
}
Aes256Gcm(decipher) => {
Aes256Gcm(decipher, _) => {
decipher.set_aad(aad);
}
_ => {}
Expand Down Expand Up @@ -575,11 +641,11 @@ impl Decipher {
decryptor.decrypt_block_b2b_mut(input.into(), output.into());
}
}
Aes128Gcm(decipher) => {
Aes128Gcm(decipher, _) => {
output[..input.len()].copy_from_slice(input);
decipher.decrypt(output);
}
Aes256Gcm(decipher) => {
Aes256Gcm(decipher, _) => {
output[..input.len()].copy_from_slice(input);
decipher.decrypt(output);
}
Expand Down Expand Up @@ -611,7 +677,7 @@ impl Decipher {
) -> Result<(), DecipherError> {
use Decipher::*;

if input.is_empty() && !matches!(self, Aes128Gcm(_) | Aes256Gcm(_)) {
if input.is_empty() && !matches!(self, Aes128Gcm(..) | Aes256Gcm(..)) {
return Ok(());
}

Expand Down Expand Up @@ -672,26 +738,26 @@ impl Decipher {
);
Ok(())
}
(Aes128Gcm(decipher), true) => {
(Aes128Gcm(decipher, _), true) => {
let tag = decipher.finish();
if tag.as_slice() == auth_tag {
Ok(())
} else {
Err(DecipherError::DataAuthenticationFailed)
}
}
(Aes128Gcm(_), false) => {
(Aes128Gcm(..), false) => {
Err(DecipherError::SetAutoPaddingFalseAes128GcmUnsupported)
}
(Aes256Gcm(decipher), true) => {
(Aes256Gcm(decipher, _), true) => {
let tag = decipher.finish();
if tag.as_slice() == auth_tag {
Ok(())
} else {
Err(DecipherError::DataAuthenticationFailed)
}
}
(Aes256Gcm(_), false) => {
(Aes256Gcm(..), false) => {
Err(DecipherError::SetAutoPaddingFalseAes256GcmUnsupported)
}
(Aes256Cbc(decryptor), true) => {
Expand Down
20 changes: 19 additions & 1 deletion ext/node/ops/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,29 @@ pub fn op_node_create_decipheriv(
#[string] algorithm: &str,
#[buffer] key: &[u8],
#[buffer] iv: &[u8],
#[smi] auth_tag_length: i32,
) -> Result<u32, cipher::DecipherContextError> {
let context = cipher::DecipherContext::new(algorithm, key, iv)?;
let auth_tag_length = if auth_tag_length == -1 {
None
} else {
Some(auth_tag_length as usize)
};

let context =
cipher::DecipherContext::new(algorithm, key, iv, auth_tag_length)?;
Ok(state.resource_table.add(context))
}

#[op2(fast)]
pub fn op_node_decipheriv_auth_tag(
state: &mut OpState,
#[smi] rid: u32,
#[smi] length: u32,
) -> Result<(), cipher::DecipherContextError> {
let context = state.resource_table.get::<cipher::DecipherContext>(rid)?;
context.validate_auth_tag(length as usize)
}

#[op2(fast)]
pub fn op_node_decipheriv_set_aad(
state: &mut OpState,
Expand Down
24 changes: 23 additions & 1 deletion ext/node/polyfills/internal/crypto/cipher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
op_node_cipheriv_take,
op_node_create_cipheriv,
op_node_create_decipheriv,
op_node_decipheriv_auth_tag,
op_node_decipheriv_decrypt,
op_node_decipheriv_final,
op_node_decipheriv_set_aad,
Expand All @@ -40,6 +41,8 @@ import type {
Encoding,
} from "ext:deno_node/internal/crypto/types.ts";
import { getDefaultEncoding } from "ext:deno_node/internal/crypto/util.ts";
import { ERR_INVALID_ARG_VALUE } from "ext:deno_node/internal/errors.ts";

import {
isAnyArrayBuffer,
isArrayBufferView,
Expand Down Expand Up @@ -318,6 +321,17 @@ class BlockModeCache {
}
}

function getUIntOption(options, key) {
let value;
if (options && (value = options[key]) != null) {
if (value >>> 0 !== value) {
throw new ERR_INVALID_ARG_VALUE(`options.${key}`, value);
}
return value;
}
return -1;
}

export class Decipheriv extends Transform implements Cipher {
/** DecipherContext resource id */
#context: number;
Expand All @@ -337,6 +351,8 @@ export class Decipheriv extends Transform implements Cipher {
iv: BinaryLike | null,
options?: TransformOptions,
) {
const authTagLength = getUIntOption(options, "authTagLength");

super({
transform(chunk, encoding, cb) {
this.push(this.update(chunk, encoding));
Expand All @@ -349,7 +365,12 @@ export class Decipheriv extends Transform implements Cipher {
...options,
});
this.#cache = new BlockModeCache(this.#autoPadding);
this.#context = op_node_create_decipheriv(cipher, toU8(key), toU8(iv));
this.#context = op_node_create_decipheriv(
cipher,
toU8(key),
toU8(iv),
authTagLength,
);
this.#needsBlockCache =
!(cipher == "aes-128-gcm" || cipher == "aes-256-gcm" ||
cipher == "aes-128-ctr" || cipher == "aes-192-ctr" ||
Expand Down Expand Up @@ -391,6 +412,7 @@ export class Decipheriv extends Transform implements Cipher {
}

setAuthTag(buffer: BinaryLike, _encoding?: string): this {
op_node_decipheriv_auth_tag(this.#context, buffer.byteLength);
this.#authTag = buffer;
return this;
}
Expand Down
1 change: 1 addition & 0 deletions tests/node_compat/config.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@
"test-crypto-dh.js",
"test-crypto-domain.js",
"test-crypto-from-binary.js",
"test-crypto-gcm-explicit-short-tag.js",
"test-crypto-hash.js",
"test-crypto-hkdf.js",
"test-crypto-hmac.js",
Expand Down
3 changes: 1 addition & 2 deletions tests/node_compat/runner/TODO.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- deno-fmt-ignore-file -->
# Remaining Node Tests

1206 tests out of 3993 have been ported from Node 23.9.0 (30.20% ported, 70.42% remaining).
1207 tests out of 3993 have been ported from Node 23.9.0 (30.23% ported, 70.40% remaining).

NOTE: This file should not be manually edited. Please edit `tests/node_compat/config.json` and run `deno task setup` in `tests/node_compat/runner` dir instead.

Expand Down Expand Up @@ -507,7 +507,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co
- [parallel/test-crypto-ecdh-convert-key.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-ecdh-convert-key.js)
- [parallel/test-crypto-encoding-validation-error.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-encoding-validation-error.js)
- [parallel/test-crypto-fips.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-fips.js)
- [parallel/test-crypto-gcm-explicit-short-tag.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-gcm-explicit-short-tag.js)
- [parallel/test-crypto-gcm-implicit-short-tag.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-gcm-implicit-short-tag.js)
- [parallel/test-crypto-getcipherinfo.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-getcipherinfo.js)
- [parallel/test-crypto-hash-stream-pipe.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-hash-stream-pipe.js)
Expand Down
4 changes: 2 additions & 2 deletions tests/node_compat/test/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,12 @@ function _expectWarning(name, expected, code) {
expected = [[expected, code]];
} else if (!Array.isArray(expected)) {
expected = Object.entries(expected).map(([a, b]) => [b, a]);
} else if (!(Array.isArray(expected[0]))) {
} else if (expected.length !== 0 && !Array.isArray(expected[0])) {
expected = [[expected[0], expected[1]]];
}
// Deprecation codes are mandatory, everything else is not.
if (name === 'DeprecationWarning') {
expected.forEach(([_, code]) => assert(code, expected));
expected.forEach(([_, code]) => assert(code, `Missing deprecation code: ${expected}`));
}
return mustCall((warning) => {
const [ message, code ] = expected.shift();
Expand Down
Loading
Loading