From 9befa566ec3ef4594fd7ffb2cbdf5b34d9705e16 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Tue, 5 Sep 2023 22:31:50 -0700 Subject: fix(ext/node): implement AES GCM cipher (#20368) Adds support for AES-GCM 128/256 bit keys in `node:crypto` and `setAAD()`, `setAuthTag()` and `getAuthTag()` Uses https://github.com/littledivy/aead-gcm-stream Fixes https://github.com/denoland/deno/issues/19836 https://github.com/denoland/deno/issues/20353 --- ext/node/Cargo.toml | 1 + ext/node/lib.rs | 2 + ext/node/ops/crypto/cipher.rs | 128 +++++++++++++++++++++++++-- ext/node/ops/crypto/mod.rs | 33 ++++++- ext/node/polyfills/internal/crypto/cipher.ts | 87 ++++++++++++++---- 5 files changed, 224 insertions(+), 27 deletions(-) (limited to 'ext/node') diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 81c79e74d..07c2b2da5 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -14,6 +14,7 @@ description = "Node compatibility for Deno" path = "lib.rs" [dependencies] +aead-gcm-stream = "0.1" aes.workspace = true brotli.workspace = true cbc.workspace = true diff --git a/ext/node/lib.rs b/ext/node/lib.rs index fa7213c26..c1bb88275 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -155,6 +155,8 @@ deno_core::extension!(deno_node, ops::crypto::op_node_create_decipheriv, ops::crypto::op_node_cipheriv_encrypt, ops::crypto::op_node_cipheriv_final, + ops::crypto::op_node_cipheriv_set_aad, + ops::crypto::op_node_decipheriv_set_aad, ops::crypto::op_node_create_cipheriv, ops::crypto::op_node_create_hash, ops::crypto::op_node_get_hashes, diff --git a/ext/node/ops/crypto/cipher.rs b/ext/node/ops/crypto/cipher.rs index 4f3f7f20d..717c12752 100644 --- a/ext/node/ops/crypto/cipher.rs +++ b/ext/node/ops/crypto/cipher.rs @@ -13,15 +13,24 @@ use std::borrow::Cow; use std::cell::RefCell; use std::rc::Rc; +type Tag = Option>; + +type Aes128Gcm = aead_gcm_stream::AesGcm; +type Aes256Gcm = aead_gcm_stream::AesGcm; + enum Cipher { Aes128Cbc(Box>), Aes128Ecb(Box>), - // TODO(kt3k): add more algorithms Aes192Cbc, Aes256Cbc, Aes128GCM, etc. + Aes128Gcm(Box), + Aes256Gcm(Box), + // TODO(kt3k): add more algorithms Aes192Cbc, Aes256Cbc, etc. } enum Decipher { Aes128Cbc(Box>), Aes128Ecb(Box>), + Aes128Gcm(Box), + Aes256Gcm(Box), // TODO(kt3k): add more algorithms Aes192Cbc, Aes256Cbc, Aes128GCM, etc. } @@ -40,6 +49,10 @@ impl CipherContext { }) } + pub fn set_aad(&self, aad: &[u8]) { + self.cipher.borrow_mut().set_aad(aad); + } + pub fn encrypt(&self, input: &[u8], output: &mut [u8]) { self.cipher.borrow_mut().encrypt(input, output); } @@ -48,7 +61,7 @@ impl CipherContext { self, input: &[u8], output: &mut [u8], - ) -> Result<(), AnyError> { + ) -> Result { Rc::try_unwrap(self.cipher) .map_err(|_| type_error("Cipher context is already in use"))? .into_inner() @@ -63,6 +76,10 @@ impl DecipherContext { }) } + pub fn set_aad(&self, aad: &[u8]) { + self.decipher.borrow_mut().set_aad(aad); + } + pub fn decrypt(&self, input: &[u8], output: &mut [u8]) { self.decipher.borrow_mut().decrypt(input, output); } @@ -71,11 +88,12 @@ impl DecipherContext { self, 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) + .r#final(input, output, auth_tag) } } @@ -103,10 +121,37 @@ impl Cipher { Aes128Cbc(Box::new(cbc::Encryptor::new(key.into(), iv.into()))) } "aes-128-ecb" => Aes128Ecb(Box::new(ecb::Encryptor::new(key.into()))), + "aes-128-gcm" => { + let mut cipher = + aead_gcm_stream::AesGcm::::new(key.into()); + cipher.init(iv.try_into()?); + + Aes128Gcm(Box::new(cipher)) + } + "aes-256-gcm" => { + let mut cipher = + aead_gcm_stream::AesGcm::::new(key.into()); + cipher.init(iv.try_into()?); + + Aes256Gcm(Box::new(cipher)) + } _ => return Err(type_error(format!("Unknown cipher {algorithm_name}"))), }) } + fn set_aad(&mut self, aad: &[u8]) { + use Cipher::*; + match self { + Aes128Gcm(cipher) => { + cipher.set_aad(aad); + } + Aes256Gcm(cipher) => { + cipher.set_aad(aad); + } + _ => {} + } + } + /// encrypt encrypts the data in the middle of the input. fn encrypt(&mut self, input: &[u8], output: &mut [u8]) { use Cipher::*; @@ -123,11 +168,19 @@ impl Cipher { encryptor.encrypt_block_b2b_mut(input.into(), output.into()); } } + Aes128Gcm(cipher) => { + output[..input.len()].copy_from_slice(input); + cipher.encrypt(output); + } + Aes256Gcm(cipher) => { + output[..input.len()].copy_from_slice(input); + cipher.encrypt(output); + } } } /// r#final encrypts the last block of the input data. - fn r#final(self, input: &[u8], output: &mut [u8]) -> Result<(), AnyError> { + fn r#final(self, input: &[u8], output: &mut [u8]) -> Result { assert!(input.len() < 16); use Cipher::*; match self { @@ -135,14 +188,16 @@ impl Cipher { let _ = (*encryptor) .encrypt_padded_b2b_mut::(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; - Ok(()) + Ok(None) } Aes128Ecb(encryptor) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; - Ok(()) + Ok(None) } + Aes128Gcm(cipher) => Ok(Some(cipher.finish().to_vec())), + Aes256Gcm(cipher) => Ok(Some(cipher.finish().to_vec())), } } } @@ -159,10 +214,37 @@ impl Decipher { Aes128Cbc(Box::new(cbc::Decryptor::new(key.into(), iv.into()))) } "aes-128-ecb" => Aes128Ecb(Box::new(ecb::Decryptor::new(key.into()))), + "aes-128-gcm" => { + let mut decipher = + aead_gcm_stream::AesGcm::::new(key.into()); + decipher.init(iv.try_into()?); + + Aes128Gcm(Box::new(decipher)) + } + "aes-256-gcm" => { + let mut decipher = + aead_gcm_stream::AesGcm::::new(key.into()); + decipher.init(iv.try_into()?); + + Aes256Gcm(Box::new(decipher)) + } _ => return Err(type_error(format!("Unknown cipher {algorithm_name}"))), }) } + fn set_aad(&mut self, aad: &[u8]) { + use Decipher::*; + match self { + Aes128Gcm(decipher) => { + decipher.set_aad(aad); + } + Aes256Gcm(decipher) => { + decipher.set_aad(aad); + } + _ => {} + } + } + /// decrypt decrypts the data in the middle of the input. fn decrypt(&mut self, input: &[u8], output: &mut [u8]) { use Decipher::*; @@ -179,26 +261,56 @@ impl Decipher { decryptor.decrypt_block_b2b_mut(input.into(), output.into()); } } + Aes128Gcm(decipher) => { + output[..input.len()].copy_from_slice(input); + decipher.decrypt(output); + } + Aes256Gcm(decipher) => { + output[..input.len()].copy_from_slice(input); + decipher.decrypt(output); + } } } /// r#final decrypts the last block of the input data. - fn r#final(self, input: &[u8], output: &mut [u8]) -> Result<(), AnyError> { - assert!(input.len() == 16); + fn r#final( + self, + input: &[u8], + output: &mut [u8], + auth_tag: &[u8], + ) -> Result<(), AnyError> { use Decipher::*; match self { Aes128Cbc(decryptor) => { + assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } Aes128Ecb(decryptor) => { + assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } + Aes128Gcm(decipher) => { + let tag = decipher.finish(); + if tag.as_slice() == auth_tag { + Ok(()) + } else { + Err(type_error("Failed to authenticate data")) + } + } + Aes256Gcm(decipher) => { + let tag = decipher.finish(); + if tag.as_slice() == auth_tag { + Ok(()) + } else { + Err(type_error("Failed to authenticate data")) + } + } } } } diff --git a/ext/node/ops/crypto/mod.rs b/ext/node/ops/crypto/mod.rs index c0b4f55f8..ce2ff0ebc 100644 --- a/ext/node/ops/crypto/mod.rs +++ b/ext/node/ops/crypto/mod.rs @@ -235,6 +235,20 @@ pub fn op_node_create_cipheriv( ) } +#[op(fast)] +pub fn op_node_cipheriv_set_aad( + state: &mut OpState, + rid: u32, + aad: &[u8], +) -> bool { + let context = match state.resource_table.get::(rid) { + Ok(context) => context, + Err(_) => return false, + }; + context.set_aad(aad); + true +} + #[op(fast)] pub fn op_node_cipheriv_encrypt( state: &mut OpState, @@ -256,7 +270,7 @@ pub fn op_node_cipheriv_final( rid: u32, input: &[u8], output: &mut [u8], -) -> Result<(), AnyError> { +) -> Result>, AnyError> { let context = state.resource_table.take::(rid)?; let context = Rc::try_unwrap(context) .map_err(|_| type_error("Cipher context is already in use"))?; @@ -278,6 +292,20 @@ pub fn op_node_create_decipheriv( ) } +#[op(fast)] +pub fn op_node_decipheriv_set_aad( + state: &mut OpState, + rid: u32, + aad: &[u8], +) -> bool { + let context = match state.resource_table.get::(rid) { + Ok(context) => context, + Err(_) => return false, + }; + context.set_aad(aad); + true +} + #[op(fast)] pub fn op_node_decipheriv_decrypt( state: &mut OpState, @@ -299,11 +327,12 @@ pub fn op_node_decipheriv_final( rid: u32, input: &[u8], output: &mut [u8], + auth_tag: &[u8], ) -> Result<(), AnyError> { let context = state.resource_table.take::(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(input, output, auth_tag) } #[op] diff --git a/ext/node/polyfills/internal/crypto/cipher.ts b/ext/node/polyfills/internal/crypto/cipher.ts index 5622576cd..cf1641326 100644 --- a/ext/node/polyfills/internal/crypto/cipher.ts +++ b/ext/node/polyfills/internal/crypto/cipher.ts @@ -36,6 +36,8 @@ function isStringOrBuffer(val) { const { ops, encode } = globalThis.__bootstrap.core; +const NO_TAG = new Uint8Array(); + export type CipherCCMTypes = | "aes-128-ccm" | "aes-192-ccm" @@ -143,6 +145,10 @@ export class Cipheriv extends Transform implements Cipher { /** plaintext data cache */ #cache: BlockModeCache; + #needsBlockCache: boolean; + + #authTag?: Buffer; + constructor( cipher: string, key: CipherKey, @@ -162,6 +168,8 @@ export class Cipheriv extends Transform implements Cipher { }); this.#cache = new BlockModeCache(false); this.#context = ops.op_node_create_cipheriv(cipher, toU8(key), toU8(iv)); + this.#needsBlockCache = + !(cipher == "aes-128-gcm" || cipher == "aes-256-gcm"); if (this.#context == 0) { throw new TypeError("Unknown cipher"); } @@ -169,21 +177,29 @@ export class Cipheriv extends Transform implements Cipher { final(encoding: string = getDefaultEncoding()): Buffer | string { const buf = new Buffer(16); - ops.op_node_cipheriv_final(this.#context, this.#cache.cache, buf); + const maybeTag = ops.op_node_cipheriv_final( + this.#context, + this.#cache.cache, + buf, + ); + if (maybeTag) { + this.#authTag = Buffer.from(maybeTag); + return encoding === "buffer" ? Buffer.from([]) : ""; + } return encoding === "buffer" ? buf : buf.toString(encoding); } getAuthTag(): Buffer { - notImplemented("crypto.Cipheriv.prototype.getAuthTag"); + return this.#authTag!; } setAAD( - _buffer: ArrayBufferView, + buffer: ArrayBufferView, _options?: { plaintextLength: number; }, ): this { - notImplemented("crypto.Cipheriv.prototype.setAAD"); + ops.op_node_cipheriv_set_aad(this.#context, buffer); return this; } @@ -198,13 +214,23 @@ export class Cipheriv extends Transform implements Cipher { outputEncoding: Encoding = getDefaultEncoding(), ): Buffer | string { // TODO(kt3k): throw ERR_INVALID_ARG_TYPE if data is not string, Buffer, or ArrayBufferView + let buf = data; if (typeof data === "string" && typeof inputEncoding === "string") { - this.#cache.add(Buffer.from(data, inputEncoding)); - } else { - this.#cache.add(data); + buf = Buffer.from(data, inputEncoding); } - const input = this.#cache.get(); + let output; + if (!this.#needsBlockCache) { + output = Buffer.allocUnsafe(buf.length); + ops.op_node_cipheriv_encrypt(this.#context, buf, output); + return outputEncoding === "buffer" + ? output + : output.toString(outputEncoding); + } + + this.#cache.add(buf); + const input = this.#cache.get(); + if (input === null) { output = Buffer.alloc(0); } else { @@ -262,6 +288,10 @@ export class Decipheriv extends Transform implements Cipher { /** ciphertext data cache */ #cache: BlockModeCache; + #needsBlockCache: boolean; + + #authTag?: BinaryLike; + constructor( cipher: string, key: CipherKey, @@ -281,6 +311,8 @@ export class Decipheriv extends Transform implements Cipher { }); this.#cache = new BlockModeCache(true); this.#context = ops.op_node_create_decipheriv(cipher, toU8(key), toU8(iv)); + this.#needsBlockCache = + !(cipher == "aes-128-gcm" || cipher == "aes-256-gcm"); if (this.#context == 0) { throw new TypeError("Unknown cipher"); } @@ -288,22 +320,34 @@ export class Decipheriv extends Transform implements Cipher { final(encoding: string = getDefaultEncoding()): Buffer | string { let buf = new Buffer(16); - ops.op_node_decipheriv_final(this.#context, this.#cache.cache, buf); + ops.op_node_decipheriv_final( + this.#context, + 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); } setAAD( - _buffer: ArrayBufferView, + buffer: ArrayBufferView, _options?: { plaintextLength: number; }, ): this { - notImplemented("crypto.Decipheriv.prototype.setAAD"); + ops.op_node_decipheriv_set_aad(this.#context, buffer); + return this; } - setAuthTag(_buffer: BinaryLike, _encoding?: string): this { - notImplemented("crypto.Decipheriv.prototype.setAuthTag"); + setAuthTag(buffer: BinaryLike, _encoding?: string): this { + this.#authTag = buffer; + return this; } setAutoPadding(_autoPadding?: boolean): this { @@ -316,13 +360,22 @@ export class Decipheriv extends Transform implements Cipher { outputEncoding: Encoding = getDefaultEncoding(), ): Buffer | string { // TODO(kt3k): throw ERR_INVALID_ARG_TYPE if data is not string, Buffer, or ArrayBufferView + let buf = data; if (typeof data === "string" && typeof inputEncoding === "string") { - this.#cache.add(Buffer.from(data, inputEncoding)); - } else { - this.#cache.add(data); + buf = Buffer.from(data, inputEncoding); } - const input = this.#cache.get(); + let output; + if (!this.#needsBlockCache) { + output = Buffer.allocUnsafe(buf.length); + ops.op_node_decipheriv_decrypt(this.#context, buf, output); + return outputEncoding === "buffer" + ? output + : output.toString(outputEncoding); + } + + this.#cache.add(buf); + const input = this.#cache.get(); if (input === null) { output = Buffer.alloc(0); } else { -- cgit v1.2.3