From 49faf97c9beb976cc0512545d64fef277e3e642c Mon Sep 17 00:00:00 2001 From: Jackson Roberts Date: Wed, 25 Feb 2026 22:14:19 -0600 Subject: [PATCH] switch to PKCS#8 key format for Bun compatibility --- src/constants.ts | 56 +++++++++----------------------- utils/convertKeys.ts | 65 ++++++++++++++++++++++++++++++++++++++ utils/genPrivateKeys.ts | 14 +++++--- utils/genProductionKeys.ts | 27 +++++++++------- 4 files changed, 105 insertions(+), 57 deletions(-) create mode 100644 utils/convertKeys.ts diff --git a/src/constants.ts b/src/constants.ts index 6a5767b..9c91465 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,4 @@ import { readFileSync } from "fs"; -import crypto from "crypto"; import { PrismaPg } from "@prisma/adapter-pg"; import { Prisma, PrismaClient } from "../generated/prisma/client"; import * as msal from "@azure/msal-node"; @@ -29,46 +28,21 @@ const isProduction = process.env.NODE_ENV === "production"; const readKeyFile = (path: string) => readFileSync(path).toString(); -/** - * Convert a PKCS#1 PEM key to PKCS#8 PEM format. - * The compiled Bun binary on Ubuntu uses an OpenSSL that doesn't auto-detect PKCS#1 format, - * so we normalize all keys to PKCS#8 at load time. - */ -const toPkcs8Private = (pem: string) => - crypto - .createPrivateKey({ key: pem, format: "pem", type: "pkcs1" }) - .export({ type: "pkcs8", format: "pem" }) as string; - -const toPkcs8Public = (pem: string) => - crypto - .createPublicKey({ key: pem, format: "pem", type: "pkcs1" }) - .export({ type: "spki", format: "pem" }) as string; - -export const accessTokenPrivateKey = toPkcs8Private( - isProduction - ? process.env.ACCESS_TOKEN_PRIVATE_KEY! - : readKeyFile(`.accessToken.key`), -); -export const refreshTokenPrivateKey = toPkcs8Private( - isProduction - ? process.env.REFRESH_TOKEN_PRIVATE_KEY! - : readKeyFile(`.refreshToken.key`), -); -export const permissionsPrivateKey = toPkcs8Private( - isProduction - ? process.env.PERMISSIONS_PRIVATE_KEY! - : readKeyFile(`.permissions.key`), -); -export const secureValuesPrivateKey = toPkcs8Private( - isProduction - ? process.env.SECURE_VALUES_PRIVATE_KEY! - : readKeyFile(`.secureValues.key`), -); -export const secureValuesPublicKey = toPkcs8Public( - isProduction - ? process.env.SECURE_VALUES_PUBLIC_KEY! - : readKeyFile(`public-keys/.secureValues.pub`), -); +export const accessTokenPrivateKey = isProduction + ? process.env.ACCESS_TOKEN_PRIVATE_KEY! + : readKeyFile(`.accessToken.key`); +export const refreshTokenPrivateKey = isProduction + ? process.env.REFRESH_TOKEN_PRIVATE_KEY! + : readKeyFile(`.refreshToken.key`); +export const permissionsPrivateKey = isProduction + ? process.env.PERMISSIONS_PRIVATE_KEY! + : readKeyFile(`.permissions.key`); +export const secureValuesPrivateKey = isProduction + ? process.env.SECURE_VALUES_PRIVATE_KEY! + : readKeyFile(`.secureValues.key`); +export const secureValuesPublicKey = isProduction + ? process.env.SECURE_VALUES_PUBLIC_KEY! + : readKeyFile(`public-keys/.secureValues.pub`); // Microsoft Auth Constants const msalConfig: msal.Configuration = { diff --git a/utils/convertKeys.ts b/utils/convertKeys.ts new file mode 100644 index 0000000..a997991 --- /dev/null +++ b/utils/convertKeys.ts @@ -0,0 +1,65 @@ +import { execSync } from "child_process"; + +const kubeconfig = "/Users/jroberts/projects/K8S-QuickDeploy/k8s.yaml"; + +function getKey(name: string): string { + const b64 = execSync( + `KUBECONFIG=${kubeconfig} kubectl get secret optima-keys-secret -n optima -o jsonpath="{.data.${name}}"`, + ) + .toString() + .trim(); + return Buffer.from(b64, "base64").toString("utf-8"); +} + +const privKeys = [ + "ACCESS_TOKEN_PRIVATE_KEY", + "REFRESH_TOKEN_PRIVATE_KEY", + "PERMISSIONS_PRIVATE_KEY", + "SECURE_VALUES_PRIVATE_KEY", +]; + +const converted: Record = {}; + +// Use openssl CLI to convert PKCS#1 to PKCS#8 (Bun's crypto has issues with some keys) +for (const k of privKeys) { + const pem = getKey(k); + const pkcs8 = execSync("openssl pkey -in /dev/stdin", { + input: pem, + }).toString(); + converted[k] = pkcs8; + console.log(`${k}: converted to PKCS#8 ✅`); +} + +const pubPem = getKey("SECURE_VALUES_PUBLIC_KEY"); +const spki = execSync("openssl rsa -RSAPublicKey_in -pubout -in /dev/stdin", { + input: pubPem, +}).toString(); +converted["SECURE_VALUES_PUBLIC_KEY"] = spki; +console.log("SECURE_VALUES_PUBLIC_KEY: converted to SPKI ✅"); + +// Generate kubectl command to recreate the secret with PKCS#8 keys +const args = Object.entries(converted) + .map(([k, v]) => `--from-literal=${k}='${v}'`) + .join(" \\\n "); + +console.log("\n--- Delete and recreate secret with PKCS#8 keys ---\n"); +console.log( + `KUBECONFIG=${kubeconfig} kubectl delete secret optima-keys-secret -n optima`, +); +console.log( + `KUBECONFIG=${kubeconfig} kubectl create secret generic optima-keys-secret -n optima \\\n ${args}`, +); + +// Actually do it +console.log("\nApplying..."); +execSync( + `KUBECONFIG=${kubeconfig} kubectl delete secret optima-keys-secret -n optima`, +); + +const literals = Object.entries(converted).map( + ([k, v]) => `--from-literal=${k}=${v}`, +); +const cmd = `KUBECONFIG=${kubeconfig} kubectl create secret generic optima-keys-secret -n optima ${literals.join(" ")}`; +execSync(cmd); + +console.log("Secret recreated with PKCS#8 keys ✅"); diff --git a/utils/genPrivateKeys.ts b/utils/genPrivateKeys.ts index d28f241..9f8ccc2 100644 --- a/utils/genPrivateKeys.ts +++ b/utils/genPrivateKeys.ts @@ -1,9 +1,9 @@ -import keypair from "keypair"; +import crypto from "crypto"; console.log(` Generating Private Keys ----------------- -This script will go through and genrate all the keys necessary for running the Credential Manager API locally. +This script will go through and generate all the keys necessary for running the Credential Manager API locally. This process might take several minutes. -----------------`); @@ -42,9 +42,13 @@ await Promise.all( if (!privExists || !pubExists) { // Always regenerate both files together to ensure the key pair matches console.log(`Generating '${v}' and '${pubPath}'...`); - const keys = keypair({ bits: 4096 }); - await Bun.write(v, keys.private); - await Bun.write(pubPath, keys.public); + const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", { + modulusLength: 4096, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, + publicKeyEncoding: { type: "spki", format: "pem" }, + }); + await Bun.write(v, privateKey); + await Bun.write(pubPath, publicKey); } return; }), diff --git a/utils/genProductionKeys.ts b/utils/genProductionKeys.ts index 2112591..f732072 100644 --- a/utils/genProductionKeys.ts +++ b/utils/genProductionKeys.ts @@ -1,4 +1,4 @@ -import keypair from "keypair"; +import crypto from "crypto"; import { mkdirSync } from "fs"; const outputDir = "production-keys"; @@ -19,14 +19,18 @@ const generatedKeys: Record = {}; for (const name of keyFiles) { console.log(`Generating '${name}' key pair (4096-bit RSA)...`); - const keys = keypair({ bits: 4096 }); - generatedKeys[name] = keys; + const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", { + modulusLength: 4096, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, + publicKeyEncoding: { type: "spki", format: "pem" }, + }); + generatedKeys[name] = { private: privateKey, public: publicKey }; const privPath = `${outputDir}/${name}.key`; const pubPath = `${outputDir}/${name}.pub`; - await Bun.write(privPath, keys.private); - await Bun.write(pubPath, keys.public); + await Bun.write(privPath, privateKey); + await Bun.write(pubPath, publicKey); console.log(` ✔ ${privPath}`); console.log(` ✔ ${pubPath}`); @@ -38,14 +42,15 @@ const toBase64 = (str: string) => Buffer.from(str).toString("base64"); const secretYaml = `apiVersion: v1 kind: Secret metadata: - name: optima-keys + name: optima-keys-secret + namespace: optima type: Opaque data: - accessToken.key: ${toBase64(generatedKeys["accessToken"].private)} - refreshToken.key: ${toBase64(generatedKeys["refreshToken"].private)} - permissions.key: ${toBase64(generatedKeys["permissions"].private)} - secureValues.key: ${toBase64(generatedKeys["secureValues"].private)} - secureValues.pub: ${toBase64(generatedKeys["secureValues"].public)} + ACCESS_TOKEN_PRIVATE_KEY: ${toBase64(generatedKeys["accessToken"].private)} + REFRESH_TOKEN_PRIVATE_KEY: ${toBase64(generatedKeys["refreshToken"].private)} + PERMISSIONS_PRIVATE_KEY: ${toBase64(generatedKeys["permissions"].private)} + SECURE_VALUES_PRIVATE_KEY: ${toBase64(generatedKeys["secureValues"].private)} + SECURE_VALUES_PUBLIC_KEY: ${toBase64(generatedKeys["secureValues"].public)} `; const secretPath = `${outputDir}/optima-keys-secret.yaml`;