1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-30 16:40:57 -05:00
denoland-deno/ext/node/polyfills/internal/crypto/hash.ts

255 lines
6.9 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. 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_create_hash,
op_node_get_hashes,
op_node_hash_clone,
op_node_hash_digest,
op_node_hash_digest_hex,
op_node_hash_update,
op_node_hash_update_str,
} from "ext:core/ops";
import { TextEncoder } from "ext:deno_web/08_text_encoding.js";
import { Buffer } from "node:buffer";
import { Transform } from "node:stream";
import {
forgivingBase64Encode as encodeToBase64,
forgivingBase64UrlEncode as encodeToBase64Url,
} from "ext:deno_web/00_infra.js";
import type { TransformOptions } from "ext:deno_node/_stream.d.ts";
import { validateString } from "ext:deno_node/internal/validators.mjs";
import type {
BinaryToTextEncoding,
Encoding,
} from "ext:deno_node/internal/crypto/types.ts";
import {
getKeyMaterial,
KeyObject,
prepareSecretKey,
} from "ext:deno_node/internal/crypto/keys.ts";
// TODO(@littledivy): Use Result<T, E> instead of boolean when
// https://bugs.chromium.org/p/v8/issues/detail?id=13600 is fixed.
function unwrapErr(ok: boolean) {
if (!ok) {
throw new Error("Context is not initialized");
}
}
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) {
op_node_hash_update(context, coerceToBytes(chunk));
callback();
},
flush(callback: () => void) {
this.push(this.digest(undefined));
callback();
},
});
if (typeof algorithm === "string") {
this.#context = op_node_create_hash(
algorithm.toLowerCase(),
);
if (this.#context === 0) {
throw new TypeError(`Unknown hash algorithm: ${algorithm}`);
}
} else {
this.#context = algorithm;
}
const context = this.#context;
}
copy(): Hash {
return new Hash(op_node_hash_clone(this.#context));
}
/**
* Updates the hash content with the given data.
*/
update(data: string | ArrayBuffer, _encoding?: string): this {
if (typeof data === "string") {
unwrapErr(op_node_hash_update_str(this.#context, data));
} else {
unwrapErr(op_node_hash_update(this.#context, coerceToBytes(data)));
}
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 {
if (encoding === "hex") {
return op_node_hash_digest_hex(this.#context);
}
const digest = op_node_hash_digest(this.#context);
if (encoding === undefined) {
return Buffer.from(digest);
}
// TODO(@littedivy): Fast paths for below encodings.
switch (encoding) {
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;
validateString(hmac, "hmac");
const u8Key = key instanceof KeyObject
? getKeyMaterial(key)
: prepareSecretKey(key, options?.encoding) as Buffer;
const alg = hmac.toLowerCase();
this.#algorithm = alg;
const blockSize = (alg === "sha512" || alg === "sha384") ? 128 : 64;
const keySize = u8Key.length;
let bufKey: Buffer;
if (keySize > blockSize) {
const hash = new Hash(alg, options);
bufKey = 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);
}
/**
* Get the list of implemented hash algorithms.
* @returns Array of hash algorithm names.
*/
export function getHashes() {
return op_node_get_hashes();
}
export default {
Hash,
Hmac,
createHash,
};