Skip to content

Commit

Permalink
feat: compute private RSA key p, q, dp, dq, qi when omitted
Browse files Browse the repository at this point in the history
resolves #26
  • Loading branch information
panva committed May 23, 2019
1 parent b0ff436 commit 6e3d6fd
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 2 deletions.
3 changes: 3 additions & 0 deletions lib/help/key_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { createPublicKey } = require('crypto')
const base64url = require('./base64url')
const errors = require('../errors')
const asn1 = require('./asn1')
const computePrimes = require('./rsa_primes')
const { OKP_CURVES, EC_CURVES } = require('./consts')

const oidHexToCurve = new Map([
Expand Down Expand Up @@ -198,6 +199,8 @@ const jwkToPem = {
if (!(jwk.p && jwk.q && jwk.dp && jwk.dq && jwk.qi)) {
throw new errors.JWKImportFailed('all other private key parameters must be present when any one of them is present')
}
} else {
jwk = computePrimes(jwk)
}

return RSAPrivateKey.encode({
Expand Down
149 changes: 149 additions & 0 deletions lib/help/rsa_primes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* global BigInt */

const { randomBytes } = require('crypto')

const base64url = require('./base64url')

const ZERO = BigInt(0)
const ONE = BigInt(1)
const TWO = BigInt(2)

const toJWKParameter = n => base64url.encodeBuffer(Buffer.from(n.toString(16), 'hex'))
const fromBuffer = buf => BigInt(`0x${buf.toString('hex')}`)
const bitLength = n => n.toString(2).length

const eGcdX = (a, b) => {
let x = ZERO
let y = ONE
let u = ONE
let v = ZERO

while (a !== ZERO) {
let q = b / a
let r = b % a
let m = x - (u * q)
let n = y - (v * q)
b = a
a = r
x = u
y = v
u = m
v = n
}
return x
}

const gcd = (a, b) => {
let shift = ZERO
while (!((a | b) & ONE)) {
a >>= ONE
b >>= ONE
shift++
}
while (!(a & ONE)) {
a >>= ONE
}
do {
while (!(b & ONE)) {
b >>= ONE
}
if (a > b) {
let x = a
a = b
b = x
}
b -= a
} while (b)

return a << shift
}

const modPow = (a, b, n) => {
a = toZn(a, n)
let result = ONE
let x = a
while (b > 0) {
var leastSignificantBit = b % TWO
b = b / TWO
if (leastSignificantBit === ONE) {
result = result * x
result = result % n
}
x = x * x
x = x % n
}
return result
}

const randBetween = (min, max) => {
const interval = max - min
const bitLen = bitLength(interval)
let rnd
do {
rnd = fromBuffer(randBits(bitLen))
} while (rnd > interval)
return rnd + min
}

const randBits = (bitLength) => {
const byteLength = Math.ceil(bitLength / 8)
const rndBytes = randomBytes(byteLength)
// Fill with 0's the extra bits
rndBytes[0] = rndBytes[0] & (2 ** (bitLength % 8) - 1)
return rndBytes
}

const toZn = (a, n) => {
a = a % n
return (a < 0) ? a + n : a
}

const odd = (n) => {
let r = n
while (r % TWO === ZERO) {
r = r / TWO
}
return r
}

const getPrimeFactors = (e, d, n) => {
const r = odd(e * d - ONE)

let y
do {
let i = modPow(randBetween(TWO, n), r, n)
let o = ZERO
while (i !== ONE) {
o = i
i = (i * i) % n
}
if (o !== (n - ONE)) {
y = o
}
} while (!y)

const p = gcd(y - ONE, n)
const q = n / p

return p > q ? { p, q } : { p: q, q: p }
}

module.exports = (jwk) => {
const e = fromBuffer(base64url.decodeToBuffer(jwk.e))
const d = fromBuffer(base64url.decodeToBuffer(jwk.d))
const n = fromBuffer(base64url.decodeToBuffer(jwk.n))

const { p, q } = getPrimeFactors(e, d, n)
const dp = d % (p - ONE)
const dq = d % (q - ONE)
const qi = toZn(eGcdX(toZn(q, p), p), p)

return {
...jwk,
p: toJWKParameter(p),
q: toJWKParameter(q),
dp: toJWKParameter(dp),
dq: toJWKParameter(dq),
qi: toJWKParameter(qi)
}
}
15 changes: 14 additions & 1 deletion test/jwe/smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const test = require('ava')
const { randomBytes } = require('crypto')

const { encrypt, decrypt } = require('../../lib/jwe')
const { JWK: { importKey }, errors } = require('../..')
const { JWK: { importKey, generateSync }, errors } = require('../..')

const PAYLOAD = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
const ENCS = [
Expand Down Expand Up @@ -119,3 +119,16 @@ Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) =>
})
})
})

{
const rsa = generateSync('RSA')
const dKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n, d: rsa.d })
const eKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n })
eKey.algorithms('wrapKey').forEach((alg) => {
ENCS.forEach((enc) => {
if (alg === 'ECDH-ES' && ['A192CBC-HS384', 'A256CBC-HS512'].includes(enc)) return
test(`key RSA (min) > alg ${alg} > ${enc}`, success, eKey, dKey, alg, enc)
test(`key RSA (min) > alg ${alg} > ${enc} (negative cases)`, failure, eKey, dKey, alg, enc)
})
})
}
16 changes: 15 additions & 1 deletion test/jwk/import.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const test = require('ava')
const crypto = require('crypto')

const { JWK: { importKey, generate }, errors } = require('../..')
const { JWS, JWE, JWK: { importKey, generate }, errors } = require('../..')

const fixtures = require('../fixtures')

Expand Down Expand Up @@ -86,6 +86,20 @@ test('failed to import throws an error', t => {
})
})

test('minimal RSA test', async t => {
const key = await generate('RSA')
const { d, e, n } = key.toJWK(true)
const minKey = importKey({ kty: 'RSA', d, e, n })
key.algorithms('sign').forEach((alg) => {
JWS.verify(JWS.sign({}, key), minKey, { alg })
JWS.verify(JWS.sign({}, minKey), key, { alg })
})
key.algorithms('wrapKey').forEach((alg) => {
JWE.decrypt(JWE.encrypt('foo', key), minKey, { alg })
JWE.decrypt(JWE.encrypt('foo', minKey), key, { alg })
})
t.pass()
})

test('fails to import RSA without all optimization parameters', async t => {
const full = (await generate('RSA')).toJWK(true)
Expand Down
10 changes: 10 additions & 0 deletions test/jws/smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,13 @@ sym.algorithms('sign').forEach((alg) => {
test(`key ${sym.kty} > alg ${alg}`, success, sym, sym, alg)
test(`key ${sym.kty} > alg ${alg} (negative cases)`, failure, sym, sym, alg)
})

{
const rsa = generateSync('RSA')
const sKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n, d: rsa.d })
const vKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n })
sKey.algorithms('sign').forEach((alg) => {
test(`key RSA (min) > alg ${alg}`, success, sKey, vKey, alg)
test(`key RSA (min) > alg ${alg} (negative cases)`, failure, sKey, vKey, alg)
})
}

0 comments on commit 6e3d6fd

Please sign in to comment.