Skip to content

Commit 5ed0646

Browse files
authored
fix(node/crypto): respect authTagLength in createCipheriv for GCM cip… (#31253)
This PR fixes a bug in `node:crypto.createCipheriv` where the `authTagLength` option was ignored for AES-GCM ciphers, resulting in a fixed 16-byte authentication tag regardless of the specified value. The fix ensures both encryption and decryption paths correctly honor `authTagLength`, matching Node.js behavior. Fixes #31102
1 parent 30cf1f4 commit 5ed0646

File tree

3 files changed

+91
-19
lines changed

3 files changed

+91
-19
lines changed

ext/node/ops/crypto/cipher.rs

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ enum Cipher {
2525
Aes128Ecb(Box<ecb::Encryptor<aes::Aes128>>),
2626
Aes192Ecb(Box<ecb::Encryptor<aes::Aes192>>),
2727
Aes256Ecb(Box<ecb::Encryptor<aes::Aes256>>),
28-
Aes128Gcm(Box<Aes128Gcm>),
29-
Aes256Gcm(Box<Aes256Gcm>),
28+
Aes128Gcm(Box<Aes128Gcm>, Option<usize>),
29+
Aes256Gcm(Box<Aes256Gcm>, Option<usize>),
3030
Aes256Cbc(Box<cbc::Encryptor<aes::Aes256>>),
3131
Aes128Ctr(Box<ctr::Ctr128BE<aes::Aes128>>),
3232
Aes192Ctr(Box<ctr::Ctr128BE<aes::Aes192>>),
@@ -74,9 +74,15 @@ impl CipherContext {
7474
algorithm: &str,
7575
key: &[u8],
7676
iv: &[u8],
77+
auth_tag_length: Option<usize>,
7778
) -> Result<Self, CipherContextError> {
7879
Ok(Self {
79-
cipher: Rc::new(RefCell::new(Cipher::new(algorithm, key, iv)?)),
80+
cipher: Rc::new(RefCell::new(Cipher::new(
81+
algorithm,
82+
key,
83+
iv,
84+
auth_tag_length,
85+
)?)),
8086
})
8187
}
8288

@@ -197,13 +203,17 @@ pub enum CipherError {
197203
#[class(type)]
198204
#[error("Unknown cipher {0}")]
199205
UnknownCipher(String),
206+
#[class(type)]
207+
#[error("Invalid authentication tag length: {0}")]
208+
InvalidAuthTag(usize),
200209
}
201210

202211
impl Cipher {
203212
fn new(
204213
algorithm_name: &str,
205214
key: &[u8],
206215
iv: &[u8],
216+
auth_tag_length: Option<usize>,
207217
) -> Result<Self, CipherError> {
208218
use Cipher::*;
209219
Ok(match algorithm_name {
@@ -218,20 +228,32 @@ impl Cipher {
218228
return Err(CipherError::InvalidKeyLength);
219229
}
220230

231+
if let Some(tag_len) = auth_tag_length
232+
&& !is_valid_gcm_tag_length(tag_len)
233+
{
234+
return Err(CipherError::InvalidAuthTag(tag_len));
235+
}
236+
221237
let cipher =
222238
aead_gcm_stream::AesGcm::<aes::Aes128>::new(key.into(), iv);
223239

224-
Aes128Gcm(Box::new(cipher))
240+
Aes128Gcm(Box::new(cipher), auth_tag_length)
225241
}
226242
"aes-256-gcm" => {
227243
if key.len() != aes::Aes256::key_size() {
228244
return Err(CipherError::InvalidKeyLength);
229245
}
230246

247+
if let Some(tag_len) = auth_tag_length
248+
&& !is_valid_gcm_tag_length(tag_len)
249+
{
250+
return Err(CipherError::InvalidAuthTag(tag_len));
251+
}
252+
231253
let cipher =
232254
aead_gcm_stream::AesGcm::<aes::Aes256>::new(key.into(), iv);
233255

234-
Aes256Gcm(Box::new(cipher))
256+
Aes256Gcm(Box::new(cipher), auth_tag_length)
235257
}
236258
"aes256" | "aes-256-cbc" => {
237259
if key.len() != 32 {
@@ -277,10 +299,10 @@ impl Cipher {
277299
fn set_aad(&mut self, aad: &[u8]) {
278300
use Cipher::*;
279301
match self {
280-
Aes128Gcm(cipher) => {
302+
Aes128Gcm(cipher, _) => {
281303
cipher.set_aad(aad);
282304
}
283-
Aes256Gcm(cipher) => {
305+
Aes256Gcm(cipher, _) => {
284306
cipher.set_aad(aad);
285307
}
286308
_ => {}
@@ -315,11 +337,11 @@ impl Cipher {
315337
encryptor.encrypt_block_b2b_mut(input.into(), output.into());
316338
}
317339
}
318-
Aes128Gcm(cipher) => {
340+
Aes128Gcm(cipher, _) => {
319341
output[..input.len()].copy_from_slice(input);
320342
cipher.encrypt(output);
321343
}
322-
Aes256Gcm(cipher) => {
344+
Aes256Gcm(cipher, _) => {
323345
output[..input.len()].copy_from_slice(input);
324346
cipher.encrypt(output);
325347
}
@@ -403,8 +425,20 @@ impl Cipher {
403425
);
404426
Ok(None)
405427
}
406-
(Aes128Gcm(cipher), _) => Ok(Some(cipher.finish().to_vec())),
407-
(Aes256Gcm(cipher), _) => Ok(Some(cipher.finish().to_vec())),
428+
(Aes128Gcm(cipher, auth_tag_length), _) => {
429+
let mut tag = cipher.finish().to_vec();
430+
if let Some(tag_len) = auth_tag_length {
431+
tag.truncate(tag_len);
432+
}
433+
Ok(Some(tag))
434+
}
435+
(Aes256Gcm(cipher, auth_tag_length), _) => {
436+
let mut tag = cipher.finish().to_vec();
437+
if let Some(tag_len) = auth_tag_length {
438+
tag.truncate(tag_len);
439+
}
440+
Ok(Some(tag))
441+
}
408442
(Aes256Cbc(encryptor), true) => {
409443
let _ = (*encryptor)
410444
.encrypt_padded_b2b_mut::<Pkcs7>(input, output)
@@ -425,8 +459,20 @@ impl Cipher {
425459
fn take_tag(self) -> Tag {
426460
use Cipher::*;
427461
match self {
428-
Aes128Gcm(cipher) => Some(cipher.finish().to_vec()),
429-
Aes256Gcm(cipher) => Some(cipher.finish().to_vec()),
462+
Aes128Gcm(cipher, auth_tag_length) => {
463+
let mut tag = cipher.finish().to_vec();
464+
if let Some(tag_len) = auth_tag_length {
465+
tag.truncate(tag_len);
466+
}
467+
Some(tag)
468+
}
469+
Aes256Gcm(cipher, auth_tag_length) => {
470+
let mut tag = cipher.finish().to_vec();
471+
if let Some(tag_len) = auth_tag_length {
472+
tag.truncate(tag_len);
473+
}
474+
Some(tag)
475+
}
430476
_ => None,
431477
}
432478
}
@@ -760,9 +806,15 @@ impl Decipher {
760806
);
761807
Ok(())
762808
}
763-
(Aes128Gcm(decipher, _), true) => {
809+
(Aes128Gcm(decipher, auth_tag_length), true) => {
764810
let tag = decipher.finish();
765-
if tag.as_slice() == auth_tag {
811+
let tag_slice = tag.as_slice();
812+
let truncated_tag = if let Some(len) = auth_tag_length {
813+
&tag_slice[..len]
814+
} else {
815+
tag_slice
816+
};
817+
if truncated_tag == auth_tag {
766818
Ok(())
767819
} else {
768820
Err(DecipherError::DataAuthenticationFailed)
@@ -771,9 +823,15 @@ impl Decipher {
771823
(Aes128Gcm(..), false) => {
772824
Err(DecipherError::SetAutoPaddingFalseAes128GcmUnsupported)
773825
}
774-
(Aes256Gcm(decipher, _), true) => {
826+
(Aes256Gcm(decipher, auth_tag_length), true) => {
775827
let tag = decipher.finish();
776-
if tag.as_slice() == auth_tag {
828+
let tag_slice = tag.as_slice();
829+
let truncated_tag = if let Some(len) = auth_tag_length {
830+
&tag_slice[..len]
831+
} else {
832+
tag_slice
833+
};
834+
if truncated_tag == auth_tag {
777835
Ok(())
778836
} else {
779837
Err(DecipherError::DataAuthenticationFailed)

ext/node/ops/crypto/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,15 @@ pub fn op_node_create_cipheriv(
235235
#[string] algorithm: &str,
236236
#[buffer] key: &[u8],
237237
#[buffer] iv: &[u8],
238+
#[smi] auth_tag_length: i32,
238239
) -> Result<u32, cipher::CipherContextError> {
239-
let context = cipher::CipherContext::new(algorithm, key, iv)?;
240+
let auth_tag_length = if auth_tag_length == -1 {
241+
None
242+
} else {
243+
Some(auth_tag_length as usize)
244+
};
245+
let context =
246+
cipher::CipherContext::new(algorithm, key, iv, auth_tag_length)?;
240247
Ok(state.resource_table.add(context))
241248
}
242249

ext/node/polyfills/internal/crypto/cipher.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ export class Cipheriv extends Transform implements Cipher {
190190
iv: BinaryLike | null,
191191
options?: TransformOptions,
192192
) {
193+
const authTagLength = getUIntOption(options, "authTagLength");
194+
193195
super({
194196
transform(chunk, encoding, cb) {
195197
this.push(this.update(chunk, encoding));
@@ -202,7 +204,12 @@ export class Cipheriv extends Transform implements Cipher {
202204
...options,
203205
});
204206
this.#cache = new BlockModeCache(false);
205-
this.#context = op_node_create_cipheriv(cipher, toU8(key), toU8(iv));
207+
this.#context = op_node_create_cipheriv(
208+
cipher,
209+
toU8(key),
210+
toU8(iv),
211+
authTagLength,
212+
);
206213
this.#needsBlockCache =
207214
!(cipher == "aes-128-gcm" || cipher == "aes-256-gcm" ||
208215
cipher == "aes-128-ctr" || cipher == "aes-192-ctr" ||

0 commit comments

Comments
 (0)