Skip to content

Commit

Permalink
fix(ext/node): add createDecipheriv (denoland#18245)
Browse files Browse the repository at this point in the history
  • Loading branch information
kt3k committed Mar 18, 2023
1 parent 44553aa commit 5223f3d
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 26 deletions.
38 changes: 38 additions & 0 deletions cli/tests/unit_node/crypto_cipher_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
},
});
73 changes: 70 additions & 3 deletions ext/node/crypto/cipher.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,16 +18,16 @@ 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 {
cipher: Rc<RefCell<Cipher>>,
}

pub struct DecipherContext {
_decipher: Rc<RefCell<Decipher>>,
decipher: Rc<RefCell<Decipher>>,
}

impl CipherContext {
Expand All @@ -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()
Expand Down Expand Up @@ -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(())
}
}
}
}
43 changes: 43 additions & 0 deletions ext/node/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
3 changes: 3 additions & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
93 changes: 70 additions & 23 deletions ext/node/polyfills/internal/crypto/cipher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -189,31 +203,48 @@ 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;
}
}

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(
Expand All @@ -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);
}
}

Expand Down

0 comments on commit 5223f3d

Please sign in to comment.