UNPKG

jwt-token-pair-generator

Version:

A secure RSA token pair generator CLI and library for generating and storing RSA keys for JWT

271 lines (270 loc) 14.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SecureKeyGenerator = void 0; const fs_1 = __importDefault(require("fs")); const crypto_1 = __importDefault(require("crypto")); const path_1 = __importDefault(require("path")); const dotenv_1 = __importDefault(require("dotenv")); const logger_1 = require("./logger"); dotenv_1.default.config(); /** * SecureKeyGenerator generates RSA key pairs for tokens and stores them securely. */ class SecureKeyGenerator { constructor(config = {}) { this.SECURE_FILE_PERMISSIONS = config.filePermissions || 0o644; if (config.keyDirectory) { this.KEY_DIRECTORY = config.keyDirectory || "secure-keys"; this.keyPath = path_1.default.join(process.cwd(), this.KEY_DIRECTORY); } this.envFileName = config.envFileName || ".env"; this.modulusLength = config.modulusLength || 2048; const logLevel = config.logLevel; this.logger = (0, logger_1.logger)(logLevel); this.logger.info(`Initialized SecureKeyGenerator with minimal logging. Use '--log all' for detailed logs.`); this.logger.info(`Initialized SecureKeyGenerator with keyDirectory: '${this.KEY_DIRECTORY}', envFileName: '${this.envFileName}', modulusLength: ${this.modulusLength}, filePermissions: ${this.SECURE_FILE_PERMISSIONS.toString(8)}`); } /** * Generates an RSA key pair. * @param keyType - Type of key ("access" or "refresh"). * @returns The generated KeyPair. */ generateRSAKeyPair(keyType) { this.logger.info(`Starting generation of RSA key pair for '${keyType}' token.`); try { const options = { modulusLength: this.modulusLength, publicKeyEncoding: { type: "spki", format: "pem", }, privateKeyEncoding: { type: "pkcs8", format: "pem", }, }; this.logger.info(`Generating unencrypted RSA keys for '${keyType}' token with modulusLength: ${this.modulusLength}.`); const { publicKey, privateKey } = crypto_1.default.generateKeyPairSync("rsa", options); this.logger.info(`RSA keys generated for '${keyType}' token. Proceeding with validation.`); this.validateKeyPair(publicKey, privateKey); this.logger.info(`RSA key pair for '${keyType}' token validated successfully.`); return { publicKey, privateKey }; } catch (error) { throw new Error(`${keyType} key generation failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Generates a secure random passphrase. * @returns A hex string representing the passphrase. */ generateSecurePassphrase() { const passphrase = crypto_1.default.randomBytes(32).toString("hex"); // Note: Avoid logging the actual passphrase in production logs. this.logger.debug("Generated secure passphrase."); return passphrase; } /** * Validates the generated key pair by signing and verifying a test message. * @param publicKey - The public key. * @param privateKey - The private key. */ validateKeyPair(publicKey, privateKey) { this.logger.info("Validating generated key pair."); try { const testMessage = crypto_1.default.randomBytes(32); // Sign the test message using the unencrypted private key const signature = crypto_1.default.sign("sha256", testMessage, privateKey); // Verify the signature using the public key const isValid = crypto_1.default.verify("sha256", testMessage, publicKey, signature); if (!isValid) { throw new Error("Key pair validation failed"); } this.logger.info("Key pair validation succeeded."); } catch (error) { throw new Error(`Key validation failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Creates the directory for storing keys with secure permissions. */ createSecureKeyDirectory() { return __awaiter(this, void 0, void 0, function* () { this.logger.info(`Ensuring key directory exists at '${this.keyPath}'.`); try { if (!this.keyPath) return; if (!fs_1.default.existsSync(this.keyPath)) { this.logger.info(`Key directory does not exist. Creating directory at '${this.keyPath}'.`); yield fs_1.default.promises.mkdir(this.keyPath, { recursive: true }); this.logger.info(`Key directory created at '${this.keyPath}'.`); } else { this.logger.info(`Key directory already exists at '${this.keyPath}'.`); } try { yield fs_1.default.promises.chmod(this.keyPath, 0o700); this.logger.info(`Set restrictive permissions (700) on key directory '${this.keyPath}'.`); } catch (error) { this.logger.warn("Could not set restrictive directory permissions. Using default permissions."); } } catch (error) { throw new Error(`Failed to create secure directory: ${error instanceof Error ? error.message : "Unknown error"}`); } }); } /** * Loads existing environment variables from the configured env file. * @returns An object containing key/value pairs from the env file. */ loadExistingEnv() { return __awaiter(this, void 0, void 0, function* () { this.logger.info(`Loading existing environment variables from '${this.envFileName}'.`); try { if (fs_1.default.existsSync(this.envFileName)) { const envContent = yield fs_1.default.promises.readFile(this.envFileName, "utf8"); const env = {}; envContent.split("\n").forEach((line) => { const trimmedLine = line.trim(); if (trimmedLine && !trimmedLine.startsWith("#")) { const [key, ...valueParts] = trimmedLine.split("="); if (key) { env[key.trim()] = valueParts.join("=").replace(/^"|"$/g, "").trim(); } } }); this.logger.info(`Loaded existing environment variables from '${this.envFileName}'.`); return env; } this.logger.info(`No existing env file found at '${this.envFileName}'. Starting fresh.`); return {}; } catch (error) { this.logger.warn(`Could not read existing env file: ${error instanceof Error ? error.message : "Unknown error"}`); return {}; } }); } /** * Writes a file with the specified permissions, with fallback on failure. * @param filePath - The path to the file. * @param content - The file content. * @param mode - The file permission mode. */ writeFileWithFallback(filePath, content, mode) { return __awaiter(this, void 0, void 0, function* () { this.logger.info(`Writing file '${filePath}' with permissions ${mode.toString(8)}.`); try { yield fs_1.default.promises.writeFile(filePath, content, { mode }); this.logger.info(`Successfully wrote file '${filePath}' with restricted permissions.`); } catch (error) { this.logger.warn(`Could not write file with restricted permissions (${mode.toString(8)}). Trying with default permissions.`); yield fs_1.default.promises.writeFile(filePath, content); this.logger.info(`Successfully wrote file '${filePath}' with default permissions.`); } }); } /** * Saves the generated token keys to disk and updates the environment file. * @param tokenKeys - The generated token key pairs. */ saveKeys(tokenKeys) { return __awaiter(this, void 0, void 0, function* () { this.logger.info("Starting to save generated token keys."); try { let newEnv = {}; const existingEnv = yield this.loadExistingEnv(); if (this.keyPath) { yield this.createSecureKeyDirectory(); // Define file paths for keys. const accessPublicPath = path_1.default.join(this.keyPath, "access-public.pem"); const accessPrivatePath = path_1.default.join(this.keyPath, "access-private.pem"); const refreshPublicPath = path_1.default.join(this.keyPath, "refresh-public.pem"); const refreshPrivatePath = path_1.default.join(this.keyPath, "refresh-private.pem"); // Save key files. this.logger.info("Saving access token public key."); yield this.writeFileWithFallback(accessPublicPath, tokenKeys.access.publicKey, this.SECURE_FILE_PERMISSIONS); this.logger.info("Saving access token private key."); yield this.writeFileWithFallback(accessPrivatePath, tokenKeys.access.privateKey, this.SECURE_FILE_PERMISSIONS); this.logger.info("Saving refresh token public key."); yield this.writeFileWithFallback(refreshPublicPath, tokenKeys.refresh.publicKey, this.SECURE_FILE_PERMISSIONS); this.logger.info("Saving refresh token private key."); yield this.writeFileWithFallback(refreshPrivatePath, tokenKeys.refresh.privateKey, this.SECURE_FILE_PERMISSIONS); // Load existing env variables. this.logger.info(`Loading existing environment variables from '${this.envFileName}'.`); newEnv = Object.assign(Object.assign({}, existingEnv), { ACCESS_TOKEN_PUBLIC_KEY_PATH: accessPublicPath, ACCESS_TOKEN_PRIVATE_KEY_PATH: accessPrivatePath, REFRESH_TOKEN_PUBLIC_KEY_PATH: refreshPublicPath, REFRESH_TOKEN_PRIVATE_KEY_PATH: refreshPrivatePath }); } // Merge new key-related variables. newEnv = Object.assign(Object.assign({}, existingEnv), { ACCESS_TOKEN_PUBLIC_KEY: tokenKeys.access.publicKey, ACCESS_TOKEN_PRIVATE_KEY: tokenKeys.access.privateKey, REFRESH_TOKEN_PUBLIC_KEY: tokenKeys.refresh.publicKey, REFRESH_TOKEN_PRIVATE_KEY: tokenKeys.refresh.privateKey }); this.logger.info("Converting key variables to env file format."); const envContent = Object.entries(newEnv) .map(([key, value]) => `${key}="${value.replace(/\n/g, "\\n")}"`) .join("\n"); this.logger.step(`Writing updated environment variables to '${this.envFileName}'.`); yield this.writeFileWithFallback(this.envFileName, envContent, this.SECURE_FILE_PERMISSIONS); this.logger.info("All keys have been generated and saved successfully."); this.printSecurityInstructions(); } catch (error) { throw new Error(`Failed to save keys: ${error instanceof Error ? error.message : "Unknown error"}`); } }); } /** * Prints security instructions to the console. */ printSecurityInstructions() { this.logger.step(` Security Notes: 1. Key files have been generated in '${this.keyPath}' 2. Both file paths and actual keys are stored in '${this.envFileName}' 3. Access and Refresh token key pairs have been generated. 4. Ensure your .gitignore includes: - ${this.envFileName} - ${this.KEY_DIRECTORY}/ 5. Consider moving keys to a secure key management service for production. 6. Backup these keys securely and never commit them to version control. 7. Previous environment variables have been preserved. Note: If you see any permission warnings, consider manually restricting file permissions using: chmod 600 ${this.keyPath}/*.pem `); } /** * Public method to generate RSA token key pairs. */ generate() { return __awaiter(this, void 0, void 0, function* () { this.logger.step("Starting RSA token key pair generation process."); try { const tokenKeys = { access: this.generateRSAKeyPair("access"), refresh: this.generateRSAKeyPair("refresh"), }; yield this.saveKeys(tokenKeys); this.logger.step("RSA token key pair generation process completed successfully."); } catch (error) { this.logger.error(`Error during key generation: ${error instanceof Error ? error.message : "Unknown error"}`); process.exit(1); } }); } } exports.SecureKeyGenerator = SecureKeyGenerator; exports.default = SecureKeyGenerator; //# sourceMappingURL=index.js.map