From 13230cd50822215bcbda2c712cd0a9ab27456ef1 Mon Sep 17 00:00:00 2001 From: Pinta365 Date: Tue, 23 Apr 2024 19:09:06 +0200 Subject: [PATCH] work on import/export of PEM-files --- README.md | 65 +++++++++--------------- deno.jsonc | 2 +- mod.test.ts | 10 ++-- mod.ts | 7 ++- src/cryptokeys.ts | 125 ++++++++++++++++++++++++---------------------- src/encoding.ts | 2 +- src/sign.ts | 4 +- src/validate.ts | 4 +- 8 files changed, 102 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index d11d122..74df95d 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ const data = await validateJWT(jwt, false); **Helper Functions** -- **`generateKey(keyStr: string, optionsOrAlgorithm?: SupportedGenerateKeyAlgorithms | Options): Promise`** +- **`generateKey(keyStr: string, optionsOrAlgorithm?: SupportedKeyAlgorithms | Options): Promise`** ```javascript // Generates a HS256 key by default @@ -100,10 +100,20 @@ const { privateKey, publicKey } = await generateKeyPair("RS512"); const key = await generateKeyPair({ algorithm: "HS512" }); ``` -- **`exportKeyFiles(options: exportKeyFilesOptions): Promise`** (Experimental) +- **`exportPEMKey(key: CryptoKey, filePath?: string): Promise`** (Experimental) +- **`importPEMKey(keyDataOrPath: string, algorithm: SupportedKeyPairAlgorithms): Promise`** (Experimental) ```javascript -// Experimental, no examples. +// Experimental. + +// Generate and export RS512 keys in PEM-format. +const { privateKey, publicKey } = await generateKeyPair("RS512"); +await exportPEMKey(privateKey, "./private_key_RS512.pem"); +await exportPEMKey(publicKey, "./public_key_RS512.pem"); + +// Import RS512 keys from PEM-format. +const importedPrivateKey = await importPEMKey("./private_key_RS512.pem", "RS512"); +const importedPublicKey = await importPEMKey("./public_key_RS512.pem", "RS512"); ``` **GenerateKeyOptions Object** @@ -116,7 +126,7 @@ The `GenerateKeyOptions` object can be used to provide flexibility when generati */ interface GenerateKeyOptions { //The HMAC algorithm to use for key generation. Defaults to 'HS256'. - algorithm?: SupportedGenerateKeyAlgorithms; + algorithm?: SupportedKeyAlgorithms; // Use with caution, as shorter keys are less secure. allowInsecureKeyLengths?: boolean; } @@ -132,7 +142,7 @@ The `GenerateKeyPairOptions` object can be used to provide flexibility when gene */ interface GenerateKeyPairOptions { //The algorithm to use for key pair generation. Defaults to 'RS256'. - algorithm?: SupportedGenerateKeyPairAlgorithms; + algorithm?: SupportedKeyPairAlgorithms; // The desired length of the RSA modulus in bits. Larger values offer greater // security, but impact performance. A common default is 2048. modulusLength?: number; @@ -142,35 +152,6 @@ interface GenerateKeyPairOptions { } ``` -The `exportKeyFilesOptions` object can be used to provide flexibility when exporting key pairs: - -```typescript -/** - * Represents the options for the `exportKeyFiles` function. - */ -export interface exportKeyFilesOptions { - /** - * The private key to be exported. - */ - privateKey: CryptoKey; - - /** - * The file path where the PEM-formatted private key will be written. No file will be written if undefined. - */ - privateFile?: string; - - /** - * The public key to be exported. - */ - publicKey: CryptoKey; - - /** - * The file path where the PEM-formatted public key will be written. No file will be written if undefined. - */ - publicFile?: string; -} -``` - **JWTOptions Object** The `JWTOptions` object can be used to provide flexibility when creating JWTs: @@ -320,17 +301,17 @@ const insecureString = "shortString"; const key = await generateKey(insecureString, keyOptions); ``` -Export a key pair to local files. (Experimental, not fully implemented) +Export/import a key pair to and from local files. (Experimental) ```javascript +// Generate and export RS512 keys in PEM-format. const { privateKey, publicKey } = await generateKeyPair("RS512"); -const fileOptions = { - privateKey, - privateFile: "./keys/private_key.pem", - publicKey, - publicFile: "./keys/public_key.pem", -}; -await exportKeyFiles(fileOptions); +await exportPEMKey(privateKey, "./private_key_RS512a.pem"); +await exportPEMKey(publicKey, "./public_key_RS512a.pem"); + +// Import RS512 keys from PEM-format. +const importedPrivateKey = await importPEMKey("./private_key_RS512.pem", "RS512"); +const importedPublicKey = await importPEMKey("./public_key_RS512.pem", "RS512"); ``` ## Issues diff --git a/deno.jsonc b/deno.jsonc index 0f62358..f00294d 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@cross/jwt", - "version": "0.4.1", + "version": "0.4.2", "exports": "./mod.ts", "tasks": { diff --git a/mod.test.ts b/mod.test.ts index 04b602f..2245ef8 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -2,13 +2,13 @@ import { assertEquals, assertRejects } from "@std/assert"; import { test } from "@cross/test"; import { generateKey, generateKeyPair, signJWT, validateJWT } from "./mod.ts"; import { JWTFormatError, JWTValidationError } from "./src/error.ts"; -import type { SupportedGenerateKeyAlgorithms, SupportedGenerateKeyPairAlgorithms } from "./src/cryptokeys.ts"; +import type { SupportedKeyAlgorithms, SupportedKeyPairAlgorithms } from "./src/cryptokeys.ts"; test("signJWT() and validateJWT() with HMAC algorithms", async () => { for (const algorithm of ["HS256", "HS384", "HS512"]) { const secret = `my_strong_secretmy_strong_secretmy_strong_secretmy_strong_secretmy_strong_secretmy_strong_${algorithm}`; - const key = await generateKey(secret, algorithm as SupportedGenerateKeyAlgorithms); + const key = await generateKey(secret, algorithm as SupportedKeyAlgorithms); const payload = { foo: `bar_${algorithm}` }; const jwtString = await signJWT(payload, key, { algorithm }); const decodedPayload = await validateJWT(jwtString, key, { algorithm }); @@ -18,7 +18,7 @@ test("signJWT() and validateJWT() with HMAC algorithms", async () => { test("signJWT() and validateJWT() with RSA algorithms", async () => { for (const algorithm of ["RS256", "RS384", "RS512"]) { - const { privateKey, publicKey } = await generateKeyPair(algorithm as SupportedGenerateKeyPairAlgorithms); + const { privateKey, publicKey } = await generateKeyPair(algorithm as SupportedKeyPairAlgorithms); const payload = { foo: `bar_${algorithm}` }; const jwtString = await signJWT(payload, privateKey, { algorithm }); const decodedPayload = await validateJWT(jwtString, publicKey, { algorithm }); @@ -28,7 +28,7 @@ test("signJWT() and validateJWT() with RSA algorithms", async () => { test("signJWT() and validateJWT() with ECDSA algorithms", async () => { for (const algorithm of ["ES256", "ES384"]) { - const { privateKey, publicKey } = await generateKeyPair(algorithm as SupportedGenerateKeyPairAlgorithms); + const { privateKey, publicKey } = await generateKeyPair(algorithm as SupportedKeyPairAlgorithms); const payload = { foo: `bar_${algorithm}` }; const jwtString = await signJWT(payload, privateKey, { algorithm }); const decodedPayload = await validateJWT(jwtString, publicKey, { algorithm }); @@ -38,7 +38,7 @@ test("signJWT() and validateJWT() with ECDSA algorithms", async () => { test("signJWT() and validateJWT() with RSA-PPS algorithms", async () => { for (const algorithm of ["PS256", "PS384", "PS512"]) { - const { privateKey, publicKey } = await generateKeyPair(algorithm as SupportedGenerateKeyPairAlgorithms); + const { privateKey, publicKey } = await generateKeyPair(algorithm as SupportedKeyPairAlgorithms); const payload = { foo: `bar_${algorithm}` }; const jwtString = await signJWT(payload, privateKey, { algorithm }); const decodedPayload = await validateJWT(jwtString, publicKey, { algorithm }); diff --git a/mod.ts b/mod.ts index 0d082aa..c0796d0 100644 --- a/mod.ts +++ b/mod.ts @@ -1,13 +1,12 @@ // mod.ts export { signJWT } from "./src/sign.ts"; export { validateJWT } from "./src/validate.ts"; -export { exportKeyFiles, generateKey, generateKeyPair } from "./src/cryptokeys.ts"; +export { exportPEMKey, generateKey, generateKeyPair, importPEMKey } from "./src/cryptokeys.ts"; export type { - ExportKeyFilesOptions, GenerateKeyOptions, GenerateKeyPairOptions, - SupportedGenerateKeyAlgorithms, - SupportedGenerateKeyPairAlgorithms, + SupportedKeyAlgorithms, + SupportedKeyPairAlgorithms, } from "./src/cryptokeys.ts"; export type { JWTOptions } from "./src/options.ts"; export type { JWTPayload } from "./src/standardclaims.ts"; diff --git a/src/cryptokeys.ts b/src/cryptokeys.ts index 0964919..45b8502 100644 --- a/src/cryptokeys.ts +++ b/src/cryptokeys.ts @@ -1,18 +1,18 @@ // cryptokeys.ts -import { encodeBase64, textEncode } from "./encoding.ts"; -import { JWTUnsupportedAlgorithmError, JWTValidationError } from "./error.ts"; +import { decodeBase64, encodeBase64, textDecode, textEncode } from "./encoding.ts"; +import { JWTFormatError, JWTUnsupportedAlgorithmError, JWTValidationError } from "./error.ts"; import { algorithmMapping } from "./options.ts"; -import { writeFile } from "@cross/fs/io"; +import { readFile, writeFile } from "@cross/fs/io"; /** * Represents valid algorithm names for key generation using HMAC. */ -export type SupportedGenerateKeyAlgorithms = "HS256" | "HS384" | "HS512"; +export type SupportedKeyAlgorithms = "HS256" | "HS384" | "HS512"; /** * Represents valid algorithm names for generating key pairs using RSA, ECDSA and RSA-PSS. */ -export type SupportedGenerateKeyPairAlgorithms = +export type SupportedKeyPairAlgorithms = | "RS256" | "RS384" | "RS512" @@ -30,7 +30,7 @@ export interface GenerateKeyOptions { /** * The HMAC algorithm to use for key generation. Defaults to 'HS256'. */ - algorithm?: SupportedGenerateKeyAlgorithms; + algorithm?: SupportedKeyAlgorithms; /** * If true, allows generation of keys with lengths shorter than recommended security guidelines. @@ -43,15 +43,15 @@ export interface GenerateKeyOptions { * Generates an HMAC key from a provided secret string. * * @param {string} keyStr - The secret string to use as the key. - * @param {SupportedGenerateKeyAlgorithms | GenerateKeyOptions} optionsOrAlgorithm - The HMAC algorithm to use or GenerateKeyOptions object (default: "HS256"). + * @param {SupportedKeyAlgorithms | GenerateKeyOptions} optionsOrAlgorithm - The HMAC algorithm to use or GenerateKeyOptions object (default: "HS256"). * @returns {Promise} A promise resolving to the generated HMAC key. * @throws {JWTUnsupportedAlgorithmError} If the provided algorithm is not supported. */ export async function generateKey( keyStr: string, - optionsOrAlgorithm: SupportedGenerateKeyAlgorithms | GenerateKeyOptions = "HS256", + optionsOrAlgorithm: SupportedKeyAlgorithms | GenerateKeyOptions = "HS256", ): Promise { - let algorithm: SupportedGenerateKeyAlgorithms = "HS256"; + let algorithm: SupportedKeyAlgorithms = "HS256"; let allowInsecureKeyLengths: boolean = false; if (typeof optionsOrAlgorithm === "object") { @@ -71,7 +71,7 @@ export async function generateKey( HS256: 32, HS384: 48, HS512: 64, - }[algorithm as SupportedGenerateKeyAlgorithms]; + }[algorithm as SupportedKeyAlgorithms]; if (!allowInsecureKeyLengths && encodedKey.byteLength < minimumLength) { throw new JWTValidationError( @@ -96,7 +96,7 @@ export interface GenerateKeyPairOptions { /** * The algorithm to use for key pair generation. Defaults to 'RS256'. */ - algorithm?: SupportedGenerateKeyPairAlgorithms; + algorithm?: SupportedKeyPairAlgorithms; /** * The desired length of the RSA modulus in bits. Larger values offer greater security, @@ -114,14 +114,14 @@ export interface GenerateKeyPairOptions { /** * Generates an RSA or ECDSA key pair (public and private key). * - * @param {SupportedGenerateKeyPairAlgorithms | GenerateKeyPairOptions} optionsOrAlgorithm - The algorithm to use or GenerateKeyPairOptions object (default: "RS256"). + * @param {SupportedKeyPairAlgorithms | GenerateKeyPairOptions} optionsOrAlgorithm - The algorithm to use or GenerateKeyPairOptions object (default: "RS256"). * @returns {Promise} A promise resolving to the generated key pair. * @throws {JWTUnsupportedAlgorithmError} If the provided algorithm is not supported. */ export async function generateKeyPair( - optionsOrAlgorithm: SupportedGenerateKeyPairAlgorithms | GenerateKeyPairOptions = "RS256", + optionsOrAlgorithm: SupportedKeyPairAlgorithms | GenerateKeyPairOptions = "RS256", ): Promise { - let algorithm: SupportedGenerateKeyPairAlgorithms = "RS256"; + let algorithm: SupportedKeyPairAlgorithms = "RS256"; const recommendedModulusLength: number = 2048; let modulusLength: number = recommendedModulusLength; let allowInsecureModulusLengths: boolean = false; @@ -169,62 +169,31 @@ export async function generateKeyPair( } /** - * Represents the options for the `exportKeyFiles` function. - */ -export interface ExportKeyFilesOptions { - /** - * The private key to be exported. - */ - privateKey: CryptoKey; - - /** - * The file path where the PEM-formatted private key will be written. No file will be written if undefined. - */ - privateFile?: string; - - /** - * The public key to be exported. - */ - publicKey: CryptoKey; - - /** - * The file path where the PEM-formatted public key will be written. No file will be written if undefined. - */ - publicFile?: string; -} - -/** - * Exports a key pair to PEM-formatted files. + * Exports a CryptoKey to PEM format and optionally writes it to a file. * - * @param {ExportKeyFilesOptions} options - Options for the key export operation. - * @returns {Promise} A promise that resolves when the files have been written. + * @param key - The CryptoKey to export. + * @param filePath - (Optional) Path to write the PEM-formatted key to. + * @returns {Promise} A Promise resolving to the PEM-formatted key string. */ -export async function exportKeyFiles( - options: ExportKeyFilesOptions, -): Promise<{ privateKey: string; publicKey: string }> { - const { privateKey, publicKey, privateFile, publicFile } = options; - - // todo: implement checks for privateKey, publicKey, privateFile, publicFile +export async function exportPEMKey(key: CryptoKey, filePath?: string): Promise { + const keyType = key.type; - const privateKeyExport = await window.crypto.subtle.exportKey("pkcs8", privateKey); - const publicKeyExport = await window.crypto.subtle.exportKey("spki", publicKey); + const exportedKey = await window.crypto.subtle.exportKey( + keyType === "private" ? "pkcs8" : "spki", + key, + ); - const privateKeyPem = formatAsPem("PRIVATE KEY", privateKeyExport); - const publicKeyPem = formatAsPem("PUBLIC KEY", publicKeyExport); + const pemKey = formatAsPem(keyType === "private" ? "PRIVATE KEY" : "PUBLIC KEY", exportedKey); - if (privateFile) { - await writeFile(privateFile, privateKeyPem); + if (filePath) { + await writeFile(filePath, pemKey); } - if (publicFile) { - await writeFile(publicFile, publicKeyPem); - } - - return { privateKey: privateKeyPem, publicKey: publicKeyPem }; + return pemKey; } /** - * Formats a key as a PEM (Privacy Enhanced Mail) block. + * Formats a key as a PEM block. * * @param {string} header - The header type for the PEM block (e.g., "PUBLIC KEY", "RSA PRIVATE KEY"). * @param {ArrayBuffer} keyData - An ArrayBuffer containing the binary key data. @@ -235,3 +204,39 @@ function formatAsPem(header: string, keyData: ArrayBuffer) { const lines = base64Key.match(/.{1,64}/g) || []; return `-----BEGIN ${header}-----\n${lines.join("\n")}\n-----END ${header}-----`; } + +export async function importPEMKey(keyDataOrPath: string, algorithm: SupportedKeyPairAlgorithms): Promise { + if (algorithm === "none" || !algorithmMapping[algorithm]) { + throw new JWTUnsupportedAlgorithmError("Unsupported key algorithm"); + } + + const algo = algorithmMapping[algorithm] as { name: string; hash?: string; namedCurve?: string }; + + let pemString: string; + + if (keyDataOrPath.includes("-----BEGIN")) { + pemString = keyDataOrPath; + } else { + const fileBuffer = await readFile(keyDataOrPath); + pemString = textDecode(fileBuffer); + } + + if (!pemString.includes("-----BEGIN")) { + throw new JWTFormatError("Wrong format. Not PEM?"); + } + + const keyType = pemString.includes("PRIVATE KEY") ? "private" : "public"; + + const pemHeader = `-----BEGIN ${keyType} KEY-----`; + const pemFooter = `-----END ${keyType} KEY-----`; + const pemContents = pemString.substring(pemHeader.length, pemString.length - pemFooter.length); + const binaryDer = decodeBase64(pemContents); + + return await window.crypto.subtle.importKey( + keyType === "private" ? "pkcs8" : "spki", + binaryDer, + { ...algo }, + false, + keyType === "private" ? ["sign"] : ["verify"], + ); +} diff --git a/src/encoding.ts b/src/encoding.ts index c276a42..c675d71 100644 --- a/src/encoding.ts +++ b/src/encoding.ts @@ -3,7 +3,7 @@ * Re-exports base64url encoding and decoding functions JSR standard library. */ export { decodeBase64Url, encodeBase64Url } from "@std/encoding/base64url"; -export { encodeBase64 } from "@std/encoding/base64"; +export { decodeBase64, encodeBase64 } from "@std/encoding/base64"; /** * Encodes a string into a Uint8Array representation using the TextEncoder API. diff --git a/src/sign.ts b/src/sign.ts index 3f8aac6..95667af 100644 --- a/src/sign.ts +++ b/src/sign.ts @@ -11,7 +11,7 @@ import { algorithmMapping, defaultOptions } from "./options.ts"; import type { JWTOptions } from "./options.ts"; import { encodeBase64Url, textEncode } from "./encoding.ts"; import { generateKey } from "./cryptokeys.ts"; -import type { SupportedGenerateKeyAlgorithms } from "./cryptokeys.ts"; +import type { SupportedKeyAlgorithms } from "./cryptokeys.ts"; import { signWithRSA } from "./sign-verify/rsa.ts"; import { signWithHMAC } from "./sign-verify/hmac.ts"; @@ -104,7 +104,7 @@ async function processKey( return { algorithm, key }; } else { if (typeof key === "string" && options?.algorithm) { - key = await generateKey(key, options?.algorithm as SupportedGenerateKeyAlgorithms); + key = await generateKey(key, options?.algorithm as SupportedKeyAlgorithms); } else if (typeof key === "string") { key = await generateKey(key); } diff --git a/src/validate.ts b/src/validate.ts index 548b072..52e1e25 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -14,7 +14,7 @@ import { algorithmMapping, defaultOptions } from "./options.ts"; import type { JWTOptions } from "./options.ts"; import { decodeBase64Url, textDecode } from "./encoding.ts"; import { generateKey } from "./cryptokeys.ts"; -import type { SupportedGenerateKeyAlgorithms } from "./cryptokeys.ts"; +import type { SupportedKeyAlgorithms } from "./cryptokeys.ts"; import { verifyWithRSA } from "./sign-verify/rsa.ts"; import { verifyWithHMAC } from "./sign-verify/hmac.ts"; @@ -152,7 +152,7 @@ async function processKey( return { algorithm, key }; } else { if (typeof key === "string" && options?.algorithm) { - key = await generateKey(key, options?.algorithm as SupportedGenerateKeyAlgorithms); + key = await generateKey(key, options?.algorithm as SupportedKeyAlgorithms); } else if (typeof key === "string") { key = await generateKey(key); }