From 6211d8c73a51b3826d02f699669aafb1e2827797 Mon Sep 17 00:00:00 2001 From: Gus Narea Date: Sat, 22 Jun 2019 16:30:39 +0100 Subject: [PATCH] Crypto: Implement generation of RSA keys See #3. --- package-lock.json | 58 ++++++++++++++++++++++++++++++----- package.json | 5 +++- src/lib/crypto.spec.ts | 68 ++++++++++++++++++++++++++++++++++++++++++ src/lib/crypto.ts | 39 ++++++++++++++++++++++++ src/types/pkijs.d.ts | 2 ++ tsconfig.json | 7 +++-- tslint.json | 9 +----- 7 files changed, 169 insertions(+), 19 deletions(-) create mode 100644 src/lib/crypto.spec.ts create mode 100644 src/lib/crypto.ts create mode 100644 src/types/pkijs.d.ts diff --git a/package-lock.json b/package-lock.json index 05c8ae0ce..bc4856939 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1045,6 +1045,14 @@ "safer-buffer": "~2.1.0" } }, + "asn1js": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.0.22.tgz", + "integrity": "sha512-mKox8OHPmLZY0FUbd8JYsAu2lQk61yFH8gTq4N7AT7NzmUKISpy0LEjV/AfLyE/SNCUicdMDZUFbocsf/9qOTg==", + "requires": { + "pvutils": "^1.0.17" + } + }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -1324,6 +1332,11 @@ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", "dev": true }, + "bytestreamjs": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-1.0.27.tgz", + "integrity": "sha512-P3eEIZi2olI+lgb7RpGRluJdGoDdSzh0A9hmNP4s7YHiuuw1KRKARxmkdDkDhQCGmV14OVkieTmE/F1/roxiWg==" + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -6063,8 +6076,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minimist-options": { "version": "3.0.2", @@ -6110,7 +6122,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" } @@ -6155,9 +6166,7 @@ "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "nanomatch": { "version": "1.2.13", @@ -6233,6 +6242,17 @@ "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=", "dev": true }, + "node-webcrypto-ossl": { + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/node-webcrypto-ossl/-/node-webcrypto-ossl-1.0.47.tgz", + "integrity": "sha512-73q5ClXxhr7c1LE5dYajdnCVAPr0dhceX/qkATmJGQWyn7xTLgW9s727Bep/6JPqdnx5v7aBraPc+1c2nqjctw==", + "requires": { + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "tslib": "^1.9.3", + "webcrypto-core": "^0.1.26" + } + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -6709,6 +6729,16 @@ } } }, + "pkijs": { + "version": "2.1.78", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-2.1.78.tgz", + "integrity": "sha512-HvZTIez4LB/VQ2BwGxSVTABKige5plbnlk4kJB3qxiU6iI/xoWZD4626TJuGP32ybJkHqZRvOLvT6yhQacBtlQ==", + "requires": { + "asn1js": "^2.0.22", + "bytestreamjs": "^1.0.27", + "pvutils": "^1.0.17" + } + }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -6809,6 +6839,11 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "pvutils": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.17.tgz", + "integrity": "sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ==" + }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -8622,8 +8657,7 @@ "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, "tslint": { "version": "5.17.0", @@ -8989,6 +9023,14 @@ "makeerror": "1.0.x" } }, + "webcrypto-core": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-0.1.26.tgz", + "integrity": "sha512-BZVgJZkkHyuz8loKvsaOKiBDXDpmMZf5xG4QAOlSeYdXlFUl9c1FRrVnAXcOdb4fTHMG+TRu81odJwwSfKnWTA==", + "requires": { + "tslib": "^1.7.1" + } + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/package.json b/package.json index be40b702e..1d6539c6f 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,10 @@ "engines": { "node": ">=8.9" }, - "dependencies": {}, + "dependencies": { + "node-webcrypto-ossl": "^1.0.47", + "pkijs": "^2.1.78" + }, "devDependencies": { "@types/jest": "^24.0.15", "codecov": "^3.5.0", diff --git a/src/lib/crypto.spec.ts b/src/lib/crypto.spec.ts new file mode 100644 index 000000000..cd9760793 --- /dev/null +++ b/src/lib/crypto.spec.ts @@ -0,0 +1,68 @@ +import { generateRsaKeys } from './crypto'; + +describe('generateRsaKeys', () => { + test('Keys should be RSA', async () => { + const keyPair = await generateRsaKeys(); + + expect(keyPair.publicKey.algorithm.name).toMatch(/^RSA-/); + expect(keyPair.privateKey.algorithm.name).toMatch(/^RSA-/); + }); + + test('Keys should be extractable', async () => { + const keyPair = await generateRsaKeys(); + + expect(keyPair.publicKey.extractable).toBe(true); + expect(keyPair.privateKey.extractable).toBe(true); + }); + + describe('Modulus', () => { + test('Default modulus should be 2048', async () => { + const keyPair = await generateRsaKeys(); + // @ts-ignore + expect(keyPair.publicKey.algorithm.modulusLength).toBe(2048); + // @ts-ignore + expect(keyPair.privateKey.algorithm.modulusLength).toBe(2048); + }); + + test('Modulus > 2048 should be supported', async () => { + const modulus = 3072; + const keyPair = await generateRsaKeys({ modulus }); + // @ts-ignore + expect(keyPair.publicKey.algorithm.modulusLength).toBe(modulus); + // @ts-ignore + expect(keyPair.privateKey.algorithm.modulusLength).toBe(modulus); + }); + + test('Modulus < 2048 should not supported', async () => { + await expect(generateRsaKeys({ modulus: 1024 })).rejects.toThrow( + 'RSA modulus must be => 2048 per RS-018 (got 1024)' + ); + }); + }); + + describe('Hashing algorithm', () => { + test('SHA-256 should be used by default', async () => { + const keyPair = await generateRsaKeys(); + // @ts-ignore + expect(keyPair.publicKey.algorithm.hash.name).toBe('SHA-256'); + // @ts-ignore + expect(keyPair.privateKey.algorithm.hash.name).toBe('SHA-256'); + }); + + ['SHA-384', 'SHA-512'].forEach(hashingAlgorithm => { + test(`${hashingAlgorithm} should be supported`, async () => { + const keyPair = await generateRsaKeys({ hashingAlgorithm }); + // @ts-ignore + expect(keyPair.publicKey.algorithm.hash.name).toBe(hashingAlgorithm); + // @ts-ignore + expect(keyPair.privateKey.algorithm.hash.name).toBe(hashingAlgorithm); + }); + }); + + test('SHA-1 should not be supported', async () => { + await expect( + generateRsaKeys({ hashingAlgorithm: 'SHA-1' }) + ).rejects.toThrow('SHA-1 is disallowed by RS-018'); + }); + }); +}); diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts new file mode 100644 index 000000000..ad2d0348f --- /dev/null +++ b/src/lib/crypto.ts @@ -0,0 +1,39 @@ +import WebCrypto from 'node-webcrypto-ossl'; +import { CryptoEngine, getAlgorithmParameters, setEngine } from 'pkijs'; + +const webcrypto = new WebCrypto(); +const cryptoEngine = new CryptoEngine({ + crypto: webcrypto, + name: 'nodeEngine', + subtle: webcrypto.subtle +}); +setEngine('nodeEngine', webcrypto, cryptoEngine); + +/** + * Generate an RSA symmetric key + * + * @param modulus The RSA modulus for the keys (2048 or greater). + * @param hashingAlgorithm The hashing algorithm (e.g., SHA-256, SHA-384, SHA-512). + * @throws Error If the modulus or the hashing algorithm is disallowed by RS-018. + */ +export async function generateRsaKeys({ + modulus = 2048, + hashingAlgorithm = 'SHA-256' +} = {}): Promise { + if (modulus < 2048) { + throw new Error(`RSA modulus must be => 2048 per RS-018 (got ${modulus})`); + } + + // RS-018 disallows MD5 and SHA-1, but only SHA-1 is supported in WebCrypto + if (hashingAlgorithm === 'SHA-1') { + throw new Error('SHA-1 is disallowed by RS-018'); + } + + const algorithm = getAlgorithmParameters('RSA-PSS', 'generatekey'); + // tslint:disable-next-line:no-object-mutation + algorithm.algorithm.hash.name = hashingAlgorithm; + // tslint:disable-next-line:no-object-mutation + algorithm.algorithm.modulusLength = modulus; + + return cryptoEngine.generateKey(algorithm.algorithm, true, algorithm.usages); +} diff --git a/src/types/pkijs.d.ts b/src/types/pkijs.d.ts new file mode 100644 index 000000000..d713b93d9 --- /dev/null +++ b/src/types/pkijs.d.ts @@ -0,0 +1,2 @@ +// @types/pkijs didn't work becuase it didn't define the "pkijs" module. +declare module 'pkijs'; diff --git a/tsconfig.json b/tsconfig.json index 8401e2f35..65133fadd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,9 +35,12 @@ // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, - "lib": ["es2017"], + "lib": [ + "es2017", + "dom" + ], "types": ["node", "jest"], - "typeRoots": ["node_modules/@types"] + "typeRoots": ["node_modules/@types", "src/types"] }, "include": ["src/**/*.ts"], "exclude": ["node_modules/**"], diff --git a/tslint.json b/tslint.json index b954a5dce..b3ab896ae 100644 --- a/tslint.json +++ b/tslint.json @@ -21,14 +21,7 @@ "no-method-signature": true, // Functional style rules - "no-this": true, - "no-class": true, - "no-mixed-interface": true, - "no-expression-statement": [ - true, - { "ignore-prefix": ["console.", "process.exit"] } - ], - "no-if-statement": true + "no-mixed-interface": true /* end tslint-immutable rules */ } }