UNPKG

timebasedcipher

Version:

Time-based key generation and AES encryption/decryption SDK

155 lines (154 loc) 6.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.decrypt = exports.encrypt = exports.generateKey = void 0; const crypto_js_1 = __importDefault(require("crypto-js")); /** * Decode the payload of a JWT without verifying its signature. * Used only to read the `exp` claim (in seconds since epoch). * * @param token - The JWT string (header.payload.signature) * @returns Parsed payload object. * @throws If the token is malformed or payload is not valid JSON. */ const decodeJwtPayload = (token) => { const parts = token.split("."); if (parts.length < 2) { throw new Error("Invalid JWT: missing payload part"); } const base64Url = parts[1]; const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); // Use CryptoJS to decode base64 → UTF-8 const wordArray = crypto_js_1.default.enc.Base64.parse(base64); const json = wordArray.toString(crypto_js_1.default.enc.Utf8); try { return JSON.parse(json); } catch { throw new Error("Invalid JWT payload JSON"); } }; /** * Generate a time-based rotating key (SHA256 hash of sharedSecret + current interval). * * The key is derived from: * SHA256(`${secretInput}:${timeSlot}`) * where: * timeSlot = floor(Date.now() / (interval * 1000) + 1000) * * When `isJwt` is true: * - `sharedSecretOrJwt` is treated as a JWT string * - The JWT payload is decoded * - The `exp` claim (if present) is checked against current time * - If the JWT is expired, an error is thrown and no key is generated * - The *raw JWT string* is still used as the secret input for key derivation * * @param sharedSecretOrJwt - A pre-shared secret string, or a JWT when `isJwt` is true. * @param interval - The rotation interval in seconds. The timeSlot calculation * is based on this value, so both sides must use the same interval. * @param isJwt - Optional flag (default: false). When true, `sharedSecretOrJwt` is * treated as a JWT and its `exp` is validated before deriving the key. * @returns A 64-character hexadecimal SHA-256 hash string to be used as an AES-256 key. * @throws If `isJwt` is true and the JWT is malformed or expired. */ const generateKey = (sharedSecretOrJwt, interval, isJwt = false) => { let secretInput = sharedSecretOrJwt; if (isJwt) { const payload = decodeJwtPayload(sharedSecretOrJwt); if (typeof payload.exp !== "number") { throw new Error("JWT payload does not contain a valid 'exp' claim"); } const nowSeconds = Math.floor(Date.now() / 1000); if (payload.exp <= nowSeconds) { throw new Error("JWT is expired"); } } const timeSlot = Math.floor(Date.now() / (interval * 1000) + 1000); const input = `${secretInput}:${timeSlot}`; // SHA-256 → hex string return crypto_js_1.default.SHA256(input).toString(crypto_js_1.default.enc.Hex); }; exports.generateKey = generateKey; /** * Encrypt an arbitrary JavaScript value using AES-256-CBC with a random IV. * * The function: * - JSON stringifies the input `data` * - Uses the provided hex-encoded key as AES-256 key material * - Generates a random 16-byte IV * - Encrypts using AES-256-CBC * - Returns a concatenated string in the format: "cipherHex:ivHex" * * @param data - Any serializable JavaScript value (e.g. object, array, string) to encrypt. * @param encryptionKeyHex - A 64-character hex string representing a 32-byte key * (typically the output of {@link generateKey}). * @returns A string in the format `"cipherHex:ivHex"` where both parts are hex-encoded. * @throws If encryption fails or JSON serialization fails. */ const encrypt = (data, encryptionKeyHex) => { const json = JSON.stringify(data); try { // key and IV as WordArray const key = crypto_js_1.default.enc.Hex.parse(encryptionKeyHex); // 32 bytes const iv = crypto_js_1.default.lib.WordArray.random(16); // 16 bytes const encrypted = crypto_js_1.default.AES.encrypt(json, key, { iv }); const cipherHex = encrypted.ciphertext.toString(crypto_js_1.default.enc.Hex); const ivHex = iv.toString(crypto_js_1.default.enc.Hex); return `${cipherHex}:${ivHex}`; } catch (err) { // eslint-disable-next-line no-console console.error("Error in encrypt:", err); throw err; } }; exports.encrypt = encrypt; /** * Decrypt an AES-256-CBC ciphertext string produced by {@link encrypt}. * * The function expects the input to be in the format: * "cipherHex:ivHex" * where: * - cipherHex is the hex-encoded ciphertext * - ivHex is the hex-encoded 16-byte IV * * It will: * - Split and parse the hex components * - Decrypt using AES-256-CBC with the provided key * - Parse the resulting UTF-8 JSON back into the original JavaScript value * * @param cipherText - The ciphertext string in the format `"cipherHex:ivHex"`. * @param encryptionKeyHex - A 64-character hex string representing a 32-byte key * (must match the key used for encryption). * @returns The decrypted JavaScript value (e.g. object, array, string). * @throws If the format is invalid, decryption fails, or the decrypted output is empty/invalid JSON. */ const decrypt = (cipherText, encryptionKeyHex) => { const [encryptedHex, ivHex] = cipherText.split(":"); if (!ivHex || !encryptedHex) { throw new Error("Invalid cipher format, expected cipherHex:ivHex"); } const key = crypto_js_1.default.enc.Hex.parse(encryptionKeyHex); const iv = crypto_js_1.default.enc.Hex.parse(ivHex); const ciphertext = crypto_js_1.default.enc.Hex.parse(encryptedHex); // Proper CipherParams object const cipherParams = crypto_js_1.default.lib.CipherParams.create({ ciphertext, }); try { const decrypted = crypto_js_1.default.AES.decrypt(cipherParams, key, { iv }); const utf8 = decrypted.toString(crypto_js_1.default.enc.Utf8); if (!utf8) { throw new Error("Decryption produced empty string"); } return JSON.parse(utf8); } catch (err) { // eslint-disable-next-line no-console console.error("Error decrypting with current key:", err); throw new Error("Unable to decrypt: no valid key found or invalid payload"); } }; exports.decrypt = decrypt;