1 |
|
2 |
|
3 | const { EOL } = require('os')
|
4 |
|
5 | const errors = require('../errors')
|
6 |
|
7 | const { keyObjectSupported } = require('./runtime_support')
|
8 | const { createPublicKey } = require('./key_object')
|
9 | const base64url = require('./base64url')
|
10 | const asn1 = require('./asn1')
|
11 | const computePrimes = require('./rsa_primes')
|
12 | const { OKP_CURVES, EC_CURVES } = require('../registry')
|
13 |
|
14 | const formatPem = (base64pem, descriptor) => `-----BEGIN ${descriptor} KEY-----${EOL}${(base64pem.match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END ${descriptor} KEY-----`
|
15 |
|
16 | const okpToJWK = {
|
17 | private (crv, keyObject) {
|
18 | const der = keyObject.export({ type: 'pkcs8', format: 'der' })
|
19 | const OneAsymmetricKey = asn1.get('OneAsymmetricKey')
|
20 | const { privateKey: { privateKey: d } } = OneAsymmetricKey.decode(der)
|
21 |
|
22 | return {
|
23 | ...okpToJWK.public(crv, createPublicKey(keyObject)),
|
24 | d: base64url.encodeBuffer(d)
|
25 | }
|
26 | },
|
27 | public (crv, keyObject) {
|
28 | const der = keyObject.export({ type: 'spki', format: 'der' })
|
29 |
|
30 | const PublicKeyInfo = asn1.get('PublicKeyInfo')
|
31 |
|
32 | const { publicKey: { data: x } } = PublicKeyInfo.decode(der)
|
33 |
|
34 | return {
|
35 | kty: 'OKP',
|
36 | crv,
|
37 | x: base64url.encodeBuffer(x)
|
38 | }
|
39 | }
|
40 | }
|
41 |
|
42 | const keyObjectToJWK = {
|
43 | rsa: {
|
44 | private (keyObject) {
|
45 | const der = keyObject.export({ type: 'pkcs8', format: 'der' })
|
46 |
|
47 | const PrivateKeyInfo = asn1.get('PrivateKeyInfo')
|
48 | const RSAPrivateKey = asn1.get('RSAPrivateKey')
|
49 |
|
50 | const { privateKey } = PrivateKeyInfo.decode(der)
|
51 | const { version, n, e, d, p, q, dp, dq, qi } = RSAPrivateKey.decode(privateKey)
|
52 |
|
53 | if (version !== 'two-prime') {
|
54 | throw new errors.JOSENotSupported('Private RSA keys with more than two primes are not supported')
|
55 | }
|
56 |
|
57 | return {
|
58 | kty: 'RSA',
|
59 | n: base64url.encodeBigInt(n),
|
60 | e: base64url.encodeBigInt(e),
|
61 | d: base64url.encodeBigInt(d),
|
62 | p: base64url.encodeBigInt(p),
|
63 | q: base64url.encodeBigInt(q),
|
64 | dp: base64url.encodeBigInt(dp),
|
65 | dq: base64url.encodeBigInt(dq),
|
66 | qi: base64url.encodeBigInt(qi)
|
67 | }
|
68 | },
|
69 | public (keyObject) {
|
70 | const der = keyObject.export({ type: 'spki', format: 'der' })
|
71 |
|
72 | const PublicKeyInfo = asn1.get('PublicKeyInfo')
|
73 | const RSAPublicKey = asn1.get('RSAPublicKey')
|
74 |
|
75 | const { publicKey: { data: publicKey } } = PublicKeyInfo.decode(der)
|
76 | const { n, e } = RSAPublicKey.decode(publicKey)
|
77 |
|
78 | return {
|
79 | kty: 'RSA',
|
80 | n: base64url.encodeBigInt(n),
|
81 | e: base64url.encodeBigInt(e)
|
82 | }
|
83 | }
|
84 | },
|
85 | ec: {
|
86 | private (keyObject) {
|
87 | const der = keyObject.export({ type: 'pkcs8', format: 'der' })
|
88 |
|
89 | const PrivateKeyInfo = asn1.get('PrivateKeyInfo')
|
90 | const ECPrivateKey = asn1.get('ECPrivateKey')
|
91 |
|
92 | const { privateKey, algorithm: { parameters: { value: crv } } } = PrivateKeyInfo.decode(der)
|
93 | const { privateKey: d, publicKey } = ECPrivateKey.decode(privateKey)
|
94 |
|
95 | if (typeof publicKey === 'undefined') {
|
96 | if (keyObjectSupported) {
|
97 | return {
|
98 | ...keyObjectToJWK.ec.public(createPublicKey(keyObject)),
|
99 | d: base64url.encodeBuffer(d)
|
100 | }
|
101 | }
|
102 |
|
103 | throw new errors.JOSENotSupported('Private EC keys without the public key embedded are not supported in your Node.js runtime version')
|
104 | }
|
105 |
|
106 | const x = publicKey.data.slice(1, ((publicKey.data.length - 1) / 2) + 1)
|
107 | const y = publicKey.data.slice(((publicKey.data.length - 1) / 2) + 1)
|
108 |
|
109 | return {
|
110 | kty: 'EC',
|
111 | crv,
|
112 | d: base64url.encodeBuffer(d),
|
113 | x: base64url.encodeBuffer(x),
|
114 | y: base64url.encodeBuffer(y)
|
115 | }
|
116 | },
|
117 | public (keyObject) {
|
118 | const der = keyObject.export({ type: 'spki', format: 'der' })
|
119 |
|
120 | const PublicKeyInfo = asn1.get('PublicKeyInfo')
|
121 |
|
122 | const { publicKey: { data: publicKey }, algorithm: { parameters: { value: crv } } } = PublicKeyInfo.decode(der)
|
123 |
|
124 | const x = publicKey.slice(1, ((publicKey.length - 1) / 2) + 1)
|
125 | const y = publicKey.slice(((publicKey.length - 1) / 2) + 1)
|
126 |
|
127 | return {
|
128 | kty: 'EC',
|
129 | crv,
|
130 | x: base64url.encodeBuffer(x),
|
131 | y: base64url.encodeBuffer(y)
|
132 | }
|
133 | }
|
134 | },
|
135 | ed25519: {
|
136 | private (keyObject) {
|
137 | return okpToJWK.private('Ed25519', keyObject)
|
138 | },
|
139 | public (keyObject) {
|
140 | return okpToJWK.public('Ed25519', keyObject)
|
141 | }
|
142 | },
|
143 | ed448: {
|
144 | private (keyObject) {
|
145 | return okpToJWK.private('Ed448', keyObject)
|
146 | },
|
147 | public (keyObject) {
|
148 | return okpToJWK.public('Ed448', keyObject)
|
149 | }
|
150 | },
|
151 | x25519: {
|
152 | private (keyObject) {
|
153 | return okpToJWK.private('X25519', keyObject)
|
154 | },
|
155 | public (keyObject) {
|
156 | return okpToJWK.public('X25519', keyObject)
|
157 | }
|
158 | },
|
159 | x448: {
|
160 | private (keyObject) {
|
161 | return okpToJWK.private('X448', keyObject)
|
162 | },
|
163 | public (keyObject) {
|
164 | return okpToJWK.public('X448', keyObject)
|
165 | }
|
166 | }
|
167 | }
|
168 |
|
169 | module.exports.keyObjectToJWK = (keyObject) => {
|
170 | if (keyObject.type === 'private') {
|
171 | return keyObjectToJWK[keyObject.asymmetricKeyType].private(keyObject)
|
172 | }
|
173 |
|
174 | return keyObjectToJWK[keyObject.asymmetricKeyType].public(keyObject)
|
175 | }
|
176 |
|
177 | const concatEcPublicKey = (x, y) => ({
|
178 | unused: 0,
|
179 | data: Buffer.concat([
|
180 | Buffer.alloc(1, 4),
|
181 | base64url.decodeToBuffer(x),
|
182 | base64url.decodeToBuffer(y)
|
183 | ])
|
184 | })
|
185 |
|
186 | const jwkToPem = {
|
187 | RSA: {
|
188 | private (jwk, { calculateMissingRSAPrimes }) {
|
189 | const RSAPrivateKey = asn1.get('RSAPrivateKey')
|
190 |
|
191 | if ('oth' in jwk) {
|
192 | throw new errors.JOSENotSupported('Private RSA keys with more than two primes are not supported')
|
193 | }
|
194 |
|
195 | if (jwk.p || jwk.q || jwk.dp || jwk.dq || jwk.qi) {
|
196 | if (!(jwk.p && jwk.q && jwk.dp && jwk.dq && jwk.qi)) {
|
197 | throw new errors.JWKInvalid('all other private key parameters must be present when any one of them is present')
|
198 | }
|
199 | } else if (calculateMissingRSAPrimes) {
|
200 | jwk = computePrimes(jwk)
|
201 | } else if (!calculateMissingRSAPrimes) {
|
202 | throw new errors.JOSENotSupported('importing private RSA keys without all other private key parameters is not enabled, see documentation and its advisory on how and when its ok to enable it')
|
203 | }
|
204 |
|
205 | return RSAPrivateKey.encode({
|
206 | version: 0,
|
207 | n: BigInt(`0x${base64url.decodeToBuffer(jwk.n).toString('hex')}`),
|
208 | e: BigInt(`0x${base64url.decodeToBuffer(jwk.e).toString('hex')}`),
|
209 | d: BigInt(`0x${base64url.decodeToBuffer(jwk.d).toString('hex')}`),
|
210 | p: BigInt(`0x${base64url.decodeToBuffer(jwk.p).toString('hex')}`),
|
211 | q: BigInt(`0x${base64url.decodeToBuffer(jwk.q).toString('hex')}`),
|
212 | dp: BigInt(`0x${base64url.decodeToBuffer(jwk.dp).toString('hex')}`),
|
213 | dq: BigInt(`0x${base64url.decodeToBuffer(jwk.dq).toString('hex')}`),
|
214 | qi: BigInt(`0x${base64url.decodeToBuffer(jwk.qi).toString('hex')}`)
|
215 | }, 'pem', { label: 'RSA PRIVATE KEY' })
|
216 | },
|
217 | public (jwk) {
|
218 | const RSAPublicKey = asn1.get('RSAPublicKey')
|
219 |
|
220 | return RSAPublicKey.encode({
|
221 | version: 0,
|
222 | n: BigInt(`0x${base64url.decodeToBuffer(jwk.n).toString('hex')}`),
|
223 | e: BigInt(`0x${base64url.decodeToBuffer(jwk.e).toString('hex')}`)
|
224 | }, 'pem', { label: 'RSA PUBLIC KEY' })
|
225 | }
|
226 | },
|
227 | EC: {
|
228 | private (jwk) {
|
229 | const ECPrivateKey = asn1.get('ECPrivateKey')
|
230 |
|
231 | return ECPrivateKey.encode({
|
232 | version: 1,
|
233 | privateKey: base64url.decodeToBuffer(jwk.d),
|
234 | parameters: { type: 'namedCurve', value: jwk.crv },
|
235 | publicKey: concatEcPublicKey(jwk.x, jwk.y)
|
236 | }, 'pem', { label: 'EC PRIVATE KEY' })
|
237 | },
|
238 | public (jwk) {
|
239 | const PublicKeyInfo = asn1.get('PublicKeyInfo')
|
240 |
|
241 | return PublicKeyInfo.encode({
|
242 | algorithm: {
|
243 | algorithm: 'ecPublicKey',
|
244 | parameters: { type: 'namedCurve', value: jwk.crv }
|
245 | },
|
246 | publicKey: concatEcPublicKey(jwk.x, jwk.y)
|
247 | }, 'pem', { label: 'PUBLIC KEY' })
|
248 | }
|
249 | },
|
250 | OKP: {
|
251 | private (jwk) {
|
252 | const OneAsymmetricKey = asn1.get('OneAsymmetricKey')
|
253 |
|
254 | const b64 = OneAsymmetricKey.encode({
|
255 | version: 0,
|
256 | privateKey: { privateKey: base64url.decodeToBuffer(jwk.d) },
|
257 | algorithm: { algorithm: jwk.crv }
|
258 | }, 'der')
|
259 |
|
260 |
|
261 | b64.write('04', 12, 1, 'hex')
|
262 |
|
263 | return formatPem(b64.toString('base64'), 'PRIVATE')
|
264 | },
|
265 | public (jwk) {
|
266 | const PublicKeyInfo = asn1.get('PublicKeyInfo')
|
267 |
|
268 | return PublicKeyInfo.encode({
|
269 | algorithm: { algorithm: jwk.crv },
|
270 | publicKey: {
|
271 | unused: 0,
|
272 | data: base64url.decodeToBuffer(jwk.x)
|
273 | }
|
274 | }, 'pem', { label: 'PUBLIC KEY' })
|
275 | }
|
276 | }
|
277 | }
|
278 |
|
279 | module.exports.jwkToPem = (jwk, { calculateMissingRSAPrimes = false } = {}) => {
|
280 | switch (jwk.kty) {
|
281 | case 'EC':
|
282 | if (!EC_CURVES.has(jwk.crv)) {
|
283 | throw new errors.JOSENotSupported(`unsupported EC key curve: ${jwk.crv}`)
|
284 | }
|
285 | break
|
286 | case 'OKP':
|
287 | if (!OKP_CURVES.has(jwk.crv)) {
|
288 | throw new errors.JOSENotSupported(`unsupported OKP key curve: ${jwk.crv}`)
|
289 | }
|
290 | break
|
291 | case 'RSA':
|
292 | break
|
293 | default:
|
294 | throw new errors.JOSENotSupported(`unsupported key type: ${jwk.kty}`)
|
295 | }
|
296 |
|
297 | if (jwk.d) {
|
298 | return jwkToPem[jwk.kty].private(jwk, { calculateMissingRSAPrimes })
|
299 | }
|
300 |
|
301 | return jwkToPem[jwk.kty].public(jwk)
|
302 | }
|