From 28d7cf8c78ec03ca3a9d37a98b28598ee35d0d15 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 23 Jun 2019 11:33:57 +0200 Subject: [PATCH] feat: add RSA-OAEP-256 support (when a node version supports it) resolves #29 --- README.md | 7 ++- docs/README.md | 3 ++ lib/help/node_support.js | 5 ++ lib/jwa/rsaes.js | 27 +++++++--- lib/jwk/key/rsa.js | 16 ++++-- test/jwk/rsa.test.js | 105 +++++++++++++++++++++++++++++---------- 6 files changed, 125 insertions(+), 38 deletions(-) create mode 100644 lib/help/node_support.js diff --git a/README.md b/README.md index 3d404bb636..5241cd8bcf 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Legend: | AES | ✓ | A128KW, A192KW, A256KW | | AES GCM | ✓ | A128GCMKW, A192GCMKW, A256GCMKW | | Direct Key Agreement | ✓ | dir | -| RSAES OAEP | ✓ | RSA-OAEP | +| RSAES OAEP | ✓ | RSA-OAEP, RSA-OAEP-256 | | RSAES-PKCS1-v1_5 | ✓ | RSA1_5 | | PBES2 | ✓ | PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW | | ECDH-ES | ✓ | ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW | @@ -79,10 +79,13 @@ Legend: | Logout Token - [OpenID Connect Back-Channel Logout 1.0][spec-oidc-logout_token] | ◯ || | JARM - [JWT Secured Authorization Response Mode for OAuth 2.0][draft-jarm] | ◯ || +Notes +- RSA-OAEP-256 is only supported when Node.js >= 12.9.0 runtime is detected +- See [#electron-support](#electron-support) for electron exceptions + --- Pending Node.js Support 🤞: -- RSA-OAEP-256 - see [nodejs/node#28335](https://github.com/nodejs/node/pull/28335) - ECDH-ES with X25519 and X448 - see [nodejs/node#26626](https://github.com/nodejs/node/pull/26626) Won't implement: diff --git a/docs/README.md b/docs/README.md index 3a86900066..50767dc341 100644 --- a/docs/README.md +++ b/docs/README.md @@ -227,10 +227,12 @@ privateKey.algorithms() // 'PS512', // 'RS512', // 'RSA-OAEP', +// 'RSA-OAEP-256', // 'RSA1_5' } privateKey.algorithms('wrapKey') // Set { // 'RSA-OAEP', +// 'RSA-OAEP-256', // 'RSA1_5' } const publicKey = generateSync('RSA', 2048, { use: 'enc' }, false) @@ -241,6 +243,7 @@ publicKey.algorithms('unwrapKey') publicKey.algorithms('wrapKey') // Set { // 'RSA-OAEP', +// 'RSA-OAEP-256', // 'RSA1_5' } ``` diff --git a/lib/help/node_support.js b/lib/help/node_support.js new file mode 100644 index 0000000000..25a5a9b69c --- /dev/null +++ b/lib/help/node_support.js @@ -0,0 +1,5 @@ +const [major, minor] = process.version.substr(1).split('.').map(x => parseInt(x, 10)) + +module.exports = { + oaepHash: major > 12 || (major === 12 && minor >= 9) +} diff --git a/lib/jwa/rsaes.js b/lib/jwa/rsaes.js index 328a0cf56c..bc236acc39 100644 --- a/lib/jwa/rsaes.js +++ b/lib/jwa/rsaes.js @@ -5,6 +5,7 @@ const { KEYOBJECT } = require('../help/consts') const resolvePadding = (alg) => { switch (alg) { + case 'RSA-OAEP-256': case 'RSA-OAEP': return constants.RSA_PKCS1_OAEP_PADDING case 'RSA1_5': @@ -12,22 +13,34 @@ const resolvePadding = (alg) => { } } -const wrapKey = (padding, { [KEYOBJECT]: keyObject }, payload) => { - return { wrapped: publicEncrypt({ key: keyObject, padding }, payload) } +const resolveOaepHash = (alg) => { + switch (alg) { + case 'RSA-OAEP-256': + return 'sha256' + case 'RSA-OAEP': + return 'sha1' + default: + return undefined + } +} + +const wrapKey = (padding, oaepHash, { [KEYOBJECT]: keyObject }, payload) => { + return { wrapped: publicEncrypt({ key: keyObject, oaepHash, padding }, payload) } } -const unwrapKey = (padding, { [KEYOBJECT]: keyObject }, payload) => { - return privateDecrypt({ key: keyObject, padding }, payload) +const unwrapKey = (padding, oaepHash, { [KEYOBJECT]: keyObject }, payload) => { + return privateDecrypt({ key: keyObject, oaepHash, padding }, payload) } module.exports = (JWA) => { - ['RSA1_5', 'RSA-OAEP'].forEach((jwaAlg) => { + ['RSA1_5', 'RSA-OAEP', 'RSA-OAEP-256'].forEach((jwaAlg) => { const padding = resolvePadding(jwaAlg) + const oaepHash = resolveOaepHash(jwaAlg) assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) - JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, padding)) - JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, padding)) + JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, padding, oaepHash)) + JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, padding, oaepHash)) }) } diff --git a/lib/jwk/key/rsa.js b/lib/jwk/key/rsa.js index 06b16aaa9d..a5f7d4322d 100644 --- a/lib/jwk/key/rsa.js +++ b/lib/jwk/key/rsa.js @@ -5,6 +5,7 @@ const { THUMBPRINT_MATERIAL, JWK_MEMBERS, PUBLIC_MEMBERS, PRIVATE_MEMBERS, KEY_MANAGEMENT_DECRYPT, KEY_MANAGEMENT_ENCRYPT } = require('../../help/consts') +const { oaepHash } = require('../../help/node_support') const Key = require('./base') @@ -13,6 +14,10 @@ const generateKeyPair = promisify(async) const SIG_ALGS = ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512'] const WRAP_ALGS = ['RSA-OAEP', 'RSA1_5'] +if (oaepHash) { + WRAP_ALGS.splice(1, 0, 'RSA-OAEP-256') +} + const RSA_PUBLIC = new Set(['e', 'n']) Object.freeze(RSA_PUBLIC) const RSA_PRIVATE = new Set([...RSA_PUBLIC, 'd', 'p', 'q', 'dp', 'dq', 'qi']) @@ -36,11 +41,14 @@ const sigAlgsAvailableFor = (length) => { } const wrapAlgsAvailableFor = (length) => { - if (length >= 592) { - return new Set(WRAP_ALGS) + switch (true) { + case length >= 784: + return new Set(WRAP_ALGS) + case length >= 592: + return new Set(['RSA-OAEP', 'RSA1_5']) + default: + return new Set(['RSA1_5']) } - - return new Set(['RSA1_5']) } // RSA Key Type diff --git a/test/jwk/rsa.test.js b/test/jwk/rsa.test.js index 70c045cbc5..f87f96ddf4 100644 --- a/test/jwk/rsa.test.js +++ b/test/jwk/rsa.test.js @@ -3,6 +3,7 @@ const { createPrivateKey, createPublicKey } = require('crypto') const { hasProperty, hasNoProperties, hasProperties } = require('../macros') const fixtures = require('../fixtures') +const { oaepHash } = require('../../lib/help/node_support') const { generateSync } = require('../../lib/jwk/generate') const RSAKey = require('../../lib/jwk/key/rsa') @@ -31,11 +32,19 @@ test(`RSA key .algorithms invalid operation`, t => { test(`RSA Private key`, hasProperty, key, 'type', 'private') test(`RSA Private key`, hasProperty, key, 'use', undefined) - test('RSA Private key algorithms (no operation)', t => { - const result = key.algorithms() - t.is(result.constructor, Set) - t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512', 'RSA-OAEP', 'RSA1_5']) - }) + if (oaepHash) { + test('RSA Private key algorithms (no operation)', t => { + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512', 'RSA-OAEP', 'RSA-OAEP-256', 'RSA1_5']) + }) + } else { + test('RSA Private key algorithms (no operation)', t => { + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512', 'RSA-OAEP', 'RSA1_5']) + }) + } test('RSA Private key algorithms (no operation, w/ alg)', t => { const key = new RSAKey(keyObject, { alg: 'RS256' }) @@ -116,11 +125,19 @@ test(`RSA key .algorithms invalid operation`, t => { t.deepEqual([...result], []) }) - test('RSA Private key .algorithms("wrapKey")', t => { - const result = key.algorithms('wrapKey') - t.is(result.constructor, Set) - t.deepEqual([...result], ['RSA-OAEP', 'RSA1_5']) - }) + if (oaepHash) { + test('RSA Private key .algorithms("wrapKey")', t => { + const result = key.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], ['RSA-OAEP', 'RSA-OAEP-256', 'RSA1_5']) + }) + } else { + test('RSA Private key .algorithms("wrapKey")', t => { + const result = key.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], ['RSA-OAEP', 'RSA1_5']) + }) + } test('RSA Private key .algorithms("wrapKey") when use is sig', t => { const sigKey = new RSAKey(keyObject, { use: 'sig' }) @@ -129,11 +146,19 @@ test(`RSA key .algorithms invalid operation`, t => { t.deepEqual([...result], []) }) - test('RSA Private key .algorithms("unwrapKey")', t => { - const result = key.algorithms('unwrapKey') - t.is(result.constructor, Set) - t.deepEqual([...result], ['RSA-OAEP', 'RSA1_5']) - }) + if (oaepHash) { + test('RSA Private key .algorithms("unwrapKey")', t => { + const result = key.algorithms('unwrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], ['RSA-OAEP', 'RSA-OAEP-256', 'RSA1_5']) + }) + } else { + test('RSA Private key .algorithms("unwrapKey")', t => { + const result = key.algorithms('unwrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], ['RSA-OAEP', 'RSA1_5']) + }) + } test('RSA Private key .algorithms("unwrapKey") when use is sig', t => { const sigKey = new RSAKey(keyObject, { use: 'sig' }) @@ -163,11 +188,19 @@ test(`RSA key .algorithms invalid operation`, t => { test(`RSA Public key`, hasProperty, key, 'type', 'public') test(`RSA Public key`, hasProperty, key, 'use', undefined) - test('RSA EC Public key algorithms (no operation)', t => { - const result = key.algorithms() - t.is(result.constructor, Set) - t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512', 'RSA-OAEP', 'RSA1_5']) - }) + if (oaepHash) { + test('RSA EC Public key algorithms (no operation)', t => { + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512', 'RSA-OAEP', 'RSA-OAEP-256', 'RSA1_5']) + }) + } else { + test('RSA EC Public key algorithms (no operation)', t => { + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512', 'RSA-OAEP', 'RSA1_5']) + }) + } test('RSA EC Public key algorithms (no operation, w/ alg)', t => { const key = new RSAKey(keyObject, { alg: 'RS256' }) @@ -248,11 +281,19 @@ test(`RSA key .algorithms invalid operation`, t => { t.deepEqual([...result], []) }) - test('RSA Public key .algorithms("wrapKey")', t => { - const result = key.algorithms('wrapKey') - t.is(result.constructor, Set) - t.deepEqual([...result], ['RSA-OAEP', 'RSA1_5']) - }) + if (oaepHash) { + test('RSA Public key .algorithms("wrapKey")', t => { + const result = key.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], ['RSA-OAEP', 'RSA-OAEP-256', 'RSA1_5']) + }) + } else { + test('RSA Public key .algorithms("wrapKey")', t => { + const result = key.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], ['RSA-OAEP', 'RSA1_5']) + }) + } test('RSA Public key .algorithms("wrapKey") when use is sig', t => { const sigKey = new RSAKey(keyObject, { use: 'sig' }) @@ -295,6 +336,13 @@ test(`RSA key .algorithms invalid operation`, t => { t.true(k.algorithms().has('RS512')) }) + if (oaepHash) { + test('RSA key >= 784 bits can do RSA-OAEP-256', t => { + const k = generateSync('RSA', 784) + t.true(k.algorithms().has('RSA-OAEP-256')) + }) + } + test('RSA key >= 784 bits can do PS384', t => { const k = generateSync('RSA', 784) t.true(k.algorithms().has('PS384')) @@ -322,6 +370,13 @@ test(`RSA key .algorithms invalid operation`, t => { t.true(k.algorithms().has('PS384')) }) + if (oaepHash) { + test('RSA key >= 896 bits can do RSA-OAEP-256', t => { + const k = generateSync('RSA', 896) + t.true(k.algorithms().has('RSA-OAEP-256')) + }) + } + test('RSA key >= 1152 bits can do PS512', t => { const k = generateSync('RSA', 1152) t.true(k.algorithms().has('PS512'))