Skip to content

Commit

Permalink
work on import/export of PEM-files
Browse files Browse the repository at this point in the history
  • Loading branch information
Pinta365 committed Apr 23, 2024
1 parent 3b14ecd commit 13230cd
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 117 deletions.
65 changes: 23 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const data = await validateJWT(jwt, false);

**Helper Functions**

- **`generateKey(keyStr: string, optionsOrAlgorithm?: SupportedGenerateKeyAlgorithms | Options): Promise<CryptoKey>`**
- **`generateKey(keyStr: string, optionsOrAlgorithm?: SupportedKeyAlgorithms | Options): Promise<CryptoKey>`**

```javascript
// Generates a HS256 key by default
Expand All @@ -100,10 +100,20 @@ const { privateKey, publicKey } = await generateKeyPair("RS512");
const key = await generateKeyPair({ algorithm: "HS512" });
```

- **`exportKeyFiles(options: exportKeyFilesOptions): Promise<ExportedKeyFiles>`** (Experimental)
- **`exportPEMKey(key: CryptoKey, filePath?: string): Promise<string>`** (Experimental)
- **`importPEMKey(keyDataOrPath: string, algorithm: SupportedKeyPairAlgorithms): Promise<CryptoKey>`** (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**
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cross/jwt",
"version": "0.4.1",
"version": "0.4.2",
"exports": "./mod.ts",

"tasks": {
Expand Down
10 changes: 5 additions & 5 deletions mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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 });
Expand All @@ -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 });
Expand All @@ -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 });
Expand Down
7 changes: 3 additions & 4 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
125 changes: 65 additions & 60 deletions src/cryptokeys.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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.
Expand All @@ -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<CryptoKey>} 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<CryptoKey> {
let algorithm: SupportedGenerateKeyAlgorithms = "HS256";
let algorithm: SupportedKeyAlgorithms = "HS256";
let allowInsecureKeyLengths: boolean = false;

if (typeof optionsOrAlgorithm === "object") {
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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<CryptoKeyPair>} 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<CryptoKeyPair> {
let algorithm: SupportedGenerateKeyPairAlgorithms = "RS256";
let algorithm: SupportedKeyPairAlgorithms = "RS256";
const recommendedModulusLength: number = 2048;
let modulusLength: number = recommendedModulusLength;
let allowInsecureModulusLengths: boolean = false;
Expand Down Expand Up @@ -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<ExportedKeyFiles>} 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<string>} 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<string> {
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.
Expand All @@ -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<CryptoKey> {
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"],
);
}
2 changes: 1 addition & 1 deletion src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 13230cd

Please sign in to comment.