summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/tests/unit_node/crypto_cipher_test.ts38
-rw-r--r--ext/node/crypto/cipher.rs73
-rw-r--r--ext/node/crypto/mod.rs43
-rw-r--r--ext/node/lib.rs3
-rw-r--r--ext/node/polyfills/internal/crypto/cipher.ts93
5 files changed, 224 insertions, 26 deletions
diff --git a/cli/tests/unit_node/crypto_cipher_test.ts b/cli/tests/unit_node/crypto_cipher_test.ts
index 5fbaa548d..b14d149f5 100644
--- a/cli/tests/unit_node/crypto_cipher_test.ts
+++ b/cli/tests/unit_node/crypto_cipher_test.ts
@@ -72,3 +72,41 @@ Deno.test({
assertEquals(cipher.final("hex"), "e11901dde4a2f99fe4efc707e48c6aed");
},
});
+
+Deno.test({
+ name: "createCipheriv - input encoding",
+ fn() {
+ const cipher = crypto.createCipheriv(
+ "aes-128-cbc",
+ new Uint8Array(16),
+ new Uint8Array(16),
+ );
+ assertEquals(
+ cipher.update("hello, world! hello, world!", "utf-8", "hex"),
+ "ca7df4d74f51b77a7440ead38343ab0f",
+ );
+ assertEquals(cipher.final("hex"), "d0da733dec1fa61125c80a6f97e6166e");
+ },
+});
+
+Deno.test({
+ name: "createDecipheriv - basic",
+ fn() {
+ const decipher = crypto.createDecipheriv(
+ "aes-128-cbc",
+ new Uint8Array(16),
+ new Uint8Array(16),
+ );
+ assertEquals(
+ decipher.update(
+ "66e94bd4ef8a2c3b884cfa59ca342b2ef795bd4a52e29ed713d313fa20e98dbca10cf66d0fddf3405370b4bf8df5bfb347c78395e0d8ae2194da0a90abc9888a94ee48f6c78fcd518a941c3896102cb1e11901dde4a2f99fe4efc707e48c6aed",
+ "hex",
+ ),
+ Buffer.alloc(80),
+ );
+ assertEquals(
+ decipher.final(),
+ Buffer.alloc(10), // Checks the padding
+ );
+ },
+});
diff --git a/ext/node/crypto/cipher.rs b/ext/node/crypto/cipher.rs
index 0833cb591..54cd61132 100644
--- a/ext/node/crypto/cipher.rs
+++ b/ext/node/crypto/cipher.rs
@@ -1,6 +1,7 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use aes::cipher::block_padding::Pkcs7;
+use aes::cipher::BlockDecryptMut;
use aes::cipher::BlockEncryptMut;
use aes::cipher::KeyIvInit;
use deno_core::error::type_error;
@@ -17,8 +18,8 @@ enum Cipher {
}
enum Decipher {
- // TODO(kt3k): implement Deciphers
- // Aes128Cbc(Box<cbc::Decryptor<aes::Aes128>>),
+ Aes128Cbc(Box<cbc::Decryptor<aes::Aes128>>),
+ // TODO(kt3k): add more algorithms Aes192Cbc, Aes256Cbc, Aes128ECB, Aes128GCM, etc.
}
pub struct CipherContext {
@@ -26,7 +27,7 @@ pub struct CipherContext {
}
pub struct DecipherContext {
- _decipher: Rc<RefCell<Decipher>>,
+ decipher: Rc<RefCell<Decipher>>,
}
impl CipherContext {
@@ -52,6 +53,29 @@ impl CipherContext {
}
}
+impl DecipherContext {
+ pub fn new(algorithm: &str, key: &[u8], iv: &[u8]) -> Result<Self, AnyError> {
+ Ok(Self {
+ decipher: Rc::new(RefCell::new(Decipher::new(algorithm, key, iv)?)),
+ })
+ }
+
+ pub fn decrypt(&self, input: &[u8], output: &mut [u8]) {
+ self.decipher.borrow_mut().decrypt(input, output);
+ }
+
+ pub fn r#final(
+ self,
+ input: &[u8],
+ output: &mut [u8],
+ ) -> Result<(), AnyError> {
+ Rc::try_unwrap(self.decipher)
+ .map_err(|_| type_error("Decipher context is already in use"))?
+ .into_inner()
+ .r#final(input, output)
+ }
+}
+
impl Resource for CipherContext {
fn name(&self) -> Cow<str> {
"cryptoCipher".into()
@@ -106,3 +130,46 @@ impl Cipher {
}
}
}
+
+impl Decipher {
+ fn new(
+ algorithm_name: &str,
+ key: &[u8],
+ iv: &[u8],
+ ) -> Result<Self, AnyError> {
+ use Decipher::*;
+ Ok(match algorithm_name {
+ "aes-128-cbc" => {
+ Aes128Cbc(Box::new(cbc::Decryptor::new(key.into(), iv.into())))
+ }
+ _ => return Err(type_error(format!("Unknown cipher {algorithm_name}"))),
+ })
+ }
+
+ /// decrypt decrypts the data in the middle of the input.
+ fn decrypt(&mut self, input: &[u8], output: &mut [u8]) {
+ use Decipher::*;
+ match self {
+ Aes128Cbc(decryptor) => {
+ assert!(input.len() % 16 == 0);
+ for (input, output) in input.chunks(16).zip(output.chunks_mut(16)) {
+ decryptor.decrypt_block_b2b_mut(input.into(), output.into());
+ }
+ }
+ }
+ }
+
+ /// 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);
+ use Decipher::*;
+ match self {
+ Aes128Cbc(decryptor) => {
+ let _ = (*decryptor)
+ .decrypt_padded_b2b_mut::<Pkcs7>(input, output)
+ .map_err(|_| type_error("Cannot unpad the input data"))?;
+ Ok(())
+ }
+ }
+ }
+}
diff --git a/ext/node/crypto/mod.rs b/ext/node/crypto/mod.rs
index 3e6af9b4b..53d064d86 100644
--- a/ext/node/crypto/mod.rs
+++ b/ext/node/crypto/mod.rs
@@ -197,3 +197,46 @@ pub fn op_node_cipheriv_final(
.map_err(|_| type_error("Cipher context is already in use"))?;
context.r#final(input, output)
}
+
+#[op(fast)]
+pub fn op_node_create_decipheriv(
+ state: &mut OpState,
+ algorithm: &str,
+ key: &[u8],
+ iv: &[u8],
+) -> u32 {
+ state.resource_table.add(
+ match cipher::DecipherContext::new(algorithm, key, iv) {
+ Ok(context) => context,
+ Err(_) => return 0,
+ },
+ )
+}
+
+#[op(fast)]
+pub fn op_node_decipheriv_decrypt(
+ state: &mut OpState,
+ rid: u32,
+ input: &[u8],
+ output: &mut [u8],
+) -> bool {
+ let context = match state.resource_table.get::<cipher::DecipherContext>(rid) {
+ Ok(context) => context,
+ Err(_) => return false,
+ };
+ context.decrypt(input, output);
+ true
+}
+
+#[op]
+pub fn op_node_decipheriv_final(
+ state: &mut OpState,
+ rid: u32,
+ input: &[u8],
+ output: &mut [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)
+}
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index 23a003bc8..d2d6b15c9 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -96,10 +96,13 @@ fn op_node_build_os() -> String {
deno_core::extension!(deno_node,
deps = [ deno_io, deno_fs ],
ops = [
+ crypto::op_node_create_decipheriv,
crypto::op_node_cipheriv_encrypt,
crypto::op_node_cipheriv_final,
crypto::op_node_create_cipheriv,
crypto::op_node_create_hash,
+ crypto::op_node_decipheriv_decrypt,
+ crypto::op_node_decipheriv_final,
crypto::op_node_hash_update,
crypto::op_node_hash_update_str,
crypto::op_node_hash_digest,
diff --git a/ext/node/polyfills/internal/crypto/cipher.ts b/ext/node/polyfills/internal/crypto/cipher.ts
index b2d24947a..d40978cf6 100644
--- a/ext/node/polyfills/internal/crypto/cipher.ts
+++ b/ext/node/polyfills/internal/crypto/cipher.ts
@@ -130,7 +130,7 @@ export class Cipheriv extends Transform implements Cipher {
options?: TransformOptions,
) {
super(options);
- this.#cache = new BlockModeCache();
+ this.#cache = new BlockModeCache(false);
this.#context = ops.op_node_create_cipheriv(cipher, key, iv);
}
@@ -161,14 +161,23 @@ export class Cipheriv extends Transform implements Cipher {
update(
data: string | Buffer | ArrayBufferView,
- // TODO(kt3k): Handle inputEncoding
- _inputEncoding?: Encoding,
+ inputEncoding?: Encoding,
outputEncoding: Encoding = getDefaultEncoding(),
): Buffer | string {
- this.#cache.add(data);
+ // TODO(kt3k): throw ERR_INVALID_ARG_TYPE if data is not string, Buffer, or ArrayBufferView
+ if (typeof data === "string" && typeof inputEncoding === "string") {
+ this.#cache.add(Buffer.from(data, inputEncoding));
+ } else {
+ this.#cache.add(data);
+ }
const input = this.#cache.get();
- const output = new Buffer(input.length);
- ops.op_node_cipheriv_encrypt(this.#context, input, output);
+ let output;
+ if (input === null) {
+ output = Buffer.alloc(0);
+ } else {
+ output = Buffer.allocUnsafe(input.length);
+ ops.op_node_cipheriv_encrypt(this.#context, input, output);
+ }
return outputEncoding === "buffer"
? output
: output.toString(outputEncoding);
@@ -178,8 +187,13 @@ export class Cipheriv extends Transform implements Cipher {
/** Caches data and output the chunk of multiple of 16.
* Used by CBC, ECB modes of block ciphers */
class BlockModeCache {
- constructor() {
+ cache: Uint8Array;
+ // The last chunk can be padded when decrypting.
+ #lastChunkIsNonZero: boolean;
+
+ constructor(lastChunkIsNotZero = false) {
this.cache = new Uint8Array(0);
+ this.#lastChunkIsNonZero = lastChunkIsNotZero;
}
add(data: Uint8Array) {
@@ -189,11 +203,19 @@ class BlockModeCache {
this.cache.set(data, cache.length);
}
- get(): Uint8Array {
- if (this.cache.length < 16) {
+ /** Gets the chunk of the length of largest multiple of 16.
+ * Used for preparing data for encryption/decryption */
+ get(): Uint8Array | null {
+ let len = this.cache.length;
+ if (this.#lastChunkIsNonZero) {
+ // Reduces the available chunk length by 1 to keep the last chunk
+ len -= 1;
+ }
+ if (len < 16) {
return null;
}
- const len = Math.floor(this.cache.length / 16) * 16;
+
+ len = Math.floor(len / 16) * 16;
const out = this.cache.subarray(0, len);
this.cache = this.cache.subarray(len);
return out;
@@ -201,19 +223,28 @@ class BlockModeCache {
}
export class Decipheriv extends Transform implements Cipher {
+ /** DecipherContext resource id */
+ #context: number;
+
+ /** ciphertext data cache */
+ #cache: BlockModeCache;
+
constructor(
- _cipher: string,
- _key: CipherKey,
- _iv: BinaryLike | null,
- _options?: TransformOptions,
+ cipher: string,
+ key: CipherKey,
+ iv: BinaryLike | null,
+ options?: TransformOptions,
) {
- super();
-
- notImplemented("crypto.Decipheriv");
+ super(options);
+ this.#cache = new BlockModeCache(true);
+ this.#context = ops.op_node_create_decipheriv(cipher, key, iv);
}
- final(_outputEncoding?: string): Buffer | string {
- notImplemented("crypto.Decipheriv.prototype.final");
+ final(encoding: string = getDefaultEncoding()): Buffer | string {
+ let buf = new Buffer(16);
+ ops.op_node_decipheriv_final(this.#context, this.#cache.cache, buf);
+ buf = buf.subarray(0, 16 - buf.at(-1)); // Padded in Pkcs7 mode
+ return encoding === "buffer" ? buf : buf.toString(encoding);
}
setAAD(
@@ -234,11 +265,27 @@ export class Decipheriv extends Transform implements Cipher {
}
update(
- _data: string | BinaryLike | ArrayBufferView,
- _inputEncoding?: Encoding,
- _outputEncoding?: Encoding,
+ data: string | Buffer | ArrayBufferView,
+ inputEncoding?: Encoding,
+ outputEncoding: Encoding = getDefaultEncoding(),
): Buffer | string {
- notImplemented("crypto.Decipheriv.prototype.update");
+ // TODO(kt3k): throw ERR_INVALID_ARG_TYPE if data is not string, Buffer, or ArrayBufferView
+ if (typeof data === "string" && typeof inputEncoding === "string") {
+ this.#cache.add(Buffer.from(data, inputEncoding));
+ } else {
+ this.#cache.add(data);
+ }
+ const input = this.#cache.get();
+ let output;
+ if (input === null) {
+ output = Buffer.alloc(0);
+ } else {
+ output = new Buffer(input.length);
+ ops.op_node_decipheriv_decrypt(this.#context, input, output);
+ }
+ return outputEncoding === "buffer"
+ ? output
+ : output.toString(outputEncoding);
}
}