Skip to content

Commit 6e3d6fd

Browse files
committed
feat: compute private RSA key p, q, dp, dq, qi when omitted
resolves #26
1 parent b0ff436 commit 6e3d6fd

File tree

5 files changed

+191
-2
lines changed

5 files changed

+191
-2
lines changed

lib/help/key_utils.js

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { createPublicKey } = require('crypto')
33
const base64url = require('./base64url')
44
const errors = require('../errors')
55
const asn1 = require('./asn1')
6+
const computePrimes = require('./rsa_primes')
67
const { OKP_CURVES, EC_CURVES } = require('./consts')
78

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

203206
return RSAPrivateKey.encode({

lib/help/rsa_primes.js

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/* global BigInt */
2+
3+
const { randomBytes } = require('crypto')
4+
5+
const base64url = require('./base64url')
6+
7+
const ZERO = BigInt(0)
8+
const ONE = BigInt(1)
9+
const TWO = BigInt(2)
10+
11+
const toJWKParameter = n => base64url.encodeBuffer(Buffer.from(n.toString(16), 'hex'))
12+
const fromBuffer = buf => BigInt(`0x${buf.toString('hex')}`)
13+
const bitLength = n => n.toString(2).length
14+
15+
const eGcdX = (a, b) => {
16+
let x = ZERO
17+
let y = ONE
18+
let u = ONE
19+
let v = ZERO
20+
21+
while (a !== ZERO) {
22+
let q = b / a
23+
let r = b % a
24+
let m = x - (u * q)
25+
let n = y - (v * q)
26+
b = a
27+
a = r
28+
x = u
29+
y = v
30+
u = m
31+
v = n
32+
}
33+
return x
34+
}
35+
36+
const gcd = (a, b) => {
37+
let shift = ZERO
38+
while (!((a | b) & ONE)) {
39+
a >>= ONE
40+
b >>= ONE
41+
shift++
42+
}
43+
while (!(a & ONE)) {
44+
a >>= ONE
45+
}
46+
do {
47+
while (!(b & ONE)) {
48+
b >>= ONE
49+
}
50+
if (a > b) {
51+
let x = a
52+
a = b
53+
b = x
54+
}
55+
b -= a
56+
} while (b)
57+
58+
return a << shift
59+
}
60+
61+
const modPow = (a, b, n) => {
62+
a = toZn(a, n)
63+
let result = ONE
64+
let x = a
65+
while (b > 0) {
66+
var leastSignificantBit = b % TWO
67+
b = b / TWO
68+
if (leastSignificantBit === ONE) {
69+
result = result * x
70+
result = result % n
71+
}
72+
x = x * x
73+
x = x % n
74+
}
75+
return result
76+
}
77+
78+
const randBetween = (min, max) => {
79+
const interval = max - min
80+
const bitLen = bitLength(interval)
81+
let rnd
82+
do {
83+
rnd = fromBuffer(randBits(bitLen))
84+
} while (rnd > interval)
85+
return rnd + min
86+
}
87+
88+
const randBits = (bitLength) => {
89+
const byteLength = Math.ceil(bitLength / 8)
90+
const rndBytes = randomBytes(byteLength)
91+
// Fill with 0's the extra bits
92+
rndBytes[0] = rndBytes[0] & (2 ** (bitLength % 8) - 1)
93+
return rndBytes
94+
}
95+
96+
const toZn = (a, n) => {
97+
a = a % n
98+
return (a < 0) ? a + n : a
99+
}
100+
101+
const odd = (n) => {
102+
let r = n
103+
while (r % TWO === ZERO) {
104+
r = r / TWO
105+
}
106+
return r
107+
}
108+
109+
const getPrimeFactors = (e, d, n) => {
110+
const r = odd(e * d - ONE)
111+
112+
let y
113+
do {
114+
let i = modPow(randBetween(TWO, n), r, n)
115+
let o = ZERO
116+
while (i !== ONE) {
117+
o = i
118+
i = (i * i) % n
119+
}
120+
if (o !== (n - ONE)) {
121+
y = o
122+
}
123+
} while (!y)
124+
125+
const p = gcd(y - ONE, n)
126+
const q = n / p
127+
128+
return p > q ? { p, q } : { p: q, q: p }
129+
}
130+
131+
module.exports = (jwk) => {
132+
const e = fromBuffer(base64url.decodeToBuffer(jwk.e))
133+
const d = fromBuffer(base64url.decodeToBuffer(jwk.d))
134+
const n = fromBuffer(base64url.decodeToBuffer(jwk.n))
135+
136+
const { p, q } = getPrimeFactors(e, d, n)
137+
const dp = d % (p - ONE)
138+
const dq = d % (q - ONE)
139+
const qi = toZn(eGcdX(toZn(q, p), p), p)
140+
141+
return {
142+
...jwk,
143+
p: toJWKParameter(p),
144+
q: toJWKParameter(q),
145+
dp: toJWKParameter(dp),
146+
dq: toJWKParameter(dq),
147+
qi: toJWKParameter(qi)
148+
}
149+
}

test/jwe/smoke.test.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const test = require('ava')
33
const { randomBytes } = require('crypto')
44

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

88
const PAYLOAD = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
99
const ENCS = [
@@ -119,3 +119,16 @@ Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) =>
119119
})
120120
})
121121
})
122+
123+
{
124+
const rsa = generateSync('RSA')
125+
const dKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n, d: rsa.d })
126+
const eKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n })
127+
eKey.algorithms('wrapKey').forEach((alg) => {
128+
ENCS.forEach((enc) => {
129+
if (alg === 'ECDH-ES' && ['A192CBC-HS384', 'A256CBC-HS512'].includes(enc)) return
130+
test(`key RSA (min) > alg ${alg} > ${enc}`, success, eKey, dKey, alg, enc)
131+
test(`key RSA (min) > alg ${alg} > ${enc} (negative cases)`, failure, eKey, dKey, alg, enc)
132+
})
133+
})
134+
}

