Skip to content

Commit

Permalink
fix(ext/node): implement hkdf-expand (#18612)
Browse files Browse the repository at this point in the history
Towards #18455
  • Loading branch information
littledivy authored Apr 6, 2023
1 parent 2d0a9ff commit df72420
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 8 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions cli/tests/node_compat/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
203 changes: 203 additions & 0 deletions cli/tests/node_compat/test/parallel/test-crypto-hkdf.js
Original file line number Diff line number Diff line change
@@ -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);
}));
}
});
1 change: 1 addition & 0 deletions ext/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
58 changes: 58 additions & 0 deletions ext/node/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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?
}
2 changes: 2 additions & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 20 additions & 8 deletions ext/node/polyfills/internal/crypto/hkdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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 {
Expand Down

0 comments on commit df72420

Please sign in to comment.