diff --git a/cli/tests/unit_node/crypto_key.ts b/cli/tests/unit_node/crypto_key.ts new file mode 100644 index 0000000000..d1a33db9e5 --- /dev/null +++ b/cli/tests/unit_node/crypto_key.ts @@ -0,0 +1,47 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { createSecretKey, randomBytes } from "node:crypto"; +import { Buffer } from "node:buffer"; +import { assertEquals } from "../../../test_util/std/testing/asserts.ts"; +import { createHmac } from "node:crypto"; + +Deno.test({ + name: "create secret key", + fn() { + const key = createSecretKey(Buffer.alloc(0)); + assertEquals(key.type, "secret"); + assertEquals(key.asymmetricKeyType, undefined); + assertEquals(key.symmetricKeySize, 0); + }, +}); + +Deno.test({ + name: "export secret key", + fn() { + const material = Buffer.from(randomBytes(32)); + const key = createSecretKey(material); + assertEquals(Buffer.from(key.export()), material); + }, +}); + +Deno.test({ + name: "export jwk secret key", + fn() { + const material = Buffer.from("secret"); + const key = createSecretKey(material); + assertEquals(key.export({ format: "jwk" }), { + kty: "oct", + k: "c2VjcmV0", + }); + }, +}); + +Deno.test({ + name: "createHmac with secret key", + fn() { + const key = createSecretKey(Buffer.from("secret")); + assertEquals( + createHmac("sha256", key).update("hello").digest().toString("hex"), + "88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b", + ); + }, +}); diff --git a/ext/node/polyfills/internal/crypto/hash.ts b/ext/node/polyfills/internal/crypto/hash.ts index d81844f30e..63c92190c1 100644 --- a/ext/node/polyfills/internal/crypto/hash.ts +++ b/ext/node/polyfills/internal/crypto/hash.ts @@ -15,10 +15,10 @@ import type { Encoding, } from "ext:deno_node/internal/crypto/types.ts"; import { + getKeyMaterial, KeyObject, prepareSecretKey, } from "ext:deno_node/internal/crypto/keys.ts"; -import { notImplemented } from "ext:deno_node/_utils.ts"; const { ops } = globalThis.__bootstrap.core; @@ -170,12 +170,12 @@ class HmacImpl extends Transform { }); // deno-lint-ignore no-this-alias const self = this; - if (key instanceof KeyObject) { - notImplemented("Hmac: KeyObject key is not implemented"); - } validateString(hmac, "hmac"); - const u8Key = prepareSecretKey(key, options?.encoding) as Buffer; + + const u8Key = key instanceof KeyObject + ? getKeyMaterial(key) + : prepareSecretKey(key, options?.encoding) as Buffer; const alg = hmac.toLowerCase(); this.#algorithm = alg; diff --git a/ext/node/polyfills/internal/crypto/keys.ts b/ext/node/polyfills/internal/crypto/keys.ts index 0348ee0e41..eb7990b069 100644 --- a/ext/node/polyfills/internal/crypto/keys.ts +++ b/ext/node/polyfills/internal/crypto/keys.ts @@ -28,6 +28,13 @@ import { isKeyObject as isKeyObject_, kKeyType, } from "ext:deno_node/internal/crypto/_keys.ts"; +import { + validateObject, + validateOneOf, +} from "ext:deno_node/internal/validators.mjs"; +import { + forgivingBase64UrlEncode as encodeToBase64Url, +} from "ext:deno_web/00_infra.js"; const getArrayBufferOrView = hideStackFrames( ( @@ -130,6 +137,17 @@ export function isCryptoKey( return isCryptoKey_(obj); } +function copyBuffer(input: string | Buffer | ArrayBufferView) { + if (typeof input === "string") return Buffer.from(input); + return ( + (ArrayBuffer.isView(input) + ? new Uint8Array(input.buffer, input.byteOffset, input.byteLength) + : new Uint8Array(input)).slice() + ); +} + +const KEY_STORE = new WeakMap(); + export class KeyObject { [kKeyType]: KeyObjectType; [kHandle]: unknown; @@ -139,18 +157,8 @@ export class KeyObject { throw new ERR_INVALID_ARG_VALUE("type", type); } - if (typeof handle !== "object") { - throw new ERR_INVALID_ARG_TYPE("handle", "object", handle); - } - this[kKeyType] = type; - - Object.defineProperty(this, kHandle, { - value: handle, - enumerable: false, - configurable: false, - writable: false, - }); + this[kHandle] = handle; } get type(): KeyObjectType { @@ -239,7 +247,7 @@ function getKeyTypes(allowKeyObject: boolean, bufferOnly = false) { } export function prepareSecretKey( - key: string | ArrayBuffer | KeyObject, + key: string | ArrayBufferView | ArrayBuffer | KeyObject, encoding: string | undefined, bufferOnly = false, ) { @@ -271,16 +279,62 @@ export function prepareSecretKey( return getArrayBufferOrView(key, "key", encoding); } +class SecretKeyObject extends KeyObject { + constructor(handle: unknown) { + super("secret", handle); + } + + get symmetricKeySize() { + return KEY_STORE.get(this[kHandle]).byteLength; + } + + get asymmetricKeyType() { + return undefined; + } + + export(): Buffer; + export(options?: JwkKeyExportOptions): JsonWebKey { + const key = KEY_STORE.get(this[kHandle]); + if (options !== undefined) { + validateObject(options, "options"); + validateOneOf( + options.format, + "options.format", + [undefined, "buffer", "jwk"], + ); + if (options.format === "jwk") { + return { + kty: "oct", + k: encodeToBase64Url(key), + }; + } + } + return key.slice(); + } +} + +function setOwnedKey(key: Uint8Array): unknown { + const handle = {}; + KEY_STORE.set(handle, key); + return handle; +} + +export function getKeyMaterial(key: KeyObject): Uint8Array { + return KEY_STORE.get(key[kHandle]); +} + export function createSecretKey(key: ArrayBufferView): KeyObject; export function createSecretKey( key: string, encoding: string, ): KeyObject; export function createSecretKey( - _key: string | ArrayBufferView, - _encoding?: string, + key: string | ArrayBufferView, + encoding?: string, ): KeyObject { - notImplemented("crypto.createSecretKey"); + key = prepareSecretKey(key, encoding, true); + const handle = setOwnedKey(copyBuffer(key)); + return new SecretKeyObject(handle); } export default {