UNPKG

15.8 kBPlain TextView Raw
1import { getStrConverter, getOutputConverter } from "./converters";
2
3import {
4 FormatType,
5 EncodingType,
6 FixedLengthOptionsEncodingType,
7 FixedLengthOptionsNoEncodingType,
8 FormatNoTextType,
9 packedValue,
10 GenericInputType,
11} from "./custom_types";
12
13export const TWO_PWR_32 = 4294967296;
14
15/* Constant used in SHA-2 families */
16export const K_sha2 = [
17 0x428a2f98,
18 0x71374491,
19 0xb5c0fbcf,
20 0xe9b5dba5,
21 0x3956c25b,
22 0x59f111f1,
23 0x923f82a4,
24 0xab1c5ed5,
25 0xd807aa98,
26 0x12835b01,
27 0x243185be,
28 0x550c7dc3,
29 0x72be5d74,
30 0x80deb1fe,
31 0x9bdc06a7,
32 0xc19bf174,
33 0xe49b69c1,
34 0xefbe4786,
35 0x0fc19dc6,
36 0x240ca1cc,
37 0x2de92c6f,
38 0x4a7484aa,
39 0x5cb0a9dc,
40 0x76f988da,
41 0x983e5152,
42 0xa831c66d,
43 0xb00327c8,
44 0xbf597fc7,
45 0xc6e00bf3,
46 0xd5a79147,
47 0x06ca6351,
48 0x14292967,
49 0x27b70a85,
50 0x2e1b2138,
51 0x4d2c6dfc,
52 0x53380d13,
53 0x650a7354,
54 0x766a0abb,
55 0x81c2c92e,
56 0x92722c85,
57 0xa2bfe8a1,
58 0xa81a664b,
59 0xc24b8b70,
60 0xc76c51a3,
61 0xd192e819,
62 0xd6990624,
63 0xf40e3585,
64 0x106aa070,
65 0x19a4c116,
66 0x1e376c08,
67 0x2748774c,
68 0x34b0bcb5,
69 0x391c0cb3,
70 0x4ed8aa4a,
71 0x5b9cca4f,
72 0x682e6ff3,
73 0x748f82ee,
74 0x78a5636f,
75 0x84c87814,
76 0x8cc70208,
77 0x90befffa,
78 0xa4506ceb,
79 0xbef9a3f7,
80 0xc67178f2,
81];
82
83/* Constant used in SHA-2 families */
84export const H_trunc = [0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4];
85
86/* Constant used in SHA-2 families */
87export const H_full = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
88
89export const sha_variant_error = "Chosen SHA variant is not supported";
90export const mac_rounds_error = "Cannot set numRounds with MAC";
91
92/**
93 * Concatenates 2 packed arrays. Clobbers array `a`.
94 *
95 * @param a First array to concatenate.
96 * @param b Second array to concatenate.
97 * @returns The concatentation of `a` + `b`.
98 */
99export function packedLEConcat(a: packedValue, b: packedValue): packedValue {
100 let i, arrOffset;
101 const aByteLen = a["binLen"] >>> 3,
102 bByteLen = b["binLen"] >>> 3,
103 leftShiftAmount = aByteLen << 3,
104 rightShiftAmount = (4 - aByteLen) << 3;
105
106 /* If a only contains "full" integers, we can just use concat which is so much easier */
107 if (aByteLen % 4 !== 0) {
108 for (i = 0; i < bByteLen; i += 4) {
109 arrOffset = (aByteLen + i) >>> 2;
110 /* Left shift chops off bits over 32-bits */
111 a["value"][arrOffset] |= b["value"][i >>> 2] << leftShiftAmount;
112 a["value"].push(0);
113 a["value"][arrOffset + 1] |= b["value"][i >>> 2] >>> rightShiftAmount;
114 }
115
116 /* Since an unconditional push was performed above, we may have pushed an extra value if it could have been
117 encoded without it. Check if popping an int off (reducing total length by 4 bytes) is still bigger than the
118 needed size. */
119 if ((a["value"].length << 2) - 4 >= bByteLen + aByteLen) {
120 a["value"].pop();
121 }
122
123 return { value: a["value"], binLen: a["binLen"] + b["binLen"] };
124 } else {
125 return { value: a["value"].concat(b["value"]), binLen: a["binLen"] + b["binLen"] };
126 }
127}
128
129/**
130 * Validate hash list containing output formatting options, ensuring presence of every option or adding the default
131 * value.
132 *
133 * @param options Hashmap of output formatting options from user.
134 * @returns Validated hashmap containing output formatting options.
135 */
136export function getOutputOpts(options?: {
137 outputUpper?: boolean;
138 b64Pad?: string;
139 shakeLen?: number;
140 outputLen?: number;
141}): { outputUpper: boolean; b64Pad: string; outputLen: number } {
142 const retVal = { outputUpper: false, b64Pad: "=", outputLen: -1 },
143 outputOptions: { outputUpper?: boolean; b64Pad?: string; shakeLen?: number; outputLen?: number } = options || {},
144 lenErrstr = "Output length must be a multiple of 8";
145
146 retVal["outputUpper"] = outputOptions["outputUpper"] || false;
147
148 if (outputOptions["b64Pad"]) {
149 retVal["b64Pad"] = outputOptions["b64Pad"];
150 }
151
152 if (outputOptions["outputLen"]) {
153 if (outputOptions["outputLen"] % 8 !== 0) {
154 throw new Error(lenErrstr);
155 }
156 retVal["outputLen"] = outputOptions["outputLen"];
157 } else if (outputOptions["shakeLen"]) {
158 if (outputOptions["shakeLen"] % 8 !== 0) {
159 throw new Error(lenErrstr);
160 }
161 retVal["outputLen"] = outputOptions["shakeLen"];
162 }
163
164 if ("boolean" !== typeof retVal["outputUpper"]) {
165 throw new Error("Invalid outputUpper formatting option");
166 }
167
168 if ("string" !== typeof retVal["b64Pad"]) {
169 throw new Error("Invalid b64Pad formatting option");
170 }
171
172 return retVal;
173}
174
175/**
176 * Parses an external constructor object and returns a packed number, if possible.
177 *
178 * @param key The human-friendly key name to prefix any errors with
179 * @param value The input value object to parse
180 * @param bigEndianMod Modifier for whether hash function is big or small endian.
181 * @param fallback Fallback value if `value` is undefined. If not present and `value` is undefined, an Error is thrown
182 */
183export function parseInputOption(
184 key: string,
185 value: GenericInputType | undefined,
186 bigEndianMod: -1 | 1,
187 fallback?: packedValue
188): packedValue {
189 const errStr = key + " must include a value and format";
190 if (!value) {
191 if (!fallback) {
192 throw new Error(errStr);
193 }
194 return fallback;
195 }
196
197 if (typeof value["value"] === "undefined" || !value["format"]) {
198 throw new Error(errStr);
199 }
200
201 return getStrConverter(
202 value["format"],
203 // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
204 // @ts-ignore - the value of encoding gets value checked by getStrConverter
205 value["encoding"] || "UTF8",
206 bigEndianMod
207 )(value["value"]);
208}
209
210export abstract class jsSHABase<StateT, VariantT> {
211 /**
212 * @param variant The desired SHA variant.
213 * @param inputFormat The input format to be used in future `update` calls.
214 * @param options Hashmap of extra input options.
215 */
216 /* Needed inputs */
217 protected readonly shaVariant: VariantT;
218 protected readonly inputFormat: FormatType;
219 protected readonly utfType: EncodingType;
220 protected readonly numRounds: number;
221
222 /* State */
223 protected abstract intermediateState: StateT;
224 protected keyWithIPad: number[];
225 protected keyWithOPad: number[];
226 protected remainder: number[];
227 protected remainderLen: number;
228 protected updateCalled: boolean;
229 protected processedLen: number;
230 protected macKeySet: boolean;
231
232 /* Variant specifics */
233 protected abstract readonly variantBlockSize: number;
234 protected abstract readonly bigEndianMod: -1 | 1;
235 protected abstract readonly outputBinLen: number;
236 protected abstract readonly isVariableLen: boolean;
237 protected abstract readonly HMACSupported: boolean;
238
239 /* Functions */
240 /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
241 protected abstract readonly converterFunc: (input: any, existingBin: number[], existingBinLen: number) => packedValue;
242 protected abstract readonly roundFunc: (block: number[], H: StateT) => StateT;
243 protected abstract readonly finalizeFunc: (
244 remainder: number[],
245 remainderBinLen: number,
246 processedBinLen: number,
247 H: StateT,
248 outputLen: number
249 ) => number[];
250 protected abstract readonly stateCloneFunc: (state: StateT) => StateT;
251 protected abstract readonly newStateFunc: (variant: VariantT) => StateT;
252 protected abstract readonly getMAC: ((options: { outputLen: number }) => number[]) | null;
253
254 protected constructor(variant: VariantT, inputFormat: "TEXT", options?: FixedLengthOptionsEncodingType);
255 protected constructor(variant: VariantT, inputFormat: FormatNoTextType, options?: FixedLengthOptionsNoEncodingType);
256 // eslint-disable-next-line @typescript-eslint/no-explicit-any
257 protected constructor(variant: any, inputFormat: any, options?: any) {
258 const inputOptions = options || {};
259 this.inputFormat = inputFormat;
260
261 this.utfType = inputOptions["encoding"] || "UTF8";
262 this.numRounds = inputOptions["numRounds"] || 1;
263
264 /* eslint-disable-next-line @typescript-eslint/ban-ts-ignore */
265 // @ts-ignore - The spec actually says ToString is called on the first parseInt argument so it's OK to use it here
266 // to check if an arugment is an integer. This cheat would break if it's used to get the value of the argument.
267 if (isNaN(this.numRounds) || this.numRounds !== parseInt(this.numRounds, 10) || 1 > this.numRounds) {
268 throw new Error("numRounds must a integer >= 1");
269 }
270
271 this.shaVariant = variant;
272 this.remainder = [];
273 this.remainderLen = 0;
274 this.updateCalled = false;
275 this.processedLen = 0;
276 this.macKeySet = false;
277 this.keyWithIPad = [];
278 this.keyWithOPad = [];
279 }
280
281 /**
282 * Hashes as many blocks as possible. Stores the rest for either a future update or getHash call.
283 *
284 * @param srcString The input to be hashed.
285 */
286 update(srcString: string | ArrayBuffer | Uint8Array): void {
287 let i,
288 updateProcessedLen = 0;
289 const variantBlockIntInc = this.variantBlockSize >>> 5,
290 convertRet = this.converterFunc(srcString, this.remainder, this.remainderLen),
291 chunkBinLen = convertRet["binLen"],
292 chunk = convertRet["value"],
293 chunkIntLen = chunkBinLen >>> 5;
294
295 for (i = 0; i < chunkIntLen; i += variantBlockIntInc) {
296 if (updateProcessedLen + this.variantBlockSize <= chunkBinLen) {
297 this.intermediateState = this.roundFunc(chunk.slice(i, i + variantBlockIntInc), this.intermediateState);
298 updateProcessedLen += this.variantBlockSize;
299 }
300 }
301 this.processedLen += updateProcessedLen;
302 this.remainder = chunk.slice(updateProcessedLen >>> 5);
303 this.remainderLen = chunkBinLen % this.variantBlockSize;
304 this.updateCalled = true;
305 }
306
307 /**
308 * Returns the desired SHA hash of the input fed in via `update` calls.
309 *
310 * @param format The desired output formatting
311 * @param options Hashmap of output formatting options. `outputLen` must be specified for variable length hashes.
312 * `outputLen` replaces the now deprecated `shakeLen` key.
313 * @returns The hash in the format specified.
314 */
315 getHash(format: "HEX", options?: { outputUpper?: boolean; outputLen?: number; shakeLen?: number }): string;
316 getHash(format: "B64", options?: { b64Pad?: string; outputLen?: number; shakeLen?: number }): string;
317 getHash(format: "BYTES", options?: { outputLen?: number; shakeLen?: number }): string;
318 getHash(format: "UINT8ARRAY", options?: { outputLen?: number; shakeLen?: number }): Uint8Array;
319 getHash(format: "ARRAYBUFFER", options?: { outputLen?: number; shakeLen?: number }): ArrayBuffer;
320 // eslint-disable-next-line @typescript-eslint/no-explicit-any
321 getHash(format: any, options?: any): any {
322 let i,
323 finalizedState,
324 outputBinLen = this.outputBinLen;
325
326 const outputOptions = getOutputOpts(options);
327
328 if (this.isVariableLen) {
329 if (outputOptions["outputLen"] === -1) {
330 throw new Error("Output length must be specified in options");
331 }
332 outputBinLen = outputOptions["outputLen"];
333 }
334
335 const formatFunc = getOutputConverter(format, outputBinLen, this.bigEndianMod, outputOptions);
336 if (this.macKeySet && this.getMAC) {
337 return formatFunc(this.getMAC(outputOptions));
338 }
339
340 finalizedState = this.finalizeFunc(
341 this.remainder.slice(),
342 this.remainderLen,
343 this.processedLen,
344 this.stateCloneFunc(this.intermediateState),
345 outputBinLen
346 );
347 for (i = 1; i < this.numRounds; i += 1) {
348 /* Need to mask out bits that should be zero due to output not being a multiple of 32 */
349 if (this.isVariableLen && outputBinLen % 32 !== 0) {
350 finalizedState[finalizedState.length - 1] &= 0x00ffffff >>> (24 - (outputBinLen % 32));
351 }
352 finalizedState = this.finalizeFunc(
353 finalizedState,
354 outputBinLen,
355 0,
356 this.newStateFunc(this.shaVariant),
357 outputBinLen
358 );
359 }
360
361 return formatFunc(finalizedState);
362 }
363
364 /**
365 * Sets the HMAC key for an eventual `getHMAC` call. Must be called immediately after jsSHA object instantiation.
366 *
367 * @param key The key used to calculate the HMAC
368 * @param inputFormat The format of key.
369 * @param options Hashmap of extra input options.
370 */
371 setHMACKey(key: string, inputFormat: "TEXT", options?: { encoding?: EncodingType }): void;
372 setHMACKey(key: string, inputFormat: "B64" | "HEX" | "BYTES"): void;
373 setHMACKey(key: ArrayBuffer, inputFormat: "ARRAYBUFFER"): void;
374 setHMACKey(key: Uint8Array, inputFormat: "UINT8ARRAY"): void;
375 // eslint-disable-next-line @typescript-eslint/no-explicit-any
376 setHMACKey(key: any, inputFormat: any, options?: any): void {
377 if (!this.HMACSupported) {
378 throw new Error("Variant does not support HMAC");
379 }
380
381 if (this.updateCalled) {
382 throw new Error("Cannot set MAC key after calling update");
383 }
384
385 const keyOptions = options || {},
386 keyConverterFunc = getStrConverter(inputFormat, keyOptions["encoding"] || "UTF8", this.bigEndianMod);
387
388 this._setHMACKey(keyConverterFunc(key));
389 }
390
391 /**
392 * Internal function that sets the MAC key.
393 *
394 * @param key The packed MAC key to use
395 */
396 protected _setHMACKey(key: packedValue): void {
397 const blockByteSize = this.variantBlockSize >>> 3,
398 lastArrayIndex = blockByteSize / 4 - 1;
399 let i;
400 if (this.numRounds !== 1) {
401 throw new Error(mac_rounds_error);
402 }
403
404 if (this.macKeySet) {
405 throw new Error("MAC key already set");
406 }
407
408 /* Figure out what to do with the key based on its size relative to
409 * the hash's block size */
410 if (blockByteSize < key["binLen"] / 8) {
411 key["value"] = this.finalizeFunc(
412 key["value"],
413 key["binLen"],
414 0,
415 this.newStateFunc(this.shaVariant),
416 this.outputBinLen
417 );
418 }
419 while (key["value"].length <= lastArrayIndex) {
420 key["value"].push(0);
421 }
422 /* Create ipad and opad */
423 for (i = 0; i <= lastArrayIndex; i += 1) {
424 this.keyWithIPad[i] = key["value"][i] ^ 0x36363636;
425 this.keyWithOPad[i] = key["value"][i] ^ 0x5c5c5c5c;
426 }
427
428 this.intermediateState = this.roundFunc(this.keyWithIPad, this.intermediateState);
429 this.processedLen = this.variantBlockSize;
430
431 this.macKeySet = true;
432 }
433
434 /**
435 * Returns the the HMAC in the specified format using the key given by a previous `setHMACKey` call.
436 *
437 * @param format The desired output formatting.
438 * @param options Hashmap of extra outputs options.
439 * @returns The HMAC in the format specified.
440 */
441 getHMAC(format: "HEX", options?: { outputUpper?: boolean }): string;
442 getHMAC(format: "B64", options?: { b64Pad?: string }): string;
443 getHMAC(format: "BYTES"): string;
444 getHMAC(format: "UINT8ARRAY"): Uint8Array;
445 getHMAC(format: "ARRAYBUFFER"): ArrayBuffer;
446 // eslint-disable-next-line @typescript-eslint/no-explicit-any
447 getHMAC(format: any, options?: any): any {
448 const outputOptions = getOutputOpts(options),
449 formatFunc = getOutputConverter(format, this.outputBinLen, this.bigEndianMod, outputOptions);
450
451 return formatFunc(this._getHMAC());
452 }
453
454 /**
455 * Internal function that returns the "raw" HMAC
456 */
457 protected _getHMAC(): number[] {
458 let finalizedState;
459
460 if (!this.macKeySet) {
461 throw new Error("Cannot call getHMAC without first setting MAC key");
462 }
463
464 const firstHash = this.finalizeFunc(
465 this.remainder.slice(),
466 this.remainderLen,
467 this.processedLen,
468 this.stateCloneFunc(this.intermediateState),
469 this.outputBinLen
470 );
471 finalizedState = this.roundFunc(this.keyWithOPad, this.newStateFunc(this.shaVariant));
472 finalizedState = this.finalizeFunc(
473 firstHash,
474 this.outputBinLen,
475 this.variantBlockSize,
476 finalizedState,
477 this.outputBinLen
478 );
479
480 return finalizedState;
481 }
482}
483
\No newline at end of file