Skip to content

Commit 34f49d0

Browse files
committed
fix(common): use cryptographically secure SHA-256 for transfer cache key generation
Replace the custom 64-bit non-cryptographic combined DJB2 hashing implementation in HttpTransferCache with a robust, pure JavaScript, synchronous SHA-256 algorithm. Using DJB2 is vulnerable to pre-image and second-preimage attacks due to its small 64-bit keyspace and mathematical simplicity. An attacker could craft colliding request inputs to poison the cache, potentially causing a CDN or the application to serve the wrong cached response to legitimate users. SHA-256 provides strong cryptographic collision resistance, preventing cache key collision attacks. A custom synchronous implementation is required because: - The Web Crypto API (`crypto.subtle.digest`) is asynchronous, whereas the transfer cache state lookup and interceptor flow must operate synchronously. - Node's native `crypto` module is not available in browser environments. Also, update the unit tests to dynamically verify the custom SHA-256 output against the native Web Crypto API.
1 parent c7e2284 commit 34f49d0

2 files changed

Lines changed: 175 additions & 19 deletions

File tree

packages/common/http/src/transfer_cache.ts

Lines changed: 142 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -369,27 +369,152 @@ function makeCacheKey(
369369
}
370370

371371
/**
372-
* Generates a 64-bit hash representation of a string by combining two independent 32-bit
373-
* DJB2 hashes (one with multiplier 31, one with multiplier 33).
372+
* Generates a SHA-256 hash representation of a string.
374373
*
375-
* Using a 64-bit keyspace virtually eliminates the probability of any accidental transfer
376-
* cache key collisions.
374+
* Note: A custom synchronous SHA-256 implementation is used here because
375+
* Web Crypto API (`crypto.subtle.digest`) is strictly asynchronous (Promise-based),
376+
* whereas the transfer cache state lookup and interceptor flow must operate synchronously due to the HttpResource API.
377377
*/
378-
function generateHash(value: string): string {
379-
const padding = '00000000';
380-
let hash1 = 0;
381-
let hash2 = 5381;
382-
383-
for (let i = 0; i < value.length; i++) {
384-
const charCode = value.charCodeAt(i);
385-
hash1 = (Math.imul(31, hash1) + charCode) << 0;
386-
hash2 = (Math.imul(33, hash2) + charCode) << 0;
387-
}
378+
export function generateHash(value: string): string {
379+
const inputBytes = new TextEncoder().encode(value);
380+
381+
// Initial hash values (first 32 bits of the fractional parts of the square roots of the first 8 primes 2..19):
382+
let hashState0 = 0x6a09e667;
383+
let hashState1 = 0xbb67ae85;
384+
let hashState2 = 0x3c6ef372;
385+
let hashState3 = 0xa54ff53a;
386+
let hashState4 = 0x510e527f;
387+
let hashState5 = 0x9b05688c;
388+
let hashState6 = 0x1f83d9ab;
389+
let hashState7 = 0x5be0cd19;
390+
391+
// SHA-256 Constants (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311):
392+
const SHA256_ROUND_CONSTANTS = [
393+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
394+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
395+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
396+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
397+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
398+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
399+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
400+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
401+
];
402+
403+
// Pre-processing (Padding):
404+
const messageLengthInBits = inputBytes.length * 8;
405+
406+
// The total length of the padded message must be a multiple of 64 bytes (512 bits)
407+
const paddedLengthInBytes = (((inputBytes.length + 8) >> 6) + 1) << 6;
408+
const paddedBytes = new Uint8Array(paddedLengthInBytes);
409+
paddedBytes.set(inputBytes);
410+
paddedBytes[inputBytes.length] = 0x80; // Append a single '1' bit (0x80 byte)
411+
412+
const paddedBytesView = new DataView(paddedBytes.buffer);
413+
const lowBits = messageLengthInBits >>> 0;
414+
const highBits = (messageLengthInBits / 0x100000000) >>> 0;
415+
paddedBytesView.setUint32(paddedLengthInBytes - 8, highBits, false);
416+
paddedBytesView.setUint32(paddedLengthInBytes - 4, lowBits, false);
417+
418+
// Process the message in successive 64-byte chunks:
419+
const messageSchedule = new Uint32Array(64);
420+
for (let chunkOffset = 0; chunkOffset < paddedLengthInBytes; chunkOffset += 64) {
421+
// Initialize first 16 words of the message schedule:
422+
for (let i = 0; i < 16; i++) {
423+
messageSchedule[i] = paddedBytesView.getUint32(chunkOffset + i * 4, false);
424+
}
425+
426+
// Extend to 64 words:
427+
for (let i = 16; i < 64; i++) {
428+
const prevWord15 = messageSchedule[i - 15];
429+
const sigma0 =
430+
(((prevWord15 >>> 7) | (prevWord15 << 25)) ^
431+
((prevWord15 >>> 18) | (prevWord15 << 14)) ^
432+
(prevWord15 >>> 3)) >>>
433+
0;
434+
435+
const prevWord2 = messageSchedule[i - 2];
436+
const sigma1 =
437+
(((prevWord2 >>> 17) | (prevWord2 << 15)) ^
438+
((prevWord2 >>> 19) | (prevWord2 << 13)) ^
439+
(prevWord2 >>> 10)) >>>
440+
0;
441+
442+
messageSchedule[i] =
443+
(messageSchedule[i - 16] + sigma0 + messageSchedule[i - 7] + sigma1) >>> 0;
444+
}
388445

389-
const hex1 = (padding + (hash1 >>> 0).toString(16)).slice(-8);
390-
const hex2 = (padding + (hash2 >>> 0).toString(16)).slice(-8);
446+
// Initialize working variables to current hash values:
447+
let workingStateA = hashState0;
448+
let workingStateB = hashState1;
449+
let workingStateC = hashState2;
450+
let workingStateD = hashState3;
451+
let workingStateE = hashState4;
452+
let workingStateF = hashState5;
453+
let workingStateG = hashState6;
454+
let workingStateH = hashState7;
455+
456+
// Compression function main loop:
457+
for (let i = 0; i < 64; i++) {
458+
const capitalSigma1 =
459+
(((workingStateE >>> 6) | (workingStateE << 26)) ^
460+
((workingStateE >>> 11) | (workingStateE << 21)) ^
461+
((workingStateE >>> 25) | (workingStateE << 7))) >>>
462+
0;
463+
const chFunction = ((workingStateE & workingStateF) ^ (~workingStateE & workingStateG)) >>> 0;
464+
const temp1 =
465+
(workingStateH +
466+
capitalSigma1 +
467+
chFunction +
468+
SHA256_ROUND_CONSTANTS[i] +
469+
messageSchedule[i]) >>>
470+
0;
471+
472+
const capitalSigma0 =
473+
(((workingStateA >>> 2) | (workingStateA << 30)) ^
474+
((workingStateA >>> 13) | (workingStateA << 19)) ^
475+
((workingStateA >>> 22) | (workingStateA << 10))) >>>
476+
0;
477+
const majFunction =
478+
((workingStateA & workingStateB) ^
479+
(workingStateA & workingStateC) ^
480+
(workingStateB & workingStateC)) >>>
481+
0;
482+
const temp2 = (capitalSigma0 + majFunction) >>> 0;
483+
484+
workingStateH = workingStateG;
485+
workingStateG = workingStateF;
486+
workingStateF = workingStateE;
487+
workingStateE = (workingStateD + temp1) >>> 0;
488+
workingStateD = workingStateC;
489+
workingStateC = workingStateB;
490+
workingStateB = workingStateA;
491+
workingStateA = (temp1 + temp2) >>> 0;
492+
}
391493

392-
return hex1 + hex2;
494+
// Update intermediate hash state:
495+
hashState0 = (hashState0 + workingStateA) >>> 0;
496+
hashState1 = (hashState1 + workingStateB) >>> 0;
497+
hashState2 = (hashState2 + workingStateC) >>> 0;
498+
hashState3 = (hashState3 + workingStateD) >>> 0;
499+
hashState4 = (hashState4 + workingStateE) >>> 0;
500+
hashState5 = (hashState5 + workingStateF) >>> 0;
501+
hashState6 = (hashState6 + workingStateG) >>> 0;
502+
hashState7 = (hashState7 + workingStateH) >>> 0;
503+
}
504+
505+
// Produce the final 64-character hexadecimal hash:
506+
return [
507+
hashState0,
508+
hashState1,
509+
hashState2,
510+
hashState3,
511+
hashState4,
512+
hashState5,
513+
hashState6,
514+
hashState7,
515+
]
516+
.map((x) => x.toString(16).padStart(8, '0'))
517+
.join('');
393518
}
394519

395520
function toBase64(buffer: unknown): string {

packages/common/http/test/transfer_cache_spec.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
REQ_URL,
3232
transferCacheInterceptorFn,
3333
withHttpTransferCache,
34+
generateHash,
3435
} from '../src/transfer_cache';
3536
import {HttpTestingController, provideHttpClientTesting} from '../testing';
3637
import {PLATFORM_BROWSER_ID, PLATFORM_SERVER_ID} from '../../src/platform_id';
@@ -385,15 +386,15 @@ describe('TransferCache', () => {
385386

386387
const transferState = TestBed.inject(TransferState);
387388
expect(JSON.parse(transferState.toJson()) as Record<string, unknown>).toEqual({
388-
'0f15d057a4462f9c': {
389+
'2da5dfaf112523258ec9c26a0abe9a093b59ed7dbe5f43e4b5ee25a407ac9cf0': {
389390
[BODY]: 'foo',
390391
[HEADERS]: {},
391392
[STATUS]: 200,
392393
[STATUS_TEXT]: 'OK',
393394
[REQ_URL]: '/test-1',
394395
[RESPONSE_TYPE]: 'json',
395396
},
396-
'0f15d418a44633dd': {
397+
'869485290d9385f3c0a9ba571918c335bbca9e03373bf8260d02f2b7dd335849': {
397398
[BODY]: 'buzz',
398399
[HEADERS]: {},
399400
[STATUS]: 200,
@@ -1065,4 +1066,34 @@ describe('TransferCache', () => {
10651066
});
10661067
});
10671068
});
1069+
1070+
describe('generateHash', () => {
1071+
async function computeNativeSha256(value: string): Promise<string> {
1072+
const msgUint8 = new TextEncoder().encode(value);
1073+
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
1074+
const hashArray = Array.from(new Uint8Array(hashBuffer));
1075+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
1076+
}
1077+
1078+
it('should generate standard SHA-256 hashes matching Web Crypto specs', async () => {
1079+
const testCases = [
1080+
'',
1081+
'hello',
1082+
'angular',
1083+
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
1084+
'Angular 🚀',
1085+
'a'.repeat(55),
1086+
'a'.repeat(56),
1087+
'a'.repeat(63),
1088+
'a'.repeat(64),
1089+
'a'.repeat(65),
1090+
'a'.repeat(1000),
1091+
];
1092+
1093+
for (const testCase of testCases) {
1094+
const expected = await computeNativeSha256(testCase);
1095+
expect(generateHash(testCase)).withContext(`For: ${testCase}`).toBe(expected);
1096+
}
1097+
});
1098+
});
10681099
});

0 commit comments

Comments
 (0)