summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--cli/tests/node_compat/config.json1
-rw-r--r--cli/tests/node_compat/test/parallel/test-crypto-hkdf.js203
-rw-r--r--ext/node/Cargo.toml1
-rw-r--r--ext/node/crypto/mod.rs58
-rw-r--r--ext/node/lib.rs2
-rw-r--r--ext/node/polyfills/internal/crypto/hkdf.ts28
8 files changed, 287 insertions, 8 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2a21a541a..bd938b1a7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1108,6 +1108,7 @@ dependencies = [
"digest 0.10.6",
"ecb",
"hex",
+ "hkdf",
"idna 0.3.0",
"indexmap",
"libz-sys",
diff --git a/Cargo.toml b/Cargo.toml
index c12b14af1..2cdf4b375 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -138,6 +138,7 @@ zstd = "=0.11.2"
# crypto
rsa = { version = "0.7.0", default-features = false, features = ["std", "pem", "hazmat"] } # hazmat needed for PrehashSigner in ext/node
+hkdf = "0.12.3"
# macros
proc-macro2 = "1"
diff --git a/cli/tests/node_compat/config.json b/cli/tests/node_compat/config.json
index e314c1958..fc9a8d131 100644
--- a/cli/tests/node_compat/config.json
+++ b/cli/tests/node_compat/config.json
@@ -228,6 +228,7 @@
"test-console-sync-write-error.js",
"test-console-table.js",
"test-console-tty-colors.js",
+ "test-crypto-hkdf.js",
"test-crypto-hmac.js",
"test-crypto-prime.js",
"test-crypto-secret-keygen.js",
diff --git a/cli/tests/node_compat/test/parallel/test-crypto-hkdf.js b/cli/tests/node_compat/test/parallel/test-crypto-hkdf.js
new file mode 100644
index 000000000..b5b35e3ce
--- /dev/null
+++ b/cli/tests/node_compat/test/parallel/test-crypto-hkdf.js
@@ -0,0 +1,203 @@
+// deno-fmt-ignore-file
+// deno-lint-ignore-file
+
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+
+'use strict';
+
+const common = require('../common');
+
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const { kMaxLength } = require('buffer');
+const assert = require('assert');
+const {
+ createSecretKey,
+ hkdf,
+ hkdfSync
+} = require('crypto');
+
+{
+ assert.throws(() => hkdf(), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /The "digest" argument must be of type string/
+ });
+
+ [1, {}, [], false, Infinity].forEach((i) => {
+ assert.throws(() => hkdf(i, 'a'), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "digest" argument must be of type string/
+ });
+ assert.throws(() => hkdfSync(i, 'a'), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "digest" argument must be of type string/
+ });
+ });
+
+ [1, {}, [], false, Infinity].forEach((i) => {
+ assert.throws(() => hkdf('sha256', i), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "ikm" argument must be /
+ });
+ assert.throws(() => hkdfSync('sha256', i), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "ikm" argument must be /
+ });
+ });
+
+ [1, {}, [], false, Infinity].forEach((i) => {
+ assert.throws(() => hkdf('sha256', 'secret', i), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "salt" argument must be /
+ });
+ assert.throws(() => hkdfSync('sha256', 'secret', i), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "salt" argument must be /
+ });
+ });
+
+ [1, {}, [], false, Infinity].forEach((i) => {
+ assert.throws(() => hkdf('sha256', 'secret', 'salt', i), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "info" argument must be /
+ });
+ assert.throws(() => hkdfSync('sha256', 'secret', 'salt', i), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "info" argument must be /
+ });
+ });
+
+ ['test', {}, [], false].forEach((i) => {
+ assert.throws(() => hkdf('sha256', 'secret', 'salt', 'info', i), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "length" argument must be of type number/
+ });
+ assert.throws(() => hkdfSync('sha256', 'secret', 'salt', 'info', i), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ message: /^The "length" argument must be of type number/
+ });
+ });
+
+ assert.throws(() => hkdf('sha256', 'secret', 'salt', 'info', -1), {
+ code: 'ERR_OUT_OF_RANGE'
+ });
+ assert.throws(() => hkdfSync('sha256', 'secret', 'salt', 'info', -1), {
+ code: 'ERR_OUT_OF_RANGE'
+ });
+ assert.throws(() => hkdf('sha256', 'secret', 'salt', 'info',
+ kMaxLength + 1), {
+ code: 'ERR_OUT_OF_RANGE'
+ });
+ assert.throws(() => hkdfSync('sha256', 'secret', 'salt', 'info',
+ kMaxLength + 1), {
+ code: 'ERR_OUT_OF_RANGE'
+ });
+
+ assert.throws(() => hkdfSync('unknown', 'a', '', '', 10), {
+ code: 'ERR_CRYPTO_INVALID_DIGEST'
+ });
+
+ assert.throws(() => hkdf('unknown', 'a', '', Buffer.alloc(1025), 10,
+ common.mustNotCall()), {
+ code: 'ERR_OUT_OF_RANGE'
+ });
+
+ assert.throws(() => hkdfSync('unknown', 'a', '', Buffer.alloc(1025), 10), {
+ code: 'ERR_OUT_OF_RANGE'
+ });
+}
+
+const algorithms = [
+ ['sha256', 'secret', 'salt', 'info', 10],
+ ['sha256', '', '', '', 10],
+ ['sha256', '', 'salt', '', 10],
+ ['sha512', 'secret', 'salt', '', 15],
+];
+
+algorithms.forEach(([ hash, secret, salt, info, length ]) => {
+ {
+ const syncResult = hkdfSync(hash, secret, salt, info, length);
+ assert(syncResult instanceof ArrayBuffer);
+ let is_async = false;
+ hkdf(hash, secret, salt, info, length,
+ common.mustSucceed((asyncResult) => {
+ assert(is_async);
+ assert(asyncResult instanceof ArrayBuffer);
+ assert.deepStrictEqual(syncResult, asyncResult);
+ }));
+ // Keep this after the hkdf call above. This verifies
+ // that the callback is invoked asynchronously.
+ is_async = true;
+ }
+
+ {
+ const buf_secret = Buffer.from(secret);
+ const buf_salt = Buffer.from(salt);
+ const buf_info = Buffer.from(info);
+
+ const syncResult = hkdfSync(hash, buf_secret, buf_salt, buf_info, length);
+ hkdf(hash, buf_secret, buf_salt, buf_info, length,
+ common.mustSucceed((asyncResult) => {
+ assert.deepStrictEqual(syncResult, asyncResult);
+ }));
+ }
+
+ {
+ const key_secret = createSecretKey(Buffer.from(secret));
+ const buf_salt = Buffer.from(salt);
+ const buf_info = Buffer.from(info);
+
+ const syncResult = hkdfSync(hash, key_secret, buf_salt, buf_info, length);
+ hkdf(hash, key_secret, buf_salt, buf_info, length,
+ common.mustSucceed((asyncResult) => {
+ assert.deepStrictEqual(syncResult, asyncResult);
+ }));
+ }
+
+ {
+ const ta_secret = new Uint8Array(Buffer.from(secret));
+ const ta_salt = new Uint16Array(Buffer.from(salt));
+ const ta_info = new Uint32Array(Buffer.from(info));
+
+ const syncResult = hkdfSync(hash, ta_secret, ta_salt, ta_info, length);
+ hkdf(hash, ta_secret, ta_salt, ta_info, length,
+ common.mustSucceed((asyncResult) => {
+ assert.deepStrictEqual(syncResult, asyncResult);
+ }));
+ }
+
+ {
+ const ta_secret = new Uint8Array(Buffer.from(secret));
+ const ta_salt = new Uint16Array(Buffer.from(salt));
+ const ta_info = new Uint32Array(Buffer.from(info));
+
+ const syncResult = hkdfSync(
+ hash,
+ ta_secret.buffer,
+ ta_salt.buffer,
+ ta_info.buffer,
+ length);
+ hkdf(hash, ta_secret, ta_salt, ta_info, length,
+ common.mustSucceed((asyncResult) => {
+ assert.deepStrictEqual(syncResult, asyncResult);
+ }));
+ }
+
+ {
+ const ta_secret = new Uint8Array(Buffer.from(secret));
+ const sa_salt = new ArrayBuffer(0);
+ const sa_info = new ArrayBuffer(1);
+
+ const syncResult = hkdfSync(
+ hash,
+ ta_secret.buffer,
+ sa_salt,
+ sa_info,
+ length);
+ hkdf(hash, ta_secret, sa_salt, sa_info, length,
+ common.mustSucceed((asyncResult) => {
+ assert.deepStrictEqual(syncResult, asyncResult);
+ }));
+ }
+});
diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml
index 4dbc79b9e..e74cf3805 100644
--- a/ext/node/Cargo.toml
+++ b/ext/node/Cargo.toml
@@ -20,6 +20,7 @@ deno_core.workspace = true
digest = { version = "0.10.5", features = ["core-api", "std"] }
ecb.workspace = true
hex.workspace = true
+hkdf.workspace = true
idna = "0.3.0"
indexmap.workspace = true
libz-sys = { version = "1.1.8", features = ["static"] }
diff --git a/ext/node/crypto/mod.rs b/ext/node/crypto/mod.rs
index 499e99fea..adacdf6d6 100644
--- a/ext/node/crypto/mod.rs
+++ b/ext/node/crypto/mod.rs
@@ -7,6 +7,7 @@ use deno_core::OpState;
use deno_core::ResourceId;
use deno_core::StringOrBuffer;
use deno_core::ZeroCopyBuf;
+use hkdf::Hkdf;
use num_bigint::BigInt;
use rand::Rng;
use std::future::Future;
@@ -419,3 +420,60 @@ pub async fn op_node_generate_secret_async(len: i32) -> ZeroCopyBuf {
.await
.unwrap()
}
+
+fn hkdf_sync(
+ hash: &str,
+ ikm: &[u8],
+ salt: &[u8],
+ info: &[u8],
+ okm: &mut [u8],
+) -> Result<(), AnyError> {
+ macro_rules! hkdf {
+ ($hash:ty) => {{
+ let hk = Hkdf::<$hash>::new(Some(salt), ikm);
+ hk.expand(info, okm)
+ .map_err(|_| type_error("HKDF-Expand failed"))?;
+ }};
+ }
+
+ match hash {
+ "md4" => hkdf!(md4::Md4),
+ "md5" => hkdf!(md5::Md5),
+ "ripemd160" => hkdf!(ripemd::Ripemd160),
+ "sha1" => hkdf!(sha1::Sha1),
+ "sha224" => hkdf!(sha2::Sha224),
+ "sha256" => hkdf!(sha2::Sha256),
+ "sha384" => hkdf!(sha2::Sha384),
+ "sha512" => hkdf!(sha2::Sha512),
+ _ => return Err(type_error("Unknown digest")),
+ }
+
+ Ok(())
+}
+
+#[op]
+pub fn op_node_hkdf(
+ hash: &str,
+ ikm: &[u8],
+ salt: &[u8],
+ info: &[u8],
+ okm: &mut [u8],
+) -> Result<(), AnyError> {
+ hkdf_sync(hash, ikm, salt, info, okm)
+}
+
+#[op]
+pub async fn op_node_hkdf_async(
+ hash: String,
+ ikm: ZeroCopyBuf,
+ salt: ZeroCopyBuf,
+ info: ZeroCopyBuf,
+ okm_len: usize,
+) -> Result<ZeroCopyBuf, AnyError> {
+ tokio::task::spawn_blocking(move || {
+ let mut okm = vec![0u8; okm_len];
+ hkdf_sync(&hash, &ikm, &salt, &info, &mut okm)?;
+ Ok(okm.into())
+ })
+ .await?
+}
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index 478efaf27..bf947f5e8 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -189,6 +189,8 @@ deno_core::extension!(deno_node,
crypto::op_node_check_prime_bytes_async,
crypto::op_node_pbkdf2,
crypto::op_node_pbkdf2_async,
+ crypto::op_node_hkdf,
+ crypto::op_node_hkdf_async,
crypto::op_node_generate_secret,
crypto::op_node_generate_secret_async,
crypto::op_node_sign,
diff --git a/ext/node/polyfills/internal/crypto/hkdf.ts b/ext/node/polyfills/internal/crypto/hkdf.ts
index deeba102f..fb26053df 100644
--- a/ext/node/polyfills/internal/crypto/hkdf.ts
+++ b/ext/node/polyfills/internal/crypto/hkdf.ts
@@ -7,6 +7,7 @@ import {
validateString,
} from "ext:deno_node/internal/validators.mjs";
import {
+ ERR_CRYPTO_INVALID_DIGEST,
ERR_INVALID_ARG_TYPE,
ERR_OUT_OF_RANGE,
hideStackFrames,
@@ -26,17 +27,19 @@ import {
isAnyArrayBuffer,
isArrayBufferView,
} from "ext:deno_node/internal/util/types.ts";
-import { notImplemented } from "ext:deno_node/_utils.ts";
-const validateParameters = hideStackFrames((hash, key, salt, info, length) => {
- key = prepareKey(key);
- salt = toBuf(salt);
- info = toBuf(info);
+const { core } = globalThis.__bootstrap;
+const { ops } = core;
+const validateParameters = hideStackFrames((hash, key, salt, info, length) => {
validateString(hash, "digest");
+ key = new Uint8Array(prepareKey(key));
validateByteSource(salt, "salt");
validateByteSource(info, "info");
+ salt = new Uint8Array(toBuf(salt));
+ info = new Uint8Array(toBuf(info));
+
validateInteger(length, "length", 0, kMaxLength);
if (info.byteLength > 1024) {
@@ -91,7 +94,7 @@ export function hkdf(
salt: BinaryLike,
info: BinaryLike,
length: number,
- callback: (err: Error | null, derivedKey: ArrayBuffer) => void,
+ callback: (err: Error | null, derivedKey: ArrayBuffer | undefined) => void,
) {
({ hash, key, salt, info, length } = validateParameters(
hash,
@@ -103,7 +106,9 @@ export function hkdf(
validateFunction(callback, "callback");
- notImplemented("crypto.hkdf");
+ core.opAsync("op_node_hkdf_async", hash, key, salt, info, length)
+ .then((okm) => callback(null, okm.buffer))
+ .catch((err) => callback(new ERR_CRYPTO_INVALID_DIGEST(err), undefined));
}
export function hkdfSync(
@@ -121,7 +126,14 @@ export function hkdfSync(
length,
));
- notImplemented("crypto.hkdfSync");
+ const okm = new Uint8Array(length);
+ try {
+ ops.op_node_hkdf(hash, key, salt, info, okm);
+ } catch (e) {
+ throw new ERR_CRYPTO_INVALID_DIGEST(e);
+ }
+
+ return okm.buffer;
}
export default {