Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: implement crypto.hash() #51044

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 42 additions & 0 deletions benchmark/crypto/oneshot-hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

const common = require('../common.js');
const { createHash, hash } = require('crypto');
const path = require('path');
const filepath = path.resolve(__dirname, '../../test/fixtures/snapshot/typescript.js');
const fs = require('fs');
const assert = require('assert');

const bench = common.createBenchmark(main, {
length: [1000, 100_000],
method: ['md5', 'sha1', 'sha256'],
type: ['string', 'buffer'],
n: [100_000, 1000],
}, {
combinationFilter: ({ length, n }) => {
return length * n <= 100_000 * 1000;
},
});

function main({ length, type, method, n }) {
let data = fs.readFileSync(filepath);
if (type === 'string') {
data = data.toString().slice(0, length);
} else {
data = Uint8Array.prototype.slice.call(data, 0, length);
}

const oneshotHash = hash ?
(method, input) => hash(method, input, 'hex') :
(method, input) => createHash(method).update(input).digest('hex');
const array = [];
for (let i = 0; i < n; i++) {
array.push(null);
}
bench.start();
for (let i = 0; i < n; i++) {
array[i] = oneshotHash(method, data);
}
bench.end(n);
assert.strictEqual(typeof array[n - 1], 'string');
}
61 changes: 61 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -3510,6 +3510,67 @@ Computes the Diffie-Hellman secret based on a `privateKey` and a `publicKey`.
Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'`
(for Diffie-Hellman), `'ec'` (for ECDH), `'x448'`, or `'x25519'` (for ECDH-ES).

### `crypto.hash(algorith, data[, outputEncoding])`

<!-- YAML
added:
- REPLACEME
-->

* `algorithm` {string|undefined}
* `data` {string|ArrayBuffer|Buffer|TypedArray|DataView} When `data` is a
marco-ippolito marked this conversation as resolved.
Show resolved Hide resolved
string, it will be encoded as UTF-8 before being hashed. If a different
input encoding is desired for a string input, user could encode the string
into a `TypedArray` using either `TextEncoder` or `Buffer.from()` and passing
the encoded `TypedArray` into this API instead.
* `outputEncoding` {string|undefined} [Encoding][encoding] used to encode the
returned digest. **Default:** `'hex'`.
* Returns: {string|Buffer}

A utility for creating one-shot hash digests of data. It can be faster than
the object-based `crypto.createHash()` when hashing a smaller amount of data
(<= 5MB) that's readily available. If the data can be big or if it is streamed,
it's still recommended to use `crypto.createHash()` instead.

The `algorithm` is dependent on the available algorithms supported by the
version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc.
On recent releases of OpenSSL, `openssl list -digest-algorithms` will
display the available digest algorithms.

Example:

```cjs
const crypto = require('node:crypto');
const { Buffer } = require('node:buffer');

// Hashing a string and return the result as a hex-encoded string.
const string = 'Node.js';
// 10b3493287f831e81a438811a1ffba01f8cec4b7
console.log(crypto.hash('sha1', string));

// Encode a base64-encoded string into a Buffer, hash it and return
// the result as a buffer.
const base64 = 'Tm9kZS5qcw==';
// <Buffer 10 b3 49 32 87 f8 31 e8 1a 43 88 11 a1 ff ba 01 f8 ce c4 b7>
console.log(crypto.hash('sha1', Buffer.from(base64, 'base64'), 'buffer'));
```

```mjs
import crypto from 'node:crypto';
import { Buffer } from 'node:buffer';

// Hashing a string and return the result as a hex-encoded string.
const string = 'Node.js';
// 10b3493287f831e81a438811a1ffba01f8cec4b7
console.log(crypto.hash('sha1', string));

// Encode a base64-encoded string into a Buffer, hash it and return
// the result as a buffer.
const base64 = 'Tm9kZS5qcw==';
// <Buffer 10 b3 49 32 87 f8 31 e8 1a 43 88 11 a1 ff ba 01 f8 ce c4 b7>
console.log(crypto.hash('sha1', Buffer.from(base64, 'base64'), 'buffer'));
```

### `crypto.generateKey(type, options, callback)`

