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
JavaScript
"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