Skip to content

Commit

Permalink
feat: add key.toPEM() export function with optional encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Apr 23, 2019
1 parent 2dbd3ed commit 1159b0d
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 6 deletions.
39 changes: 39 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ I can continue maintaining it and adding new features carefree. You may also don
- [key.secret](#keysecret)
- [key.algorithms([operation])](#keyalgorithmsoperation)
- [key.toJWK([private])](#keytojwkprivate)
- [key.toPEM([private[, encoding]])](#keytopemprivate-encoding)
- JWK.importKey
- [JWK.importKey(key[, options]) asymmetric key import](#jwkimportkeykey-options-asymmetric-key-import)
- [JWK.importKey(secret[, options]) secret key import](#jwkimportkeysecret-options-secret-key-import)
Expand Down Expand Up @@ -245,6 +246,44 @@ key.toJWK(true)

---

#### `key.toPEM([private[, encoding]])`

Exports an asymmetric key as a PEM string with specified encoding and optional encryption for private keys.

- `private`: `<boolean>` When true exports keys as private. **Default:** 'false'
- `encoding`: `<Object>` See below
- Returns: `<string>`

For public key export, the following encoding options can be used:

- `type`: `<string>` Must be one of 'pkcs1' (RSA only) or 'spki'. **Default:** 'spki'


For private key export, the following encoding options can be used:

- `type`: `<string>` Must be one of 'pkcs1' (RSA only), 'pkcs8' or 'sec1' (EC only). **Default:** 'pkcs8'
- `cipher`: `<string>` If specified, the private key will be encrypted with the given cipher and
passphrase using PKCS#5 v2.0 password based encryption. **Default**: 'undefined' (no encryption)
- `passphrase`: `<string>` &vert; `<Buffer>` The passphrase to use for encryption. **Default**: 'undefined' (no encryption)

<details>
<summary><em><strong>Example</strong></em> (Click to expand)</summary>

```js
const { JWK: { generateSync } } = require('@panva/jose')

const key = generateSync('RSA', 2048)
key.toPEM()
// -----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEATPpxgDY7XU8cYX9Rb44xxXDO6zP\nzELVOHTcutCiXS9HZvUrZsnG7U/SPj0AT1hsH6lTUK4uFr7GG7KWgsf1Aw==\n-----END PUBLIC KEY-----\n
key.toPEM(true)
// -----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUdAzlvX4i+RJS2BL\nQrqRj/ndTbpqugX61Ih9X+rvAcShRANCAAQBM+nGANjtdTxxhf1FvjjHFcM7rM/M\nQtU4dNy60KJdL0dm9StmycbtT9I+PQBPWGwfqVNQri4WvsYbspaCx/UD\n-----END PRIVATE KEY-----\n
key.toPEM(true, { passphrase: 'super-strong', cipher: 'aes-256-cbc' })
// -----BEGIN ENCRYPTED PRIVATE KEY-----\nMIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAjjeqsgorjSqwICCAAw\nDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEJFcyG1ZBe2FZuvXIqiRFUcEgZD5\nWzt2XIUGIEZQIUUpJ1naaIFKiZvBcFAXhqG5KJ6PgaohgcmRUK8OZTA9Ome+uXB+\n9PLLfKscOsyr0gkd45gYYNRDLYwbQSqDQ4g8pHrCVjR+R3mh1nk8jIkOxSppwzmF\n7aoCmnQo7oXRy1+kRZL7OfwAD5gAXnsIA42D9RgOG1XIiBYTvAITcFVX0UPh0zM=\n-----END ENCRYPTED PRIVATE KEY-----\n
```
</details>

---

#### `JWK.importKey(key[, options])` asymmetric key import

Imports an asymmetric private or public key. Supports importing JWK formatted keys (private, public,
Expand Down
8 changes: 4 additions & 4 deletions lib/help/key_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ const jwkToPem = {
dp: base64url.decodeToBuffer(jwk.dp),
dq: base64url.decodeToBuffer(jwk.dq),
qi: base64url.decodeToBuffer(jwk.qi)
}, 'pem', { label: 'RSA PRIVATE KEY' }).toString('base64')
}, 'pem', { label: 'RSA PRIVATE KEY' })
},
public (jwk) {
const RSAPublicKey = asn1.get('RSAPublicKey')
Expand All @@ -207,7 +207,7 @@ const jwkToPem = {
version: 0,
n: base64url.decodeToBuffer(jwk.n),
e: base64url.decodeToBuffer(jwk.e)
}, 'pem', { label: 'RSA PUBLIC KEY' }).toString('base64')
}, 'pem', { label: 'RSA PUBLIC KEY' })
}
},
EC: {
Expand All @@ -222,7 +222,7 @@ const jwkToPem = {
value: crvToOid.get(jwk.crv)
},
publicKey: concatEcPublicKey(jwk.x, jwk.y)
}, 'pem', { label: 'EC PRIVATE KEY' }).toString('base64')
}, 'pem', { label: 'EC PRIVATE KEY' })
},
public (jwk) {
const PublicKeyInfo = asn1.get('PublicKeyInfo')
Expand All @@ -233,7 +233,7 @@ const jwkToPem = {
parameters: crvToOidBuf.get(jwk.crv)
},
publicKey: concatEcPublicKey(jwk.x, jwk.y)
}, 'pem', { label: 'PUBLIC KEY' }).toString('base64')
}, 'pem', { label: 'PUBLIC KEY' })
}
},
OKP: {
Expand Down
7 changes: 7 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ type keyObjectTypes = asymmetricKeyObjectTypes | 'secret'

export namespace JWK {

interface pemEncodingOptions {
type?: string
cipher?: string
passphrase?: string
}

class Key {
kty: keyType
type: keyObjectTypes
Expand All @@ -28,6 +34,7 @@ export namespace JWK {
kid: string
thumbprint: string

toPEM(private?: boolean, encoding?: pemEncodingOptions): string

algorithms(operation?: keyOperation): Set<string>
}
Expand Down
4 changes: 2 additions & 2 deletions lib/jwa/ecdh/derive.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ const computeSecret = ({ crv, d }, { x, y = '' }) => {
const exchange = createECDH(curve)

exchange.setPrivateKey(base64url.decodeToBuffer(d))
let secret = exchange.computeSecret(pubToBuffer(x, y))
return secret

return exchange.computeSecret(pubToBuffer(x, y))
}

const concat = (key, length, value) => {
Expand Down
31 changes: 31 additions & 0 deletions lib/jwk/key/base.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { createPublicKey } = require('crypto')

const { keyObjectToJWK } = require('../../help/key_utils')
const { THUMBPRINT_MATERIAL, PUBLIC_MEMBERS, PRIVATE_MEMBERS, JWK_MEMBERS } = require('../../help/symbols')
const { KEYOBJECT } = require('../../help/symbols')
Expand Down Expand Up @@ -54,6 +56,35 @@ class Key {
})
}

toPEM (priv = false, encoding = {}) {
if (this.secret) {
throw new TypeError('symmetric keys cannot be exported as PEM')
}

if (priv && this.public === true) {
throw new TypeError('public key cannot be exported as private')
}

const { type = priv ? 'pkcs8' : 'spki', cipher, passphrase } = encoding

let keyObject = this[KEYOBJECT]

if (!priv) {
if (this.private) {
keyObject = createPublicKey(keyObject)
}
if (cipher || passphrase) {
throw new TypeError('cipher and passphrase can only be applied when exporting private keys')
}
}

if (priv) {
return keyObject.export({ format: 'pem', type, cipher, passphrase })
}

return keyObject.export({ format: 'pem', type })
}

toJWK (priv = false) {
if (priv && this.public === true) {
throw new TypeError('public key cannot be exported as private')
Expand Down
34 changes: 34 additions & 0 deletions test/cookbook/jwk.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,66 @@ const { JWK: { importKey }, JWKS: { KeyStore } } = require('../..')
test('public EC', t => {
const jwk = recipes.get('3.1')
const key = importKey(jwk)
t.true(key.toPEM().includes('BEGIN PUBLIC KEY'))
t.deepEqual(key.toJWK(), jwk)
t.deepEqual(key.toJWK(false), jwk)
t.throws(() => {
key.toJWK(true)
}, { instanceOf: TypeError, message: 'public key cannot be exported as private' })
t.throws(() => {
key.toPEM(false, { cipher: 'aes-256-cbc' })
}, { instanceOf: TypeError, message: 'cipher and passphrase can only be applied when exporting private keys' })
t.throws(() => {
key.toPEM(false, { passphrase: 'top secret' })
}, { instanceOf: TypeError, message: 'cipher and passphrase can only be applied when exporting private keys' })
t.throws(() => {
key.toPEM(true)
}, { instanceOf: TypeError, message: 'public key cannot be exported as private' })
})

test('private EC', t => {
const jwk = recipes.get('3.2')
const key = importKey(jwk)
t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY'))
t.true(key.toPEM(true, { type: 'sec1' }).includes('BEGIN EC PRIVATE KEY'))
t.true(key.toPEM(true, { type: 'sec1', cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('ENCRYPTED'))
t.true(key.toPEM(true).includes('BEGIN PRIVATE KEY'))
t.true(key.toPEM().includes('BEGIN PUBLIC KEY'))
t.deepEqual(key.toJWK(true), jwk)
const { d, ...pub } = jwk
t.deepEqual(key.toJWK(), pub)
t.deepEqual(key.toJWK(false), pub)
t.throws(() => {
key.toPEM(false, { cipher: 'aes-256-cbc' })
}, { instanceOf: TypeError, message: 'cipher and passphrase can only be applied when exporting private keys' })
t.throws(() => {
key.toPEM(false, { passphrase: 'top secret' })
}, { instanceOf: TypeError, message: 'cipher and passphrase can only be applied when exporting private keys' })
})

test('public RSA', t => {
const jwk = recipes.get('3.3')
const key = importKey(jwk)
t.true(key.toPEM().includes('BEGIN PUBLIC KEY'))
t.deepEqual(key.toJWK(), jwk)
t.deepEqual(key.toJWK(false), jwk)
t.throws(() => {
key.toJWK(true)
}, { instanceOf: TypeError, message: 'public key cannot be exported as private' })
t.throws(() => {
key.toPEM(true)
}, { instanceOf: TypeError, message: 'public key cannot be exported as private' })
})

test('private RSA', t => {
const jwk = recipes.get('3.4')
const key = importKey(jwk)
t.true(key.toPEM(true, { type: 'pkcs1' }).includes('BEGIN RSA PRIVATE KEY'))
t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret', type: 'pkcs1' }).includes('ENCRYPTED'))
t.true(key.toPEM(true, { type: 'pkcs1', cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN RSA PRIVATE KEY'))
t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY'))
t.true(key.toPEM(true).includes('BEGIN PRIVATE KEY'))
t.true(key.toPEM().includes('BEGIN PUBLIC KEY'))
t.deepEqual(key.toJWK(true), jwk)
const { d, dp, dq, p, q, qi, ...pub } = jwk
t.deepEqual(key.toJWK(), pub)
Expand All @@ -45,6 +76,9 @@ test('private RSA', t => {
test('oct (1/2)', t => {
const jwk = recipes.get('3.5')
const key = importKey(jwk)
t.throws(() => {
key.toPEM()
}, { instanceOf: TypeError, message: 'symmetric keys cannot be exported as PEM' })
t.deepEqual(key.toJWK(true), jwk)
const { k, ...pub } = jwk
t.deepEqual(key.toJWK(), pub)
Expand Down

0 comments on commit 1159b0d

Please sign in to comment.