<!-- YAML
Expand Down
6 changes: 1 addition & 5 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const {
normalizeEncoding,
kIsEncodingSymbol,
defineLazyProperties,
encodingsMap,
} = require('internal/util');
const {
isAnyArrayBuffer,
Expand All @@ -95,7 +96,6 @@ const {
const {
inspect: utilInspect,
} = require('internal/util/inspect');
const { encodings } = internalBinding('string_decoder');

const {
codes: {
Expand Down Expand Up @@ -149,10 +149,6 @@ const constants = ObjectDefineProperties({}, {
Buffer.poolSize = 8 * 1024;
let poolSize, poolOffset, allocPool;

const encodingsMap = { __proto__: null };
for (let i = 0; i < encodings.length; ++i)
encodingsMap[encodings[i]] = i;

function createPool() {
poolSize = Buffer.poolSize;
allocPool = createUnsafeBuffer(poolSize).buffer;
Expand Down
2 changes: 2 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ const {
const {
Hash,
Hmac,
hash,
} = require('internal/crypto/hash');
const {
X509Certificate,
Expand Down Expand Up @@ -219,6 +220,7 @@ module.exports = {
getFips,
setFips,
verify: verifyOneShot,
hash,

// Classes
Certificate,
Expand Down
31 changes: 31 additions & 0 deletions lib/internal/crypto/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const {
ObjectSetPrototypeOf,
ReflectApply,
StringPrototypeToLowerCase,
Symbol,
} = primordials;

Expand All @@ -11,6 +12,7 @@ const {
HashJob,
Hmac: _Hmac,
kCryptoJobAsync,
oneShotDigest,
} = internalBinding('crypto');

const {
Expand All @@ -29,6 +31,8 @@ const {

const {
lazyDOMException,
normalizeEncoding,
encodingsMap,
} = require('internal/util');

const {
Expand All @@ -40,13 +44,15 @@ const {
ERR_CRYPTO_HASH_FINALIZED,
ERR_CRYPTO_HASH_UPDATE_FAILED,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
},
} = require('internal/errors');

const {
validateEncoding,
validateString,
validateUint32,
validateBuffer,
} = require('internal/validators');

const {
Expand Down Expand Up @@ -188,8 +194,33 @@ async function asyncDigest(algorithm, data) {
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}

function hash(algorithm, input, outputEncoding = 'hex') {
validateString(algorithm, 'algorithm');
if (typeof input !== 'string') {
validateBuffer(input, 'input');
}
let normalized = outputEncoding;
// Fast case: if it's 'hex', we don't need to validate it further.
if (outputEncoding !== 'hex') {
validateString(outputEncoding, 'outputEncoding');
normalized = normalizeEncoding(outputEncoding);
// If the encoding is invalid, normalizeEncoding() returns undefined.
if (normalized === undefined) {
// normalizeEncoding() doesn't handle 'buffer'.
if (StringPrototypeToLowerCase(outputEncoding) === 'buffer') {
normalized = 'buffer';
} else {
throw new ERR_INVALID_ARG_VALUE('outputEncoding', outputEncoding);
}
}
}
return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
input, normalized, encodingsMap[normalized]);
}

module.exports = {
Hash,
Hmac,
asyncDigest,
hash,
};
6 changes: 6 additions & 0 deletions lib/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const {
} = internalBinding('util');
const { isNativeError, isPromise } = internalBinding('types');
const { getOptionValue } = require('internal/options');
const { encodings } = internalBinding('string_decoder');

const noCrypto = !process.versions.openssl;

Expand Down Expand Up @@ -859,6 +860,10 @@ class WeakReference {
}
}

const encodingsMap = { __proto__: null };
for (let i = 0; i < encodings.length; ++i)
encodingsMap[encodings[i]] = i;

module.exports = {
getLazy,
assertCrypto,
Expand All @@ -872,6 +877,7 @@ module.exports = {
defineReplaceableLazyAttribute,
deprecate,
emitExperimentalWarning,
encodingsMap,
exposeInterface,
exposeLazyInterfaces,
exposeNamespace,
Expand Down
15 changes: 7 additions & 8 deletions lib/string_decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,18 @@ const {
kSize,
decode,
flush,
encodings,
} = internalBinding('string_decoder');
const internalUtil = require('internal/util');
const {
kIsEncodingSymbol,
encodingsMap,
normalizeEncoding: _normalizeEncoding,
} = require('internal/util');
const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_THIS,
ERR_UNKNOWN_ENCODING,
} = require('internal/errors').codes;
const isEncoding = Buffer[internalUtil.kIsEncodingSymbol];
const isEncoding = Buffer[kIsEncodingSymbol];

const kNativeDecoder = Symbol('kNativeDecoder');

Expand All @@ -60,7 +63,7 @@ const kNativeDecoder = Symbol('kNativeDecoder');
* @throws {TypeError} Throws an error when encoding is invalid
*/
function normalizeEncoding(enc) {
const nenc = internalUtil.normalizeEncoding(enc);
const nenc = _normalizeEncoding(enc);
if (nenc === undefined) {
if (Buffer.isEncoding === isEncoding || !Buffer.isEncoding(enc))
throw new ERR_UNKNOWN_ENCODING(enc);
Expand All @@ -69,10 +72,6 @@ function normalizeEncoding(enc) {
return nenc;
}

const encodingsMap = {};
for (let i = 0; i < encodings.length; ++i)
encodingsMap[encodings[i]] = i;

/**
* StringDecoder provides an interface for efficiently splitting a series of
* buffers into a series of JS strings without breaking apart multi-byte
Expand Down
10 changes: 10 additions & 0 deletions src/api/encoding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ enum encoding ParseEncoding(const char* encoding,
return default_encoding;
}

enum encoding ParseEncoding(Isolate* isolate,
Local<Value> encoding_v,
Local<Value> encoding_id,
enum encoding default_encoding) {
if (encoding_id->IsUint32()) {
return static_cast<enum encoding>(encoding_id.As<v8::Uint32>()->Value());
}

return ParseEncoding(isolate, encoding_v, default_encoding);
}

enum encoding ParseEncoding(Isolate* isolate,
Local<Value> encoding_v,
Expand Down