Skip to content

Commit 28e9e68

Browse files
committed
refactor: ensure export functions continue to work with KeyObject inputs
1 parent d4e3960 commit 28e9e68

File tree

5 files changed

+66
-13
lines changed

5 files changed

+66
-13
lines changed

src/key/export.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import keyToJWK from '../lib/key_to_jwk.js'
44
import type * as types from '../types.d.ts'
55

66
/**
7-
* Exports a public {@link !CryptoKey} to a PEM-encoded SPKI string format.
7+
* Exports a public {@link !CryptoKey} or {@link !KeyObject} to a PEM-encoded SPKI string format.
88
*
99
* This function is exported (as a named export) from the main `'jose'` module entry point as well
1010
* as from its subpath export `'jose/key/export'`.
@@ -19,12 +19,12 @@ import type * as types from '../types.d.ts'
1919
*
2020
* @param key Key to export to a PEM-encoded SPKI string format.
2121
*/
22-
export async function exportSPKI(key: types.CryptoKey): Promise<string> {
22+
export async function exportSPKI(key: types.CryptoKey | types.KeyObject): Promise<string> {
2323
return exportPublic(key)
2424
}
2525

2626
/**
27-
* Exports a private {@link !CryptoKey} to a PEM-encoded PKCS8 string format.
27+
* Exports a private {@link !CryptoKey} or {@link !KeyObject} to a PEM-encoded PKCS8 string format.
2828
*
2929
* This function is exported (as a named export) from the main `'jose'` module entry point as well
3030
* as from its subpath export `'jose/key/export'`.
@@ -39,12 +39,12 @@ export async function exportSPKI(key: types.CryptoKey): Promise<string> {
3939
*
4040
* @param key Key to export to a PEM-encoded PKCS8 string format.
4141
*/
42-
export async function exportPKCS8(key: types.CryptoKey): Promise<string> {
42+
export async function exportPKCS8(key: types.CryptoKey | types.KeyObject): Promise<string> {
4343
return exportPrivate(key)
4444
}
4545

4646
/**
47-
* Exports a {@link !CryptoKey} to a JWK.
47+
* Exports a {@link !CryptoKey}, {@link !KeyObject}, or {@link !Uint8Array} to a JWK.
4848
*
4949
* This function is exported (as a named export) from the main `'jose'` module entry point as well
5050
* as from its subpath export `'jose/key/export'`.
@@ -61,6 +61,8 @@ export async function exportPKCS8(key: types.CryptoKey): Promise<string> {
6161
*
6262
* @param key Key to export as JWK.
6363
*/
64-
export async function exportJWK(key: types.CryptoKey | Uint8Array): Promise<types.JWK> {
64+
export async function exportJWK(
65+
key: types.CryptoKey | types.KeyObject | Uint8Array,
66+
): Promise<types.JWK> {
6567
return keyToJWK(key)
6668
}

src/lib/asn1.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type * as types from '../types.d.ts'
22
import invalidKeyInput from './invalid_key_input.js'
33
import { encodeBase64, decodeBase64 } from './base64url.js'
44
import { JOSENotSupported } from '../util/errors.js'
5-
import { isCryptoKey } from './is_key_like.js'
5+
import { isCryptoKey, isKeyObject } from './is_key_like.js'
66

77
import type { PEMImportOptions } from '../key/import.js'
88

@@ -11,13 +11,30 @@ const formatPEM = (b64: string, descriptor: string) => {
1111
return `-----BEGIN ${descriptor}-----\n${newlined}\n-----END ${descriptor}-----`
1212
}
1313

14+
interface ExportOptions {
15+
format: 'pem'
16+
type: 'spki' | 'pkcs8'
17+
}
18+
19+
interface ExtractableKeyObject extends types.KeyObject {
20+
export(arg: ExportOptions): string
21+
}
22+
1423
const genericExport = async (
1524
keyType: 'private' | 'public',
1625
keyFormat: 'spki' | 'pkcs8',
1726
key: unknown,
1827
) => {
28+
if (isKeyObject(key)) {
29+
if (key.type !== keyType) {
30+
throw new TypeError(`key is not a ${keyType} key`)
31+
}
32+
33+
return (key as ExtractableKeyObject).export({ format: 'pem', type: keyFormat })
34+
}
35+
1936
if (!isCryptoKey(key)) {
20-
throw new TypeError(invalidKeyInput(key, 'CryptoKey'))
37+
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'KeyObject'))
2138
}
2239

2340
if (!key.extractable) {

src/lib/key_to_jwk.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
11
import type * as types from '../types.d.ts'
22
import invalidKeyInput from './invalid_key_input.js'
33
import { encode as base64url } from './base64url.js'
4-
import { isCryptoKey } from './is_key_like.js'
4+
import { isCryptoKey, isKeyObject } from './is_key_like.js'
55

6-
export default async (key: unknown): Promise<types.JWK> => {
6+
interface ExportOptions {
7+
format: 'jwk'
8+
}
9+
10+
interface ExtractableKeyObject extends types.KeyObject {
11+
export(arg: ExportOptions): types.JWK
12+
export(): Uint8Array
13+
}
14+
15+
export default async function keyToJWK(key: unknown): Promise<types.JWK> {
16+
if (isKeyObject(key)) {
17+
if (key.type === 'secret') {
18+
key = (key as ExtractableKeyObject).export()
19+
} else {
20+
return (key as ExtractableKeyObject).export({ format: 'jwk' })
21+
}
22+
}
723
if (key instanceof Uint8Array) {
824
return {
925
kty: 'oct',
1026
k: base64url(key),
1127
}
1228
}
1329
if (!isCryptoKey(key)) {
14-
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'Uint8Array'))
30+
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'KeyObject', 'Uint8Array'))
1531
}
1632
if (!key.extractable) {
1733
throw new TypeError('non-extractable CryptoKey cannot be exported as a JWK')

tap/jwk.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type * as jose from '../src/index.js'
66
export default (
77
QUnit: QUnit,
88
lib: typeof jose,
9-
_keys: Pick<typeof jose, 'exportJWK' | 'generateKeyPair' | 'generateSecret' | 'importJWK'>,
9+
keys: Pick<typeof jose, 'exportJWK' | 'generateKeyPair' | 'generateSecret' | 'importJWK'>,
1010
) => {
1111
const { module, test } = QUnit
1212
module('jwk.ts')
@@ -95,6 +95,16 @@ export default (
9595
)
9696
}
9797

98+
if (env.isNode && lib.importJWK !== keys.importJWK) {
99+
const nCrypto = globalThis.process.getBuiltinModule('node:crypto')
100+
t.deepEqual(
101+
await lib.exportJWK(
102+
nCrypto[jwk.d ? 'createPrivateKey' : 'createPublicKey']({ format: 'jwk', key: jwk }),
103+
),
104+
exported,
105+
)
106+
}
107+
98108
t.ok(1)
99109
}
100110

tap/pem.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function normalize(pem: string) {
1010
export default (
1111
QUnit: QUnit,
1212
lib: typeof jose,
13-
_keys: Pick<typeof jose, 'exportJWK' | 'generateKeyPair' | 'generateSecret' | 'importJWK'>,
13+
keys: Pick<typeof jose, 'exportJWK' | 'importJWK'>,
1414
) => {
1515
const { module, test } = QUnit
1616
module('pem.ts')
@@ -148,6 +148,14 @@ export default (
148148

149149
if (!x509) {
150150
t.strictEqual(normalize(await exportFn(k)), normalize(pem))
151+
if (env.isNode && lib.importJWK !== keys.importJWK) {
152+
const nCrypto = globalThis.process.getBuiltinModule('node:crypto')
153+
if (pem.startsWith('-----BEGIN PRIVATE KEY-----')) {
154+
t.strictEqual(normalize(await exportFn(nCrypto.createPrivateKey(pem))), normalize(pem))
155+
} else {
156+
t.strictEqual(normalize(await exportFn(nCrypto.createPublicKey(pem))), normalize(pem))
157+
}
158+
}
151159
} else {
152160
await exportFn(k)
153161
}

0 commit comments

Comments
 (0)