mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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. Also, update the unit tests to dynamically verify the custom SHA-256 output against the native Web Crypto API.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dist/browser/main-[hash].js": 227093,
|
||||
"dist/browser/polyfills-[hash].js": 34544,
|
||||
"dist/browser/main-[hash].js": 233303,
|
||||
"dist/browser/polyfills-[hash].js": 35677,
|
||||
"dist/browser/event-dispatch-contract.min.js": 476
|
||||
}
|
||||
|
||||
@@ -103,13 +103,13 @@ interface TransferHttpResponse {
|
||||
/** headers */
|
||||
[HEADERS]: Record<string, string[]>;
|
||||
/** status */
|
||||
[STATUS]?: number;
|
||||
[STATUS]: number;
|
||||
/** statusText */
|
||||
[STATUS_TEXT]?: string;
|
||||
[STATUS_TEXT]: string;
|
||||
/** url */
|
||||
[REQ_URL]?: string;
|
||||
[REQ_URL]: string;
|
||||
/** responseType */
|
||||
[RESPONSE_TYPE]?: HttpRequest<unknown>['responseType'];
|
||||
[RESPONSE_TYPE]: HttpRequest<unknown>['responseType'];
|
||||
}
|
||||
|
||||
interface CacheOptions extends HttpTransferCacheOptions {
|
||||
@@ -125,7 +125,7 @@ export const CACHE_OPTIONS = new InjectionToken<CacheOptions>(
|
||||
*/
|
||||
const ALLOWED_METHODS = ['GET', 'HEAD'];
|
||||
|
||||
function shouldCacheRequest(req: HttpRequest<unknown>, options: CacheOptions): boolean {
|
||||
function canUseOrCacheRequest(req: HttpRequest<unknown>, options: CacheOptions): boolean {
|
||||
const {isCacheActive, ...globalOptions} = options;
|
||||
const {transferCache: requestOptions, method: requestMethod} = req;
|
||||
|
||||
@@ -155,25 +155,30 @@ function getHeadersToInclude(
|
||||
options: CacheOptions,
|
||||
requestOptions: HttpTransferCacheOptions | boolean | undefined,
|
||||
): string[] | undefined {
|
||||
const {includeHeaders: globalHeaders} = options;
|
||||
let headersToInclude = globalHeaders;
|
||||
if (typeof requestOptions === 'object' && requestOptions.includeHeaders) {
|
||||
// Request-specific config takes precedence over the global config.
|
||||
headersToInclude = requestOptions.includeHeaders;
|
||||
}
|
||||
return headersToInclude;
|
||||
// Request-specific config takes precedence over the global config.
|
||||
return typeof requestOptions === 'object' && requestOptions.includeHeaders
|
||||
? requestOptions.includeHeaders
|
||||
: options.includeHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cached response for a given request.
|
||||
* @param req The request to retrieve the cached response for.
|
||||
* @param options The caching options.
|
||||
* @param transferState The transfer state to retrieve the cached response from.
|
||||
* @param originMap The origin map to map the request URL to the origin. (Not needed when `storeKey` is provided).
|
||||
* @param storeKey The key to use to store the cached response in the transfer state. (If not provided, it will be computed from the request and originMap).
|
||||
* @param skipUseCacheChecks Whether to skip the use cache checks. (Only disable when the checks have been performed beforehand).
|
||||
*/
|
||||
export function retrieveStateFromCache(
|
||||
req: HttpRequest<unknown>,
|
||||
options: CacheOptions,
|
||||
transferState: TransferState,
|
||||
originMap: Record<string, string> | null,
|
||||
storeKey?: StateKey<TransferHttpResponse>,
|
||||
skipUseCacheChecks = false,
|
||||
): HttpResponse<unknown> | null {
|
||||
const {transferCache: requestOptions} = req;
|
||||
|
||||
// In the following situations we do not want to cache the request
|
||||
if (!shouldCacheRequest(req, options)) {
|
||||
if (!skipUseCacheChecks && !canUseOrCacheRequest(req, options)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -187,58 +192,61 @@ export function retrieveStateFromCache(
|
||||
);
|
||||
}
|
||||
|
||||
const requestUrl =
|
||||
typeof ngServerMode !== 'undefined' && ngServerMode && originMap
|
||||
? mapRequestOriginUrl(req.url, originMap)
|
||||
: req.url;
|
||||
if (!storeKey) {
|
||||
const requestUrl =
|
||||
typeof ngServerMode !== 'undefined' && ngServerMode && originMap
|
||||
? mapRequestOriginUrl(req.url, originMap)
|
||||
: req.url;
|
||||
|
||||
const storeKey = makeCacheKey(req, requestUrl);
|
||||
const response = transferState.get(storeKey, null);
|
||||
|
||||
const headersToInclude = getHeadersToInclude(options, requestOptions);
|
||||
|
||||
if (response) {
|
||||
const {
|
||||
[BODY]: undecodedBody,
|
||||
[RESPONSE_TYPE]: responseType,
|
||||
[HEADERS]: httpHeaders,
|
||||
[STATUS]: status,
|
||||
[STATUS_TEXT]: statusText,
|
||||
[REQ_URL]: url,
|
||||
} = response;
|
||||
// Request found in cache. Respond using it.
|
||||
let body: ArrayBuffer | Blob | string | undefined = undecodedBody;
|
||||
|
||||
switch (responseType) {
|
||||
case 'arraybuffer':
|
||||
body = fromBase64(undecodedBody);
|
||||
break;
|
||||
case 'blob':
|
||||
body = new Blob([fromBase64(undecodedBody)]);
|
||||
break;
|
||||
}
|
||||
|
||||
// We want to warn users accessing a header provided from the cache
|
||||
// That HttpTransferCache alters the headers
|
||||
// The warning will be logged a single time by HttpHeaders instance
|
||||
let headers = new HttpHeaders(httpHeaders);
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
// Append extra logic in dev mode to produce a warning when a header
|
||||
// that was not transferred to the client is accessed in the code via `get`
|
||||
// and `has` calls.
|
||||
headers = appendMissingHeadersDetection(req.url, headers, headersToInclude ?? []);
|
||||
}
|
||||
|
||||
return new HttpResponse({
|
||||
body,
|
||||
headers,
|
||||
status,
|
||||
statusText,
|
||||
url,
|
||||
});
|
||||
storeKey = makeCacheKey(req, requestUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
const response = transferState.get(storeKey, null);
|
||||
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
[BODY]: undecodedBody,
|
||||
[RESPONSE_TYPE]: responseType,
|
||||
[HEADERS]: httpHeaders,
|
||||
[STATUS]: status,
|
||||
[STATUS_TEXT]: statusText,
|
||||
[REQ_URL]: url,
|
||||
} = response;
|
||||
// Request found in cache. Respond using it.
|
||||
let body: ArrayBuffer | Blob | string | undefined = undecodedBody;
|
||||
|
||||
switch (responseType) {
|
||||
case 'arraybuffer':
|
||||
body = fromBase64(undecodedBody);
|
||||
break;
|
||||
case 'blob':
|
||||
body = new Blob([fromBase64(undecodedBody)]);
|
||||
break;
|
||||
}
|
||||
|
||||
// We want to warn users accessing a header provided from the cache
|
||||
// That HttpTransferCache alters the headers
|
||||
// The warning will be logged a single time by HttpHeaders instance
|
||||
let headers = new HttpHeaders(httpHeaders);
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
// Append extra logic in dev mode to produce a warning when a header
|
||||
// that was not transferred to the client is accessed in the code via `get`
|
||||
// and `has` calls.
|
||||
const {transferCache: requestOptions} = req;
|
||||
const headersToInclude = getHeadersToInclude(options, requestOptions);
|
||||
headers = appendMissingHeadersDetection(req.url, headers, headersToInclude ?? []);
|
||||
}
|
||||
|
||||
return new HttpResponse({
|
||||
body,
|
||||
headers,
|
||||
status,
|
||||
statusText,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
export function transferCacheInterceptorFn(
|
||||
@@ -246,48 +254,56 @@ export function transferCacheInterceptorFn(
|
||||
next: HttpHandlerFn,
|
||||
): Observable<HttpEvent<unknown>> {
|
||||
const options = inject(CACHE_OPTIONS);
|
||||
const transferState = inject(TransferState);
|
||||
|
||||
const originMap = inject(HTTP_TRANSFER_CACHE_ORIGIN_MAP, {optional: true});
|
||||
|
||||
const cachedResponse = retrieveStateFromCache(req, options, transferState, originMap);
|
||||
if (cachedResponse) {
|
||||
return of(cachedResponse);
|
||||
if (!canUseOrCacheRequest(req, options)) {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
const {transferCache: requestOptions} = req;
|
||||
const headersToInclude = getHeadersToInclude(options, requestOptions);
|
||||
|
||||
const transferState = inject(TransferState);
|
||||
const originMap = inject(HTTP_TRANSFER_CACHE_ORIGIN_MAP, {optional: true});
|
||||
const requestUrl =
|
||||
typeof ngServerMode !== 'undefined' && ngServerMode && originMap
|
||||
? mapRequestOriginUrl(req.url, originMap)
|
||||
: req.url;
|
||||
const storeKey = makeCacheKey(req, requestUrl);
|
||||
|
||||
// In the following situations we do not want to cache the request
|
||||
if (!shouldCacheRequest(req, options)) {
|
||||
return next(req);
|
||||
const cachedResponse = retrieveStateFromCache(
|
||||
req,
|
||||
options,
|
||||
transferState,
|
||||
/** originMap */ null,
|
||||
storeKey,
|
||||
/** skipUseCacheChecks */ true,
|
||||
);
|
||||
|
||||
if (cachedResponse) {
|
||||
return of(cachedResponse);
|
||||
}
|
||||
|
||||
const event$ = next(req);
|
||||
|
||||
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
|
||||
// Request not found in cache. Make the request and cache it if on the server.
|
||||
return event$.pipe(
|
||||
tap((event: HttpEvent<unknown>) => {
|
||||
// Only cache successful HTTP responses that do not have Cache-Control
|
||||
// directives that forbid shared caching (no-store or private).
|
||||
if (event instanceof HttpResponse && !hasUncacheableCacheControl(event.headers)) {
|
||||
if (event instanceof HttpResponse) {
|
||||
const {headers, body, status, statusText} = event;
|
||||
|
||||
// Only cache successful HTTP responses that do not have Cache-Control
|
||||
// directives that forbid shared caching (no-store or private).
|
||||
if (hasUncacheableCacheControl(headers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {transferCache: requestOptions, responseType} = req;
|
||||
const headersToInclude = getHeadersToInclude(options, requestOptions);
|
||||
|
||||
transferState.set<TransferHttpResponse>(storeKey, {
|
||||
[BODY]:
|
||||
req.responseType === 'arraybuffer' || req.responseType === 'blob'
|
||||
? toBase64(event.body)
|
||||
: event.body,
|
||||
[HEADERS]: getFilteredHeaders(event.headers, headersToInclude),
|
||||
[STATUS]: event.status,
|
||||
[STATUS_TEXT]: event.statusText,
|
||||
responseType === 'arraybuffer' || responseType === 'blob' ? toBase64(body) : body,
|
||||
[HEADERS]: getFilteredHeaders(headers, headersToInclude),
|
||||
[STATUS]: status,
|
||||
[STATUS_TEXT]: statusText,
|
||||
[REQ_URL]: requestUrl,
|
||||
[RESPONSE_TYPE]: req.responseType,
|
||||
[RESPONSE_TYPE]: responseType,
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -299,15 +315,17 @@ export function transferCacheInterceptorFn(
|
||||
|
||||
/** @returns true when the request contains authentication or cookie headers. */
|
||||
function hasAuthHeaders(req: HttpRequest<unknown>): boolean {
|
||||
const headers = req.headers;
|
||||
|
||||
return (
|
||||
req.headers.has('authorization') ||
|
||||
req.headers.has('proxy-authorization') ||
|
||||
req.headers.has('cookie')
|
||||
headers.has('authorization') || headers.has('proxy-authorization') || headers.has('cookie')
|
||||
);
|
||||
}
|
||||
|
||||
function hasOutgoingCredentials(req: HttpRequest<unknown>): boolean {
|
||||
return req.withCredentials || req.credentials === 'include' || req.credentials === 'same-origin';
|
||||
const {withCredentials, credentials} = req;
|
||||
|
||||
return withCredentials || credentials === 'include' || credentials === 'same-origin';
|
||||
}
|
||||
|
||||
const UNCACHEABLE_CACHE_CONTROL_DIRECTIVES = new Set(['no-store', 'private', 'no-cache']);
|
||||
@@ -377,26 +395,6 @@ function makeCacheKey(
|
||||
return makeStateKey(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that returns a hash representation of a string using a variant of DJB2 hash
|
||||
* algorithm.
|
||||
*
|
||||
* This is the same hashing logic that is used to generate component ids.
|
||||
*/
|
||||
function generateHash(value: string): string {
|
||||
let hash = 0;
|
||||
|
||||
for (const char of value) {
|
||||
hash = (Math.imul(31, hash) + char.charCodeAt(0)) << 0;
|
||||
}
|
||||
|
||||
// Force positive number hash.
|
||||
// 2147483647 = equivalent of Integer.MAX_VALUE.
|
||||
hash += 2147483647 + 1;
|
||||
|
||||
return hash.toString();
|
||||
}
|
||||
|
||||
function toBase64(buffer: unknown): string {
|
||||
//TODO: replace with when is Baseline widely available
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64
|
||||
@@ -534,3 +532,163 @@ function verifyMappedOrigin(url: string): void {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-256 Constants (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311):
|
||||
*/
|
||||
const SHA256_ROUND_CONSTANTS = /* @__PURE__ */ new Uint32Array([
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||
]);
|
||||
|
||||
let textEncoder: TextEncoder | undefined;
|
||||
|
||||
/**
|
||||
* Generates a SHA-256 hash representation of a string.
|
||||
*
|
||||
* Note: A custom synchronous SHA-256 implementation is used here because the Web Crypto API
|
||||
* (`crypto.subtle.digest`) is strictly asynchronous (Promise-based), whereas the transfer cache
|
||||
* state lookup and interceptor flow must operate synchronously due to the HttpResource API.
|
||||
*
|
||||
* The previous DJB2 hashing logic was 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.
|
||||
*/
|
||||
export function generateHash(value: string): string {
|
||||
textEncoder ??= new TextEncoder();
|
||||
const inputBytes = textEncoder.encode(value);
|
||||
|
||||
// Initial hash values (first 32 bits of the fractional parts of the square roots of the first 8 primes 2..19):
|
||||
let hashState0 = 0x6a09e667;
|
||||
let hashState1 = 0xbb67ae85;
|
||||
let hashState2 = 0x3c6ef372;
|
||||
let hashState3 = 0xa54ff53a;
|
||||
let hashState4 = 0x510e527f;
|
||||
let hashState5 = 0x9b05688c;
|
||||
let hashState6 = 0x1f83d9ab;
|
||||
let hashState7 = 0x5be0cd19;
|
||||
|
||||
// Pre-processing (Padding):
|
||||
const messageLengthInBits = inputBytes.length * 8;
|
||||
|
||||
// The total length of the padded message must be a multiple of 64 bytes (512 bits)
|
||||
const paddedLengthInBytes = (((inputBytes.length + 8) >> 6) + 1) << 6;
|
||||
const paddedBytes = new Uint8Array(paddedLengthInBytes);
|
||||
paddedBytes.set(inputBytes);
|
||||
paddedBytes[inputBytes.length] = 0x80; // Append a single '1' bit (0x80 byte)
|
||||
|
||||
const paddedBytesView = new DataView(paddedBytes.buffer);
|
||||
const lowBits = messageLengthInBits >>> 0;
|
||||
const highBits = (messageLengthInBits / 0x100000000) >>> 0;
|
||||
paddedBytesView.setUint32(paddedLengthInBytes - 8, highBits, false);
|
||||
paddedBytesView.setUint32(paddedLengthInBytes - 4, lowBits, false);
|
||||
|
||||
// Process the message in successive 64-byte chunks:
|
||||
const messageSchedule = new Uint32Array(64);
|
||||
for (let chunkOffset = 0; chunkOffset < paddedLengthInBytes; chunkOffset += 64) {
|
||||
// Initialize first 16 words of the message schedule:
|
||||
for (let i = 0; i < 16; i++) {
|
||||
messageSchedule[i] = paddedBytesView.getUint32(chunkOffset + i * 4, false);
|
||||
}
|
||||
|
||||
// Extend to 64 words:
|
||||
for (let i = 16; i < 64; i++) {
|
||||
const prevWord15 = messageSchedule[i - 15];
|
||||
const sigma0 =
|
||||
(((prevWord15 >>> 7) | (prevWord15 << 25)) ^
|
||||
((prevWord15 >>> 18) | (prevWord15 << 14)) ^
|
||||
(prevWord15 >>> 3)) >>>
|
||||
0;
|
||||
|
||||
const prevWord2 = messageSchedule[i - 2];
|
||||
const sigma1 =
|
||||
(((prevWord2 >>> 17) | (prevWord2 << 15)) ^
|
||||
((prevWord2 >>> 19) | (prevWord2 << 13)) ^
|
||||
(prevWord2 >>> 10)) >>>
|
||||
0;
|
||||
|
||||
messageSchedule[i] =
|
||||
(messageSchedule[i - 16] + sigma0 + messageSchedule[i - 7] + sigma1) >>> 0;
|
||||
}
|
||||
|
||||
// Initialize working variables to current hash values:
|
||||
let workingStateA = hashState0;
|
||||
let workingStateB = hashState1;
|
||||
let workingStateC = hashState2;
|
||||
let workingStateD = hashState3;
|
||||
let workingStateE = hashState4;
|
||||
let workingStateF = hashState5;
|
||||
let workingStateG = hashState6;
|
||||
let workingStateH = hashState7;
|
||||
|
||||
// Compression function main loop:
|
||||
for (let i = 0; i < 64; i++) {
|
||||
const capitalSigma1 =
|
||||
(((workingStateE >>> 6) | (workingStateE << 26)) ^
|
||||
((workingStateE >>> 11) | (workingStateE << 21)) ^
|
||||
((workingStateE >>> 25) | (workingStateE << 7))) >>>
|
||||
0;
|
||||
const chFunction = ((workingStateE & workingStateF) ^ (~workingStateE & workingStateG)) >>> 0;
|
||||
const temp1 =
|
||||
(workingStateH +
|
||||
capitalSigma1 +
|
||||
chFunction +
|
||||
SHA256_ROUND_CONSTANTS[i] +
|
||||
messageSchedule[i]) >>>
|
||||
0;
|
||||
|
||||
const capitalSigma0 =
|
||||
(((workingStateA >>> 2) | (workingStateA << 30)) ^
|
||||
((workingStateA >>> 13) | (workingStateA << 19)) ^
|
||||
((workingStateA >>> 22) | (workingStateA << 10))) >>>
|
||||
0;
|
||||
const majFunction =
|
||||
((workingStateA & workingStateB) ^
|
||||
(workingStateA & workingStateC) ^
|
||||
(workingStateB & workingStateC)) >>>
|
||||
0;
|
||||
const temp2 = (capitalSigma0 + majFunction) >>> 0;
|
||||
|
||||
workingStateH = workingStateG;
|
||||
workingStateG = workingStateF;
|
||||
workingStateF = workingStateE;
|
||||
workingStateE = (workingStateD + temp1) >>> 0;
|
||||
workingStateD = workingStateC;
|
||||
workingStateC = workingStateB;
|
||||
workingStateB = workingStateA;
|
||||
workingStateA = (temp1 + temp2) >>> 0;
|
||||
}
|
||||
|
||||
// Update intermediate hash state:
|
||||
hashState0 = (hashState0 + workingStateA) >>> 0;
|
||||
hashState1 = (hashState1 + workingStateB) >>> 0;
|
||||
hashState2 = (hashState2 + workingStateC) >>> 0;
|
||||
hashState3 = (hashState3 + workingStateD) >>> 0;
|
||||
hashState4 = (hashState4 + workingStateE) >>> 0;
|
||||
hashState5 = (hashState5 + workingStateF) >>> 0;
|
||||
hashState6 = (hashState6 + workingStateG) >>> 0;
|
||||
hashState7 = (hashState7 + workingStateH) >>> 0;
|
||||
}
|
||||
|
||||
// Produce the final 64-character hexadecimal hash:
|
||||
return [
|
||||
hashState0,
|
||||
hashState1,
|
||||
hashState2,
|
||||
hashState3,
|
||||
hashState4,
|
||||
hashState5,
|
||||
hashState6,
|
||||
hashState7,
|
||||
]
|
||||
.map((x) => x.toString(16).padStart(8, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
REQ_URL,
|
||||
transferCacheInterceptorFn,
|
||||
withHttpTransferCache,
|
||||
generateHash,
|
||||
} from '../src/transfer_cache';
|
||||
import {HttpTestingController, provideHttpClientTesting} from '../testing';
|
||||
import {PLATFORM_BROWSER_ID, PLATFORM_SERVER_ID} from '../../src/platform_id';
|
||||
@@ -385,7 +386,7 @@ describe('TransferCache', () => {
|
||||
|
||||
const transferState = TestBed.inject(TransferState);
|
||||
expect(JSON.parse(transferState.toJson()) as Record<string, unknown>).toEqual({
|
||||
'2400571479': {
|
||||
'2da5dfaf112523258ec9c26a0abe9a093b59ed7dbe5f43e4b5ee25a407ac9cf0': {
|
||||
[BODY]: 'foo',
|
||||
[HEADERS]: {},
|
||||
[STATUS]: 200,
|
||||
@@ -393,7 +394,7 @@ describe('TransferCache', () => {
|
||||
[REQ_URL]: '/test-1',
|
||||
[RESPONSE_TYPE]: 'json',
|
||||
},
|
||||
'2400572440': {
|
||||
'869485290d9385f3c0a9ba571918c335bbca9e03373bf8260d02f2b7dd335849': {
|
||||
[BODY]: 'buzz',
|
||||
[HEADERS]: {},
|
||||
[STATUS]: 200,
|
||||
@@ -1065,4 +1066,34 @@ describe('TransferCache', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateHash', () => {
|
||||
async function computeNativeSha256(value: string): Promise<string> {
|
||||
const msgUint8 = new TextEncoder().encode(value);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
it('should generate standard SHA-256 hashes matching Web Crypto specs', async () => {
|
||||
const testCases = [
|
||||
'',
|
||||
'hello',
|
||||
'angular',
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
|
||||
'Angular 🚀',
|
||||
'a'.repeat(55),
|
||||
'a'.repeat(56),
|
||||
'a'.repeat(63),
|
||||
'a'.repeat(64),
|
||||
'a'.repeat(65),
|
||||
'a'.repeat(1000),
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const expected = await computeNativeSha256(testCase);
|
||||
expect(generateHash(testCase)).withContext(`For: ${testCase}`).toBe(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
"RuntimeError",
|
||||
"SCHEDULE_IN_ROOT_ZONE",
|
||||
"SCHEDULE_IN_ROOT_ZONE_DEFAULT",
|
||||
"SHA256_ROUND_CONSTANTS",
|
||||
"SIGNAL",
|
||||
"SIMPLE_CHANGES_STORE",
|
||||
"SKIP_HYDRATION_ATTR_NAME",
|
||||
@@ -325,6 +326,7 @@
|
||||
"callHookInternal",
|
||||
"callHooks",
|
||||
"canHydrateNode",
|
||||
"canUseOrCacheRequest",
|
||||
"cancelLeavingNodes",
|
||||
"captureError",
|
||||
"checkStable",
|
||||
@@ -474,7 +476,6 @@
|
||||
"getFactoryDef",
|
||||
"getFirstLContainer",
|
||||
"getGlobalLocale",
|
||||
"getHeadersToInclude",
|
||||
"getInheritedInjectableDef",
|
||||
"getInitialLViewFlagsFromDef",
|
||||
"getInjectFlag",
|
||||
@@ -774,7 +775,6 @@
|
||||
"shimHostAttribute",
|
||||
"shimStylesContent",
|
||||
"shouldBeIgnoredByZone",
|
||||
"shouldCacheRequest",
|
||||
"shouldSearchParent",
|
||||
"siblingAfter",
|
||||
"skipTextNodes",
|
||||
@@ -784,6 +784,7 @@
|
||||
"stringifyCSSSelectorList",
|
||||
"subscribeOn",
|
||||
"syncViewWithBlueprint",
|
||||
"textEncoder",
|
||||
"throwInvalidWriteToSignalErrorFn",
|
||||
"throwProviderNotFoundError",
|
||||
"timeoutProvider",
|
||||
@@ -811,4 +812,4 @@
|
||||
],
|
||||
"lazy": []
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user