From 76a56adf72bde721bd2da22fc25d1730ab751e3a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 15 Nov 2022 11:56:29 +0100 Subject: [PATCH] fix: CDK does not work in FIPS-restricted environments (#22878) In an environment where node is compiled with FIPS restrictions in mind, `crypto.createHash('md5')` does not work, for fear of MD5 being used for crypto purposes. We do not use it for crypto purposes, just to come up with unique identifiers for certain constructs. Nevertheless, CDK cannot work if `md5` is not available from the Node standard library. Fall back to a pure JavaScript implementation if the built-in MD5 hash does not work. This results in about a ~10x performance penalty, but the only thing we can do in order to produce the same templates. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 6 +- packages/@aws-cdk/aws-ec2/lib/instance.ts | 8 +- packages/@aws-cdk/aws-ec2/lib/volume.ts | 7 +- packages/@aws-cdk/aws-glue/lib/code.ts | 6 +- .../aws-lambda-event-sources/lib/kafka.ts | 6 +- .../@aws-cdk/aws-lambda/lib/function-hash.ts | 10 +- .../@aws-cdk/aws-rds/lib/database-secret.ts | 7 +- .../lib/website-redirect.ts | 4 +- .../lib/vpc-endpoint-service-domain-name.ts | 6 +- .../aws-sagemaker/lib/private/util.ts | 6 +- .../core/lib/helpers-internal/index.ts | 2 + packages/@aws-cdk/core/lib/private/md5.ts | 200 ++++++++++++++++++ .../core/lib/private/unique-resource-name.ts | 5 +- .../@aws-cdk/core/lib/private/uniqueid.ts | 4 +- .../@aws-cdk/core/test/private/md5.test.ts | 43 ++++ .../test/private/unique-resource-name.test.ts | 4 +- .../lib/assertions/private/hash.ts | 6 +- .../cdk-build-tools/config/eslintrc.js | 10 + 18 files changed, 287 insertions(+), 53 deletions(-) create mode 100644 packages/@aws-cdk/core/lib/private/md5.ts create mode 100644 packages/@aws-cdk/core/test/private/md5.test.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index f62a58f4f59be..b53cd45f27e23 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -1,5 +1,5 @@ -import * as crypto from 'crypto'; import { Lazy, RemovalPolicy, Resource, CfnResource } from '@aws-cdk/core'; +import { md5hash } from '@aws-cdk/core/lib/helpers-internal'; import { Construct } from 'constructs'; import { CfnDeployment } from './apigateway.generated'; import { Method } from './method'; @@ -173,9 +173,7 @@ class LatestDeploymentResource extends CfnDeployment { // if hash components were added to the deployment, we use them to calculate // a logical ID for the deployment resource. if (hash.length > 0) { - const md5 = crypto.createHash('md5'); - hash.map(x => this.stack.resolve(x)).forEach(c => md5.update(JSON.stringify(c))); - lid += md5.digest('hex'); + lid += md5hash(hash.map(x => this.stack.resolve(x)).map(c => JSON.stringify(c)).join('')); } return lid; diff --git a/packages/@aws-cdk/aws-ec2/lib/instance.ts b/packages/@aws-cdk/aws-ec2/lib/instance.ts index 8537ae0c32c51..4997f7dcbda3e 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance.ts @@ -1,7 +1,7 @@ -import * as crypto from 'crypto'; import * as iam from '@aws-cdk/aws-iam'; import { Annotations, Aspects, Duration, Fn, IResource, Lazy, Resource, Stack, Tags } from '@aws-cdk/core'; +import { md5hash } from '@aws-cdk/core/lib/helpers-internal'; import { Construct } from 'constructs'; import { InstanceRequireImdsv2Aspect } from './aspects'; import { CloudFormationInit } from './cfn-init'; @@ -423,14 +423,14 @@ export class Instance extends Resource implements IInstance { if (recursing) { return originalLogicalId; } if (!(props.userDataCausesReplacement ?? props.initOptions)) { return originalLogicalId; } - const md5 = crypto.createHash('md5'); + const fragments = new Array(); recursing = true; try { - md5.update(JSON.stringify(context.resolve(this.userData.render()))); + fragments.push(JSON.stringify(context.resolve(this.userData.render()))); } finally { recursing = false; } - const digest = md5.digest('hex').slice(0, 16); + const digest = md5hash(fragments.join('')).slice(0, 16); return `${originalLogicalId}${digest}`; }, })); diff --git a/packages/@aws-cdk/aws-ec2/lib/volume.ts b/packages/@aws-cdk/aws-ec2/lib/volume.ts index fb45ac14b67af..3e01d3823207c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/volume.ts +++ b/packages/@aws-cdk/aws-ec2/lib/volume.ts @@ -1,8 +1,7 @@ -import * as crypto from 'crypto'; - import { AccountRootPrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam'; import { IKey, ViaServicePrincipal } from '@aws-cdk/aws-kms'; import { IResource, Resource, Size, SizeRoundingBehavior, Stack, Token, Tags, Names, RemovalPolicy } from '@aws-cdk/core'; +import { md5hash } from '@aws-cdk/core/lib/helpers-internal'; import { Construct } from 'constructs'; import { CfnVolume } from './ec2.generated'; import { IInstance } from './instance'; @@ -565,9 +564,7 @@ abstract class VolumeBase extends Resource implements IVolume { } private calculateResourceTagValue(constructs: Construct[]): string { - const md5 = crypto.createHash('md5'); - constructs.forEach(construct => md5.update(Names.uniqueId(construct))); - return md5.digest('hex'); + return md5hash(constructs.map(c => Names.uniqueId(c)).join('')); } } diff --git a/packages/@aws-cdk/aws-glue/lib/code.ts b/packages/@aws-cdk/aws-glue/lib/code.ts index 9f2f03d9884be..5a914064fc26f 100644 --- a/packages/@aws-cdk/aws-glue/lib/code.ts +++ b/packages/@aws-cdk/aws-glue/lib/code.ts @@ -1,9 +1,9 @@ -import * as crypto from 'crypto'; import * as fs from 'fs'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as s3assets from '@aws-cdk/aws-s3-assets'; import * as cdk from '@aws-cdk/core'; +import { md5hash } from '@aws-cdk/core/lib/helpers-internal'; import * as constructs from 'constructs'; /** @@ -95,9 +95,7 @@ export class AssetCode extends Code { * Hash a string */ private hashcode(s: string): string { - const hash = crypto.createHash('md5'); - hash.update(s); - return hash.digest('hex'); + return md5hash(s); }; } diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts index 39acc9fd414a4..975cf799c7220 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts @@ -1,4 +1,4 @@ -import * as crypto from 'crypto'; +import { md5hash } from '@aws-cdk/core/lib/helpers-internal'; import { ISecurityGroup, IVpc, SubnetSelection } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -219,9 +219,7 @@ export class SelfManagedKafkaEventSource extends StreamEventSource { } private mappingId(target: lambda.IFunction) { - let hash = crypto.createHash('md5'); - hash.update(JSON.stringify(Stack.of(target).resolve(this.innerProps.bootstrapServers))); - const idHash = hash.digest('hex'); + const idHash = md5hash(JSON.stringify(Stack.of(target).resolve(this.innerProps.bootstrapServers))); return `KafkaEventSource:${idHash}:${this.innerProps.topic}`; } diff --git a/packages/@aws-cdk/aws-lambda/lib/function-hash.ts b/packages/@aws-cdk/aws-lambda/lib/function-hash.ts index 952f8f43eed8a..0d90d1974f514 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-hash.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-hash.ts @@ -1,5 +1,5 @@ -import * as crypto from 'crypto'; import { CfnResource, FeatureFlags, Stack } from '@aws-cdk/core'; +import { md5hash } from '@aws-cdk/core/lib/helpers-internal'; import { LAMBDA_RECOGNIZE_LAYER_VERSION, LAMBDA_RECOGNIZE_VERSION_PROPS } from '@aws-cdk/cx-api'; import { Function as LambdaFunction } from './function'; import { ILayerVersion } from './layers'; @@ -34,9 +34,7 @@ export function calculateFunctionHash(fn: LambdaFunction) { stringifiedConfig = stringifiedConfig + calculateLayersHash(fn._layers); } - const hash = crypto.createHash('md5'); - hash.update(stringifiedConfig); - return hash.digest('hex'); + return md5hash(stringifiedConfig); } export function trimFromStart(s: string, maxLength: number) { @@ -146,7 +144,5 @@ function calculateLayersHash(layers: ILayerVersion[]): string { layerConfig[layer.node.id] = properties; } - const hash = crypto.createHash('md5'); - hash.update(JSON.stringify(layerConfig)); - return hash.digest('hex'); + return md5hash(JSON.stringify(layerConfig)); } diff --git a/packages/@aws-cdk/aws-rds/lib/database-secret.ts b/packages/@aws-cdk/aws-rds/lib/database-secret.ts index 25b691c92f86b..8eb2dc91329ba 100644 --- a/packages/@aws-cdk/aws-rds/lib/database-secret.ts +++ b/packages/@aws-cdk/aws-rds/lib/database-secret.ts @@ -1,7 +1,7 @@ -import * as crypto from 'crypto'; import * as kms from '@aws-cdk/aws-kms'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import { Aws, Names } from '@aws-cdk/core'; +import { md5hash } from '@aws-cdk/core/lib/helpers-internal'; import { Construct } from 'constructs'; import { DEFAULT_PASSWORD_EXCLUDE_CHARS } from './private/util'; @@ -88,14 +88,13 @@ export class DatabaseSecret extends secretsmanager.Secret { }); if (props.replaceOnPasswordCriteriaChanges) { - const hash = crypto.createHash('md5'); - hash.update(JSON.stringify({ + const hash = md5hash(JSON.stringify({ // Use here the options that influence the password generation. // If at some point we add other password customization options // they sould be added here below (e.g. `passwordLength`). excludeCharacters, })); - const logicalId = `${Names.uniqueId(this)}${hash.digest('hex')}`; + const logicalId = `${Names.uniqueId(this)}${hash}`; const secret = this.node.defaultChild as secretsmanager.CfnSecret; secret.overrideLogicalId(logicalId.slice(-255)); // Take last 255 chars diff --git a/packages/@aws-cdk/aws-route53-patterns/lib/website-redirect.ts b/packages/@aws-cdk/aws-route53-patterns/lib/website-redirect.ts index c5a1b97ef4e74..20f0eacd8ec2e 100644 --- a/packages/@aws-cdk/aws-route53-patterns/lib/website-redirect.ts +++ b/packages/@aws-cdk/aws-route53-patterns/lib/website-redirect.ts @@ -1,4 +1,4 @@ -import * as crypto from 'crypto'; +import { md5hash } from '@aws-cdk/core/lib/helpers-internal'; import { DnsValidatedCertificate, ICertificate } from '@aws-cdk/aws-certificatemanager'; import { CloudFrontWebDistribution, OriginProtocolPolicy, PriceClass, ViewerCertificate, ViewerProtocolPolicy } from '@aws-cdk/aws-cloudfront'; import { ARecord, AaaaRecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; @@ -97,7 +97,7 @@ export class HttpsRedirect extends Construct { }); domainNames.forEach((domainName) => { - const hash = crypto.createHash('md5').update(domainName).digest('hex').slice(0, 6); + const hash = md5hash(domainName).slice(0, 6); const aliasProps = { recordName: domainName, zone: props.zone, diff --git a/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts b/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts index f9998739fa5c7..0080593706b88 100644 --- a/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts +++ b/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts @@ -1,4 +1,4 @@ -import * as crypto from 'crypto'; +import { md5hash } from '@aws-cdk/core/lib/helpers-internal'; import { IVpcEndpointService } from '@aws-cdk/aws-ec2'; import { Fn, Names, Stack } from '@aws-cdk/core'; import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '@aws-cdk/custom-resources'; @@ -225,7 +225,5 @@ interface PrivateDnsConfiguration { * Hash a string */ function hashcode(s: string): string { - const hash = crypto.createHash('md5'); - hash.update(s); - return hash.digest('hex'); + return md5hash(s); }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/lib/private/util.ts b/packages/@aws-cdk/aws-sagemaker/lib/private/util.ts index 742dd54a1fe2c..3419c14f0d950 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/private/util.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/private/util.ts @@ -1,5 +1,5 @@ -import * as crypto from 'crypto'; import * as cdk from '@aws-cdk/core'; +import { md5hash } from '@aws-cdk/core/lib/helpers-internal'; /** * Generates a hash from the provided string for the purposes of avoiding construct ID collision @@ -8,9 +8,7 @@ import * as cdk from '@aws-cdk/core'; * @returns A hex string representing the hash of the provided string */ export function hashcode(s: string): string { - const hash = crypto.createHash('md5'); - hash.update(s); - return hash.digest('hex'); + return md5hash(s); } /** diff --git a/packages/@aws-cdk/core/lib/helpers-internal/index.ts b/packages/@aws-cdk/core/lib/helpers-internal/index.ts index 9e48a3b027867..9a36222b224cf 100644 --- a/packages/@aws-cdk/core/lib/helpers-internal/index.ts +++ b/packages/@aws-cdk/core/lib/helpers-internal/index.ts @@ -1,2 +1,4 @@ export * from './cfn-parse'; +// Other libraries are going to need this as well +export { md5hash } from '../private/md5'; export * from './customize-roles'; diff --git a/packages/@aws-cdk/core/lib/private/md5.ts b/packages/@aws-cdk/core/lib/private/md5.ts new file mode 100644 index 0000000000000..1caca455d327c --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/md5.ts @@ -0,0 +1,200 @@ +/** + * md5 hashing + * + * Uses the built-in 'crypto' library by default for a C implementation + * of md5, but in case the crypto library has been compiled to disable + * FIPS-noncompliant hash suites, fall back to a pure JS implementation of + * md5. + */ +import * as crypto from 'crypto'; + +let _impl: undefined | ((x: string) => string); + +/* eslint-disable no-restricted-syntax */ + +/** + * Return a hash of the given input string, in hex format + */ +export function md5hash(x: string) { + if (!_impl) { + try { + crypto.createHash('md5'); + _impl = cryptoMd5; + } catch (e) { + _impl = jsMd5; + } + } + return _impl(x); +} + +/* eslint-disable no-bitwise */ + +export function cryptoMd5(x: string) { + const hash = crypto.createHash('md5'); + hash.update(x); + return hash.digest('hex'); +} + +export function jsMd5(s: string) { + return hex(md5Buffer(Buffer.from(s, 'utf-8'))); +} + +function md5Round(x: number[], k: ReadonlyArray) { + let a = x[0], b = x[1], c = x[2], d = x[3]; + + a = F(a, b, c, d, k[0], 7, -680876936); + d = F(d, a, b, c, k[1], 12, -389564586); + c = F(c, d, a, b, k[2], 17, 606105819); + b = F(b, c, d, a, k[3], 22, -1044525330); + a = F(a, b, c, d, k[4], 7, -176418897); + d = F(d, a, b, c, k[5], 12, 1200080426); + c = F(c, d, a, b, k[6], 17, -1473231341); + b = F(b, c, d, a, k[7], 22, -45705983); + a = F(a, b, c, d, k[8], 7, 1770035416); + d = F(d, a, b, c, k[9], 12, -1958414417); + c = F(c, d, a, b, k[10], 17, -42063); + b = F(b, c, d, a, k[11], 22, -1990404162); + a = F(a, b, c, d, k[12], 7, 1804603682); + d = F(d, a, b, c, k[13], 12, -40341101); + c = F(c, d, a, b, k[14], 17, -1502002290); + b = F(b, c, d, a, k[15], 22, 1236535329); + + a = G(a, b, c, d, k[1], 5, -165796510); + d = G(d, a, b, c, k[6], 9, -1069501632); + c = G(c, d, a, b, k[11], 14, 643717713); + b = G(b, c, d, a, k[0], 20, -373897302); + a = G(a, b, c, d, k[5], 5, -701558691); + d = G(d, a, b, c, k[10], 9, 38016083); + c = G(c, d, a, b, k[15], 14, -660478335); + b = G(b, c, d, a, k[4], 20, -405537848); + a = G(a, b, c, d, k[9], 5, 568446438); + d = G(d, a, b, c, k[14], 9, -1019803690); + c = G(c, d, a, b, k[3], 14, -187363961); + b = G(b, c, d, a, k[8], 20, 1163531501); + a = G(a, b, c, d, k[13], 5, -1444681467); + d = G(d, a, b, c, k[2], 9, -51403784); + c = G(c, d, a, b, k[7], 14, 1735328473); + b = G(b, c, d, a, k[12], 20, -1926607734); + + a = H(a, b, c, d, k[5], 4, -378558); + d = H(d, a, b, c, k[8], 11, -2022574463); + c = H(c, d, a, b, k[11], 16, 1839030562); + b = H(b, c, d, a, k[14], 23, -35309556); + a = H(a, b, c, d, k[1], 4, -1530992060); + d = H(d, a, b, c, k[4], 11, 1272893353); + c = H(c, d, a, b, k[7], 16, -155497632); + b = H(b, c, d, a, k[10], 23, -1094730640); + a = H(a, b, c, d, k[13], 4, 681279174); + d = H(d, a, b, c, k[0], 11, -358537222); + c = H(c, d, a, b, k[3], 16, -722521979); + b = H(b, c, d, a, k[6], 23, 76029189); + a = H(a, b, c, d, k[9], 4, -640364487); + d = H(d, a, b, c, k[12], 11, -421815835); + c = H(c, d, a, b, k[15], 16, 530742520); + b = H(b, c, d, a, k[2], 23, -995338651); + + a = I(a, b, c, d, k[0], 6, -198630844); + d = I(d, a, b, c, k[7], 10, 1126891415); + c = I(c, d, a, b, k[14], 15, -1416354905); + b = I(b, c, d, a, k[5], 21, -57434055); + a = I(a, b, c, d, k[12], 6, 1700485571); + d = I(d, a, b, c, k[3], 10, -1894986606); + c = I(c, d, a, b, k[10], 15, -1051523); + b = I(b, c, d, a, k[1], 21, -2054922799); + a = I(a, b, c, d, k[8], 6, 1873313359); + d = I(d, a, b, c, k[15], 10, -30611744); + c = I(c, d, a, b, k[6], 15, -1560198380); + b = I(b, c, d, a, k[13], 21, 1309151649); + a = I(a, b, c, d, k[4], 6, -145523070); + d = I(d, a, b, c, k[11], 10, -1120210379); + c = I(c, d, a, b, k[2], 15, 718787259); + b = I(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); +} + +function cmn(q: number, a: number, b: number, x: number, s: number, t: number) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); +} + +function F(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { + return cmn((b & c) | (~b & d), a, b, x, s, t); +} + +function G(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { + return cmn((b & d) | (c & ~d), a, b, x, s, t); +} + +function H(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { + return cmn(b ^ c ^ d, a, b, x, s, t); +} + +function I(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { + return cmn(c ^ (b | ~d), a, b, x, s, t); +} + +function md5Buffer(buf: Buffer) { + let n = buf.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i = 0; + + for (; i + 64 <= n; i += 64) { + md5Round(state, bytesToWordsBuf(buf, i)); + } + + // Padding - a single high 1 byte, and the length of the original message at the end + // Need to add 2 tails if the message is less than 9 bytes shorter than a multiple of 64 + // (Otherwise not enough room for high bit and the 64 bit size) + const remainingBytes = n - i; + const padding = Buffer.alloc(64 - remainingBytes < 9 ? 128 : 64); + + buf.copy(padding, 0, i); + padding.writeUint8(0x80, remainingBytes); // High bit + + const bitLength = n * 8; + padding.writeUint32LE(n << 3, padding.length - 8); + if (bitLength >= 0xFFFFFFFF) { + padding.writeUint32LE(Math.floor(bitLength / 0xFFFFFFFF), padding.length - 4); + } + + md5Round(state, bytesToWordsBuf(padding, 0)); + if (padding.length > 64) { + md5Round(state, bytesToWordsBuf(padding, 64)); + } + + return state; +} + +function bytesToWordsBuf(buf: Buffer, byteOffset: number) { + const ret = new Array(16); + let i = 0, j = byteOffset; + + for (; i < 16; i++, j += 4) { + ret[i] = buf.readUint32LE(j); + } + return ret; +} + +let hex_chr = '0123456789abcdef'.split(''); + +function hexify(n: number) { + const s = new Array(); + for (let j = 0; j < 4; j++) { + s.push(hex_chr[(n >>> (j * 8 + 4)) & 0x0f] + hex_chr[(n >>> (j * 8)) & 0x0f]); + } + return s.join(''); +} + +function hex(x: number[]) { + const ret = new Array(x.length); + for (let i = 0; i < x.length; i++) ret[i] = hexify(x[i]); + return ret.join(''); +} + +function add32(a: number, b: number) { + return (a + b) & 0xffffffff; +} diff --git a/packages/@aws-cdk/core/lib/private/unique-resource-name.ts b/packages/@aws-cdk/core/lib/private/unique-resource-name.ts index cf816dc9a5758..938d25eedb79c 100644 --- a/packages/@aws-cdk/core/lib/private/unique-resource-name.ts +++ b/packages/@aws-cdk/core/lib/private/unique-resource-name.ts @@ -1,5 +1,4 @@ -import { createHash } from 'crypto'; -// import { unresolved } from './encoding'; +import { md5hash } from './md5'; /** * Options for creating a unique resource name. @@ -86,7 +85,7 @@ export function makeUniqueResourceName(components: string[], options: MakeUnique * The hash is limited in size. */ function pathHash(path: string[]): string { - const md5 = createHash('md5').update(path.join(PATH_SEP)).digest('hex'); + const md5 = md5hash(path.join(PATH_SEP)); return md5.slice(0, HASH_LEN).toUpperCase(); } diff --git a/packages/@aws-cdk/core/lib/private/uniqueid.ts b/packages/@aws-cdk/core/lib/private/uniqueid.ts index c9d1d333891db..e4ee21a264b47 100644 --- a/packages/@aws-cdk/core/lib/private/uniqueid.ts +++ b/packages/@aws-cdk/core/lib/private/uniqueid.ts @@ -1,5 +1,5 @@ -import * as crypto from 'crypto'; import { unresolved } from './encoding'; +import { md5hash } from './md5'; /** * Resources with this ID are hidden from humans @@ -76,7 +76,7 @@ export function makeUniqueId(components: string[]) { * The hash is limited in size. */ function pathHash(path: string[]): string { - const md5 = crypto.createHash('md5').update(path.join(PATH_SEP)).digest('hex'); + const md5 = md5hash(path.join(PATH_SEP)); return md5.slice(0, HASH_LEN).toUpperCase(); } diff --git a/packages/@aws-cdk/core/test/private/md5.test.ts b/packages/@aws-cdk/core/test/private/md5.test.ts new file mode 100644 index 0000000000000..6158bf6b624cb --- /dev/null +++ b/packages/@aws-cdk/core/test/private/md5.test.ts @@ -0,0 +1,43 @@ +import { jsMd5, cryptoMd5 } from '../../lib/private/md5'; + +test.each([ + '', + 'asdf', + 'hello', + '🤠', + 'x'.repeat(54), + 'y'.repeat(63), + 'z'.repeat(64), + 'a'.repeat(64) + 'y', + 'b'.repeat(115), + 'c'.repeat(128), +])('test md5 equality for %p', s => { + expect(jsMd5(s)).toEqual(cryptoMd5(s)); +}); + +// eslint-disable-next-line jest/no-disabled-tests +test.skip('test md5 equality for a giant string (larger than 512MB)', () => { + const s = 'x'.repeat(515_000_000); + expect(jsMd5(s)).toEqual(cryptoMd5(s)); +}); + +describe('timing', () => { + const s = 'x'.repeat(352321); + const N = 100; + + // On my machine: + // - crypto: 73ms + // - native: 1187ms + + test('crypto', () => { + for (let i = 0; i < N; i++) { + cryptoMd5(s); + } + }); + + test('native', () => { + for (let i = 0; i < N; i++) { + jsMd5(s); + } + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/private/unique-resource-name.test.ts b/packages/@aws-cdk/core/test/private/unique-resource-name.test.ts index c2dcdeff445bc..1de8fab4b92f8 100644 --- a/packages/@aws-cdk/core/test/private/unique-resource-name.test.ts +++ b/packages/@aws-cdk/core/test/private/unique-resource-name.test.ts @@ -1,8 +1,8 @@ -import { createHash } from 'crypto'; +import { md5hash } from '../../lib/helpers-internal'; import { makeUniqueResourceName } from '../../lib/private/unique-resource-name'; const pathHash = (path: string[]): string => { - return createHash('md5').update(path.join('/')).digest('hex').slice(0, 8).toUpperCase(); + return md5hash(path.join('/')).slice(0, 8).toUpperCase(); }; describe('makeUniqueResourceName tests', () => { diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/private/hash.ts b/packages/@aws-cdk/integ-tests/lib/assertions/private/hash.ts index 38649bbea4473..0dc4b4ad5f5c9 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/private/hash.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/private/hash.ts @@ -1,10 +1,8 @@ -import * as crypto from 'crypto'; +import { md5hash as coreMd5 } from '@aws-cdk/core/lib/helpers-internal'; export function md5hash(obj: any): string { if (!obj || (typeof(obj) === 'object' && Object.keys(obj).length === 0)) { throw new Error('Cannot compute md5 hash for falsy object'); } - const hash = crypto.createHash('md5'); - hash.update(JSON.stringify(obj)); - return hash.digest('hex'); + return coreMd5(JSON.stringify(obj)); } diff --git a/tools/@aws-cdk/cdk-build-tools/config/eslintrc.js b/tools/@aws-cdk/cdk-build-tools/config/eslintrc.js index e7abc89b0f610..61ab478724fed 100644 --- a/tools/@aws-cdk/cdk-build-tools/config/eslintrc.js +++ b/tools/@aws-cdk/cdk-build-tools/config/eslintrc.js @@ -153,6 +153,16 @@ module.exports = { // Are you sure | is not a typo for || ? 'no-bitwise': ['error'], + // No more md5, will break in FIPS environments + "no-restricted-syntax": [ + "error", + { + // Both qualified and unqualified calls + "selector": "CallExpression:matches([callee.name='createHash'], [callee.property.name='createHash']) Literal[value='md5']", + "message": "Use the md5hash() function from the core library if you want md5" + } + ], + // Oh ho ho naming. Everyone's favorite topic! // FIXME: there's no way to do this properly. The proposed tslint replacement // works very differently, also checking names in object literals, which we use all over the