deno/ext/node/polyfills/internal/crypto/hkdf.ts
gitstart-app[bot] 6314c3c46d
Some checks failed
ci / pre-build (push) Has been cancelled
ci / build libs (push) Has been cancelled
ci / publish canary (push) Has been cancelled
ci / test debug linux-aarch64 (push) Has been cancelled
ci / test release linux-aarch64 (push) Has been cancelled
ci / test debug macos-aarch64 (push) Has been cancelled
ci / test release macos-aarch64 (push) Has been cancelled
ci / bench release linux-x86_64 (push) Has been cancelled
ci / lint debug linux-x86_64 (push) Has been cancelled
ci / lint debug macos-x86_64 (push) Has been cancelled
ci / lint debug windows-x86_64 (push) Has been cancelled
ci / test debug linux-x86_64 (push) Has been cancelled
ci / test release linux-x86_64 (push) Has been cancelled
ci / test debug macos-x86_64 (push) Has been cancelled
ci / test release macos-x86_64 (push) Has been cancelled
ci / test debug windows-x86_64 (push) Has been cancelled
ci / test release windows-x86_64 (push) Has been cancelled
fix(ext/node): crypto.hkdfSync returns wrong result for non-Uint8Array TypedArray inputs (#30463)
The original HKDF implementation incorrectly handled
TypedArrays by converting them through the toBuf() function, which only
handles strings and Buffers. This caused TypedArrays to be processed
incorrectly, losing their actual byte representation.

Closes https://github.com/denoland/deno/issues/29913

---------

Co-authored-by: gitstart-app[bot] <80938352+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
2025-08-23 11:56:19 +02:00

187 lines
4.2 KiB
TypeScript

// Copyright 2018-2025 the Deno authors. MIT license.
// Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license.
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
import {
op_node_get_hash_size,
op_node_hkdf,
op_node_hkdf_async,
} from "ext:core/ops";
import {
validateFunction,
validateInteger,
validateString,
} from "ext:deno_node/internal/validators.mjs";
import {
ERR_CRYPTO_INVALID_DIGEST,
ERR_CRYPTO_INVALID_KEYLEN,
ERR_INVALID_ARG_TYPE,
ERR_OUT_OF_RANGE,
hideStackFrames,
} from "ext:deno_node/internal/errors.ts";
import {
kHandle,
toBuf,
validateByteSource,
} from "ext:deno_node/internal/crypto/util.ts";
import {
createSecretKey,
KeyObject,
} from "ext:deno_node/internal/crypto/keys.ts";
import type { BinaryLike } from "ext:deno_node/internal/crypto/types.ts";
import { kMaxLength } from "ext:deno_node/internal/buffer.mjs";
import {
isAnyArrayBuffer,
isArrayBufferView,
} from "ext:deno_node/internal/util/types.ts";
import { isKeyObject } from "ext:deno_node/internal/crypto/_keys.ts";
import { getHashes } from "ext:deno_node/internal/crypto/hash.ts";
import { Buffer } from "node:buffer";
// Consume raw bytes for any ArrayBufferView/ArrayBuffer; strings via toBuf.
function toRawBytes(x: unknown): Buffer {
if (isArrayBufferView(x)) {
const v = x as ArrayBufferView;
return Buffer.from(v.buffer, v.byteOffset, v.byteLength);
}
if (isAnyArrayBuffer(x)) {
return Buffer.from(x as ArrayBufferLike);
}
// For strings / other BinaryLike, keep existing semantics (UTF-8 etc.)
return Buffer.from(toBuf(x as unknown as string));
}
const validateParameters = hideStackFrames((hash, key, salt, info, length) => {
validateString(hash, "digest");
key = prepareKey(key);
validateByteSource(salt, "salt");
validateByteSource(info, "info");
salt = toRawBytes(toBuf(salt));
info = toRawBytes(toBuf(info));
validateInteger(length, "length", 0, kMaxLength);
if (info.byteLength > 1024) {
throw new ERR_OUT_OF_RANGE(
"info",
"must not contain more than 1024 bytes",
info.byteLength,
);
}
validateAlgorithm(hash);
const size = op_node_get_hash_size(hash);
if (typeof size === "number" && size * 255 < length) {
throw new ERR_CRYPTO_INVALID_KEYLEN();
}
return {
hash,
key,
salt,
info,
length,
};
});
function prepareKey(key: BinaryLike | KeyObject) {
if (isKeyObject(key)) {
return key;
}
if (isAnyArrayBuffer(key)) {
return createSecretKey(new Uint8Array(key as unknown as ArrayBufferLike));
}
key = toBuf(key as string);
if (!isArrayBufferView(key)) {
throw new ERR_INVALID_ARG_TYPE(
"ikm",
[
"string",
"SecretKeyObject",
"ArrayBuffer",
"TypedArray",
"DataView",
"Buffer",
],
key,
);
}
return createSecretKey(key);
}
export function hkdf(
hash: string,
key: BinaryLike | KeyObject,
salt: BinaryLike,
info: BinaryLike,
length: number,
callback: (err: Error | null, derivedKey: ArrayBuffer | undefined) => void,
) {
({ hash, key, salt, info, length } = validateParameters(
hash,
key,
salt,
info,
length,
));
validateFunction(callback, "callback");
hash = hash.toLowerCase();
op_node_hkdf_async(hash, key[kHandle], salt, info, length)
.then((okm) => callback(null, okm.buffer))
.catch((err) => callback(new ERR_CRYPTO_INVALID_DIGEST(err), undefined));
}
export function hkdfSync(
hash: string,
key: BinaryLike | KeyObject,
salt: BinaryLike,
info: BinaryLike,
length: number,
) {
({ hash, key, salt, info, length } = validateParameters(
hash,
key,
salt,
info,
length,
));
hash = hash.toLowerCase();
const okm = new Uint8Array(length);
try {
op_node_hkdf(hash, key[kHandle], salt, info, okm);
} catch (e) {
throw new ERR_CRYPTO_INVALID_DIGEST(e);
}
return okm.buffer;
}
let hashes: Set<string> | null = null;
function validateAlgorithm(algorithm: string) {
if (hashes === null) {
hashes = new Set(getHashes());
}
if (!hashes.has(algorithm)) {
throw new ERR_CRYPTO_INVALID_DIGEST(algorithm);
}
}
export default {
hkdf,
hkdfSync,
};