summaryrefslogtreecommitdiff
path: root/ext/node/polyfills/internal/crypto/hash.ts
diff options
context:
space:
mode:
Diffstat (limited to 'ext/node/polyfills/internal/crypto/hash.ts')
-rw-r--r--ext/node/polyfills/internal/crypto/hash.ts230
1 files changed, 230 insertions, 0 deletions
diff --git a/ext/node/polyfills/internal/crypto/hash.ts b/ext/node/polyfills/internal/crypto/hash.ts
new file mode 100644
index 000000000..7995e5f8c
--- /dev/null
+++ b/ext/node/polyfills/internal/crypto/hash.ts
@@ -0,0 +1,230 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license.
+
+import {
+ TextDecoder,
+ TextEncoder,
+} from "internal:deno_web/08_text_encoding.js";
+import { Buffer } from "internal:deno_node/polyfills/buffer.ts";
+import { Transform } from "internal:deno_node/polyfills/stream.ts";
+import { encode as encodeToHex } from "internal:deno_node/polyfills/internal/crypto/_hex.ts";
+import {
+ forgivingBase64Encode as encodeToBase64,
+ forgivingBase64UrlEncode as encodeToBase64Url,
+} from "internal:deno_web/00_infra.js";
+import type { TransformOptions } from "internal:deno_node/polyfills/_stream.d.ts";
+import { validateString } from "internal:deno_node/polyfills/internal/validators.mjs";
+import type {
+ BinaryToTextEncoding,
+ Encoding,
+} from "internal:deno_node/polyfills/internal/crypto/types.ts";
+import {
+ KeyObject,
+ prepareSecretKey,
+} from "internal:deno_node/polyfills/internal/crypto/keys.ts";
+import { notImplemented } from "internal:deno_node/polyfills/_utils.ts";
+
+const { ops } = globalThis.__bootstrap.core;
+
+const coerceToBytes = (data: string | BufferSource): Uint8Array => {
+ if (data instanceof Uint8Array) {
+ return data;
+ } else if (typeof data === "string") {
+ // This assumes UTF-8, which may not be correct.
+ return new TextEncoder().encode(data);
+ } else if (ArrayBuffer.isView(data)) {
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+ } else if (data instanceof ArrayBuffer) {
+ return new Uint8Array(data);
+ } else {
+ throw new TypeError("expected data to be string | BufferSource");
+ }
+};
+
+/**
+ * The Hash class is a utility for creating hash digests of data. It can be used in one of two ways:
+ *
+ * - As a stream that is both readable and writable, where data is written to produce a computed hash digest on the readable side, or
+ * - Using the hash.update() and hash.digest() methods to produce the computed hash.
+ *
+ * The crypto.createHash() method is used to create Hash instances. Hash objects are not to be created directly using the new keyword.
+ */
+export class Hash extends Transform {
+ #context: number;
+
+ constructor(
+ algorithm: string | number,
+ _opts?: TransformOptions,
+ ) {
+ super({
+ transform(chunk: string, _encoding: string, callback: () => void) {
+ ops.op_node_hash_update(context, coerceToBytes(chunk));
+ callback();
+ },
+ flush(callback: () => void) {
+ this.push(context.digest(undefined));
+ callback();
+ },
+ });
+
+ if (typeof algorithm === "string") {
+ this.#context = ops.op_node_create_hash(
+ algorithm,
+ );
+ } else {
+ this.#context = algorithm;
+ }
+
+ const context = this.#context;
+ }
+
+ copy(): Hash {
+ return new Hash(ops.op_node_clone_hash(this.#context));
+ }
+
+ /**
+ * Updates the hash content with the given data.
+ */
+ update(data: string | ArrayBuffer, _encoding?: string): this {
+ let bytes;
+ if (typeof data === "string") {
+ data = new TextEncoder().encode(data);
+ bytes = coerceToBytes(data);
+ } else {
+ bytes = coerceToBytes(data);
+ }
+
+ ops.op_node_hash_update(this.#context, bytes);
+
+ return this;
+ }
+
+ /**
+ * Calculates the digest of all of the data.
+ *
+ * If encoding is provided a string will be returned; otherwise a Buffer is returned.
+ *
+ * Supported encodings are currently 'hex', 'binary', 'base64', 'base64url'.
+ */
+ digest(encoding?: string): Buffer | string {
+ const digest = this.#context.digest(undefined);
+ if (encoding === undefined) {
+ return Buffer.from(digest);
+ }
+
+ switch (encoding) {
+ case "hex":
+ return new TextDecoder().decode(encodeToHex(new Uint8Array(digest)));
+ case "binary":
+ return String.fromCharCode(...digest);
+ case "base64":
+ return encodeToBase64(digest);
+ case "base64url":
+ return encodeToBase64Url(digest);
+ case "buffer":
+ return Buffer.from(digest);
+ default:
+ return Buffer.from(digest).toString(encoding);
+ }
+ }
+}
+
+export function Hmac(
+ hmac: string,
+ key: string | ArrayBuffer | KeyObject,
+ options?: TransformOptions,
+): Hmac {
+ return new HmacImpl(hmac, key, options);
+}
+
+type Hmac = HmacImpl;
+
+class HmacImpl extends Transform {
+ #ipad: Uint8Array;
+ #opad: Uint8Array;
+ #ZEROES = Buffer.alloc(128);
+ #algorithm: string;
+ #hash: Hash;
+
+ constructor(
+ hmac: string,
+ key: string | ArrayBuffer | KeyObject,
+ options?: TransformOptions,
+ ) {
+ super({
+ transform(chunk: string, encoding: string, callback: () => void) {
+ // deno-lint-ignore no-explicit-any
+ self.update(coerceToBytes(chunk), encoding as any);
+ callback();
+ },
+ flush(callback: () => void) {
+ this.push(self.digest());
+ callback();
+ },
+ });
+ // 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 alg = hmac.toLowerCase();
+ this.#hash = new Hash(alg, options);
+ this.#algorithm = alg;
+ const blockSize = (alg === "sha512" || alg === "sha384") ? 128 : 64;
+ const keySize = u8Key.length;
+
+ let bufKey: Buffer;
+
+ if (keySize > blockSize) {
+ bufKey = this.#hash.update(u8Key).digest() as Buffer;
+ } else {
+ bufKey = Buffer.concat([u8Key, this.#ZEROES], blockSize);
+ }
+
+ this.#ipad = Buffer.allocUnsafe(blockSize);
+ this.#opad = Buffer.allocUnsafe(blockSize);
+
+ for (let i = 0; i < blockSize; i++) {
+ this.#ipad[i] = bufKey[i] ^ 0x36;
+ this.#opad[i] = bufKey[i] ^ 0x5C;
+ }
+
+ this.#hash = new Hash(alg);
+ this.#hash.update(this.#ipad);
+ }
+
+ digest(): Buffer;
+ digest(encoding: BinaryToTextEncoding): string;
+ digest(encoding?: BinaryToTextEncoding): Buffer | string {
+ const result = this.#hash.digest();
+
+ return new Hash(this.#algorithm).update(this.#opad).update(result).digest(
+ encoding,
+ );
+ }
+
+ update(data: string | ArrayBuffer, inputEncoding?: Encoding): this {
+ this.#hash.update(data, inputEncoding);
+ return this;
+ }
+}
+
+Hmac.prototype = HmacImpl.prototype;
+
+/**
+ * Creates and returns a Hash object that can be used to generate hash digests
+ * using the given `algorithm`. Optional `options` argument controls stream behavior.
+ */
+export function createHash(algorithm: string, opts?: TransformOptions) {
+ return new Hash(algorithm, opts);
+}
+
+export default {
+ Hash,
+ Hmac,
+ createHash,
+};