diff options
Diffstat (limited to 'ext/node')
-rw-r--r-- | ext/node/Cargo.toml | 1 | ||||
-rw-r--r-- | ext/node/lib.rs | 2 | ||||
-rw-r--r-- | ext/node/ops/crypto/cipher.rs | 128 | ||||
-rw-r--r-- | ext/node/ops/crypto/mod.rs | 33 | ||||
-rw-r--r-- | ext/node/polyfills/internal/crypto/cipher.ts | 87 |
5 files changed, 224 insertions, 27 deletions
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<Vec<u8>>; + +type Aes128Gcm = aead_gcm_stream::AesGcm<aes::Aes128>; +type Aes256Gcm = aead_gcm_stream::AesGcm<aes::Aes256>; + enum Cipher { Aes128Cbc(Box<cbc::Encryptor<aes::Aes128>>), Aes128Ecb(Box<ecb::Encryptor<aes::Aes128>>), - // TODO(kt3k): add more algorithms Aes192Cbc, Aes256Cbc, Aes128GCM, etc. + Aes128Gcm(Box<Aes128Gcm>), + Aes256Gcm(Box<Aes256Gcm>), + // TODO(kt3k): add more algorithms Aes192Cbc, Aes256Cbc, etc. } enum Decipher { Aes128Cbc(Box<cbc::Decryptor<aes::Aes128>>), Aes128Ecb(Box<ecb::Decryptor<aes::Aes128>>), + Aes128Gcm(Box<Aes128Gcm>), + Aes256Gcm(Box<Aes256Gcm>), // 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<Tag, AnyError> { 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::<aes::Aes128>::new(key.into()); + cipher.init(iv.try_into()?); + + Aes128Gcm(Box::new(cipher)) + } + "aes-256-gcm" => { + let mut cipher = + aead_gcm_stream::AesGcm::<aes::Aes256>::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<Tag, AnyError> { assert!(input.len() < 16); use Cipher::*; match self { @@ -135,14 +188,16 @@ impl Cipher { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot pad the input data"))?; - Ok(()) + Ok(None) } Aes128Ecb(encryptor) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::<Pkcs7>(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::<aes::Aes128>::new(key.into()); + decipher.init(iv.try_into()?); + + Aes128Gcm(Box::new(decipher)) + } + "aes-256-gcm" => { + let mut decipher = + aead_gcm_stream::AesGcm::<aes::Aes256>::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::<Pkcs7>(input, output) .map_err(|_| type_error("Cannot unpad the input data"))?; Ok(()) } Aes128Ecb(decryptor) => { + 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) => { + 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 @@ -236,6 +236,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::<cipher::CipherContext>(rid) { + Ok(context) => context, + Err(_) => return false, + }; + context.set_aad(aad); + true +} + +#[op(fast)] pub fn op_node_cipheriv_encrypt( state: &mut OpState, rid: u32, @@ -256,7 +270,7 @@ pub fn op_node_cipheriv_final( rid: u32, input: &[u8], output: &mut [u8], -) -> Result<(), AnyError> { +) -> 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"))?; @@ -279,6 +293,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::<cipher::DecipherContext>(rid) { + Ok(context) => context, + Err(_) => return false, + }; + context.set_aad(aad); + true +} + +#[op(fast)] pub fn op_node_decipheriv_decrypt( state: &mut OpState, rid: u32, @@ -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::<cipher::DecipherContext>(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 { |