Skip to content

Commit

Permalink
feat: add RSA-OAEP-256 support (when a node version supports it)
Browse files Browse the repository at this point in the history
resolves #29
  • Loading branch information
panva committed Aug 20, 2019
1 parent d157d23 commit 28d7cf8
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 38 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -241,6 +243,7 @@ publicKey.algorithms('unwrapKey')
publicKey.algorithms('wrapKey')
// Set {
// 'RSA-OAEP',
// 'RSA-OAEP-256',
// 'RSA1_5' }
```
</details>
Expand Down
5 changes: 5 additions & 0 deletions lib/help/node_support.js
Original file line number Diff line number Diff line change
@@ -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)
}
27 changes: 20 additions & 7 deletions lib/jwa/rsaes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,42 @@ 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':
return constants.RSA_PKCS1_PADDING
}
}

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))
})
}
16 changes: 12 additions & 4 deletions lib/jwk/key/rsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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'])
Expand All @@ -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
Expand Down
105 changes: 80 additions & 25 deletions test/jwk/rsa.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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' })
Expand Down Expand Up @@ -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' })
Expand All @@ -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' })
Expand Down Expand Up @@ -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' })
Expand Down Expand Up @@ -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' })
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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'))
Expand Down

0 comments on commit 28d7cf8

Please sign in to comment.