test/jwk/import.test.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const test = require('ava')
22
const crypto = require('crypto')
33

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

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

@@ -86,6 +86,20 @@ test('failed to import throws an error', t => {
8686
})
8787
})
8888

89+
test('minimal RSA test', async t => {
90+
const key = await generate('RSA')
91+
const { d, e, n } = key.toJWK(true)
92+
const minKey = importKey({ kty: 'RSA', d, e, n })
93+
key.algorithms('sign').forEach((alg) => {
94+
JWS.verify(JWS.sign({}, key), minKey, { alg })
95+
JWS.verify(JWS.sign({}, minKey), key, { alg })
96+
})
97+
key.algorithms('wrapKey').forEach((alg) => {
98+
JWE.decrypt(JWE.encrypt('foo', key), minKey, { alg })
99+
JWE.decrypt(JWE.encrypt('foo', minKey), key, { alg })
100+
})
101+
t.pass()
102+
})
89103

90104
test('fails to import RSA without all optimization parameters', async t => {
91105
const full = (await generate('RSA')).toJWK(true)

test/jws/smoke.test.js

+10
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,13 @@ sym.algorithms('sign').forEach((alg) => {
7575
test(`key ${sym.kty} > alg ${alg}`, success, sym, sym, alg)
7676
test(`key ${sym.kty} > alg ${alg} (negative cases)`, failure, sym, sym, alg)
7777
})
78+
79+
{
80+
const rsa = generateSync('RSA')
81+
const sKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n, d: rsa.d })
82+
const vKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n })
83+
sKey.algorithms('sign').forEach((alg) => {
84+
test(`key RSA (min) > alg ${alg}`, success, sKey, vKey, alg)
85+
test(`key RSA (min) > alg ${alg} (negative cases)`, failure, sKey, vKey, alg)
86+
})
87+
}

0 commit comments

Comments
 (0)