Skip to content
File

Blob: src/node/internal/crypto_hash.ts

typescript326 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26import { default as cryptoImpl } from 'node-internal:crypto';
27type ArrayLike = cryptoImpl.ArrayLike;
28 
29import {
30 kFinalized,
31 kHandle,
32 kState,
33 getArrayBufferOrView,
34 getStringOption,
35} from 'node-internal:crypto_util';
36 
37import { Buffer } from 'node-internal:internal_buffer';
38 
39import {
40 ERR_CRYPTO_HASH_FINALIZED,
41 ERR_CRYPTO_HASH_UPDATE_FAILED,
42 ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE,
43 ERR_INVALID_ARG_TYPE,
44} from 'node-internal:internal_errors';
45 
46import { validateString, validateUint32 } from 'node-internal:validators';
47 
48import {
49 isArrayBufferView,
50 isCryptoKey,
51 isAnyArrayBuffer,
52} from 'node-internal:internal_types';
53 
54import {
55 Transform,
56 type TransformOptions,
57 type TransformCallback,
58} from 'node-internal:streams_transform';
59 
60import { KeyObject } from 'node-internal:crypto_keys';
61 
62export interface HashOptions extends TransformOptions {
63 outputLength?: number;
64}
65 
66interface _kState {
67 [kFinalized]: boolean;
68}
69 
70declare class Hash extends Transform {
71 [kHandle]: cryptoImpl.HashHandle;
72 [kState]: _kState;
73 
74 constructor(algorithm: string | cryptoImpl.HashHandle, options?: HashOptions);
75 
76 copy(options?: HashOptions): Hash;
77 update(
78 data: string | Buffer | ArrayBufferView,
79 encoding?: string
80 ): Hash | Hmac;
81 digest(outputEncoding?: string): Buffer | string;
82}
83 
84// These helper functions are needed because the constructors can
85// use new, in which case V8 cannot inline the recursive constructor call
86export function createHash(algorithm: string, options?: HashOptions): Hash {
87 return new Hash(algorithm, options);
88}
89 
90function Hash(
91 this: unknown,
92 algorithm: string | cryptoImpl.HashHandle,
93 options?: HashOptions
94): Hash {
95 if (!(this instanceof Hash)) {
96 return new Hash(algorithm, options);
97 }
98 
99 const xofLen = typeof options === 'object' ? options.outputLength : undefined;
100 if (xofLen !== undefined) validateUint32(xofLen, 'options.outputLength');
101 if (algorithm instanceof cryptoImpl.HashHandle) {
102 this[kHandle] = algorithm.copy(xofLen as number);
103 } else {
104 validateString(algorithm, 'algorithm');
105 this[kHandle] = new cryptoImpl.HashHandle(algorithm, xofLen as number);
106 }
107 this[kState] = {
108 [kFinalized]: false,
109 };
110 
111 Transform.call(this, options);
112 return this;
113}
114 
115Object.setPrototypeOf(Hash.prototype, Transform.prototype);
116Object.setPrototypeOf(Hash, Transform);
117 
118Hash.prototype.copy = function (this: Hash, options?: HashOptions): Hash {
119 const state = this[kState];
120 if (state[kFinalized]) throw new ERR_CRYPTO_HASH_FINALIZED();
121 
122 return new Hash(this[kHandle], options);
123};
124 
125Hash.prototype._transform = function (
126 this: Hash | Hmac,
127 chunk: string | Buffer | ArrayBufferView,
128 encoding: string,
129 callback: TransformCallback
130): void {
131 if (typeof chunk === 'string') {
132 chunk = Buffer.from(chunk, encoding);
133 }
134 this[kHandle].update(chunk);
135 callback();
136};
137 
138Hash.prototype._flush = function (
139 this: Hash | Hmac,
140 callback: TransformCallback
141): void {
142 this.push(Buffer.from(this[kHandle].digest()));
143 callback();
144};
145 
146Hash.prototype.update = function (
147 this: Hash | Hmac,
148 data: string | Buffer | ArrayBufferView,
149 encoding?: string
150): Hash | Hmac {
151 encoding ??= 'utf8';
152 if (encoding === 'buffer') {
153 encoding = undefined;
154 }
155 
156 const state = this[kState];
157 if (state[kFinalized]) throw new ERR_CRYPTO_HASH_FINALIZED();
158 
159 if (typeof data === 'string') {
160 data = Buffer.from(data, encoding);
161 } else if (!isArrayBufferView(data)) {
162 throw new ERR_INVALID_ARG_TYPE(
163 'data',
164 ['string', 'Buffer', 'TypedArray', 'DataView'],
165 data
166 );
167 }
168 
169 if (!this[kHandle].update(data)) throw new ERR_CRYPTO_HASH_UPDATE_FAILED();
170 return this;
171};
172 
173Hash.prototype.digest = function (
174 this: Hash,
175 outputEncoding?: string
176): Buffer | string {
177 const state = this[kState];
178 if (state[kFinalized]) throw new ERR_CRYPTO_HASH_FINALIZED();
179 
180 // Explicit conversion for backward compatibility.
181 const ret = Buffer.from(this[kHandle].digest());
182 state[kFinalized] = true;
183 if (outputEncoding !== undefined && outputEncoding !== 'buffer') {
184 return ret.toString(outputEncoding);
185 } else {
186 return ret;
187 }
188};
189 
190///////////////////////////
191 
192declare class Hmac extends Transform {
193 [kHandle]: cryptoImpl.HmacHandle;
194 [kState]: _kState;
195 constructor(
196 hmac: string,
197 key: ArrayLike | KeyObject | CryptoKey,
198 options?: TransformOptions
199 );
200 copy(options?: HashOptions): Hash;
201 update(
202 data: string | Buffer | ArrayBufferView,
203 encoding?: string
204 ): Hash | Hmac;
205 digest(outputEncoding?: string): Buffer | string;
206}
207 
208export function createHmac(
209 hmac: string,
210 key: CryptoKey,
211 options?: TransformOptions
212): Hmac {
213 return new Hmac(hmac, key, options);
214}
215 
216function Hmac(
217 this: Hmac,
218 hmac: string,
219 key: CryptoKey,
220 options?: TransformOptions
221): Hmac {
222 if (!(this instanceof Hmac)) {
223 return new Hmac(hmac, key, options);
224 }
225 validateString(hmac, 'hmac');
226 const encoding = getStringOption(options, 'encoding');
227 
228 if (key instanceof KeyObject) {
229 if (key.type !== 'secret') {
230 throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret');
231 }
232 this[kHandle] = new cryptoImpl.HmacHandle(hmac, key[kHandle]);
233 } else if (isCryptoKey(key)) {
234 if (key.type !== 'secret') {
235 throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret');
236 }
237 this[kHandle] = new cryptoImpl.HmacHandle(hmac, key);
238 } else if (
239 typeof key !== 'string' &&
240 !isArrayBufferView(key) &&
241 !isAnyArrayBuffer(key)
242 ) {
243 throw new ERR_INVALID_ARG_TYPE(
244 'key',
245 [
246 'ArrayBuffer',
247 'Buffer',
248 'ArrayBufferView',
249 'string',
250 'KeyObject',
251 'CryptoKey',
252 ],
253 key
254 );
255 } else {
256 this[kHandle] = new cryptoImpl.HmacHandle(
257 hmac,
258 getArrayBufferOrView(key, 'key', encoding)
259 );
260 }
261 
262 this[kState] = {
263 [kFinalized]: false,
264 };
265 Transform.call(this, options);
266 return this;
267}
268Object.setPrototypeOf(Hmac.prototype, Transform.prototype);
269Object.setPrototypeOf(Hmac, Transform);
270 
271// eslint-disable-next-line @typescript-eslint/unbound-method
272Hmac.prototype.update = Hash.prototype.update;
273 
274Hmac.prototype.digest = function (
275 this: Hmac,
276 outputEncoding?: string
277): Buffer | string {
278 const state = this[kState];
279 if (state[kFinalized]) {
280 return !outputEncoding || outputEncoding === 'buffer'
281 ? Buffer.from('')
282 : '';
283 }
284 
285 // Explicit conversion for backward compatibility.
286 const ret = Buffer.from(this[kHandle].digest());
287 state[kFinalized] = true;
288 if (outputEncoding !== undefined && outputEncoding !== 'buffer') {
289 return ret.toString(outputEncoding);
290 } else {
291 return ret;
292 }
293};
294 
295// eslint-disable-next-line @typescript-eslint/unbound-method
296Hmac.prototype._flush = Hash.prototype._flush;
297// eslint-disable-next-line @typescript-eslint/unbound-method
298Hmac.prototype._transform = Hash.prototype._transform;
299 
300export function hash(
301 algorithm: string,
302 data: string | ArrayBufferView,
303 outputEncoding: string = 'hex'
304): string | Buffer {
305 validateString(algorithm, 'algorithm');
306 validateString(outputEncoding, 'outputEncoding');
307 
308 if (typeof data === 'string') {
309 const hash = createHash(algorithm);
310 hash.update(data, 'utf8');
311 return hash.digest(outputEncoding);
312 } else if (isArrayBufferView(data)) {
313 const hash = createHash(algorithm);
314 hash.update(data, 'utf8');
315 return hash.digest(outputEncoding);
316 }
317 
318 throw new ERR_INVALID_ARG_TYPE(
319 'data',
320 ['string', 'Buffer', 'TypedArray', 'DataView'],
321 data
322 );
323}
324 
325export { Hash, Hmac };