UNPKG

@webda/core

Version:

Expose API with Lambda

434 lines 14.8 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { createCipheriv, createDecipheriv, createHash, createHmac, generateKeyPairSync, randomBytes } from "crypto"; import jwt from "jsonwebtoken"; import { pem2jwk } from "pem-jwk"; import * as util from "util"; import { Core } from "../index.js"; import { JSONUtils } from "../utils/serializers.js"; import { Inject, Route, Service, ServiceParameters } from "./service.js"; export class SecretString { constructor(str, encrypter) { this.str = str; this.encrypter = encrypter; } static from(value, path) { if (value instanceof SecretString) { return value.getValue(); } if (Core.get()) { Core.get()?.log("WARN", "A secret string is not encrypted", value); } else { console.error("WARN", "A secret string is not encrypted"); } return value; } getValue() { return this.str; } toString() { return "********"; } [util.inspect.custom](depth, options, inspect) { return "********"; } } export class CryptoServiceParameters extends ServiceParameters { constructor(params) { var _a, _b, _c, _d, _e, _f, _g, _h; super(params); this.symetricKeyLength ?? (this.symetricKeyLength = 256); this.symetricCipher ?? (this.symetricCipher = "aes-256-ctr"); this.asymetricType ?? (this.asymetricType = "rsa"); this.asymetricOptions ?? (this.asymetricOptions = {}); (_a = this.asymetricOptions).modulusLength ?? (_a.modulusLength = 2048); (_b = this.asymetricOptions).privateKeyEncoding ?? (_b.privateKeyEncoding = {}); (_c = this.asymetricOptions.privateKeyEncoding).format ?? (_c.format = "pem"); (_d = this.asymetricOptions.privateKeyEncoding).type ?? (_d.type = "pkcs8"); (_e = this.asymetricOptions).publicKeyEncoding ?? (_e.publicKeyEncoding = {}); (_f = this.asymetricOptions.publicKeyEncoding).format ?? (_f.format = "pem"); (_g = this.asymetricOptions.publicKeyEncoding).type ?? (_g.type = "spki"); this.jwt ?? (this.jwt = {}); (_h = this.jwt).algorithm ?? (_h.algorithm = "HS256"); } } /** * @WebdaModda */ class CryptoService extends Service { constructor() { super(...arguments); /** * JWKS cache */ this.jwks = {}; } /** * Register an encrypter for configuration * @param name * @param encrypter */ static registerEncrypter(name, encrypter) { if (CryptoService.encrypters[name]) { console.error("Encrypter", name, "already registered"); } CryptoService.encrypters[name] = encrypter; } /** * @override */ loadParameters(params) { return new CryptoServiceParameters(params); } /** * @override */ async init() { await super.init(); CryptoService.encrypters["self"] = this; // Load keys if (!(await this.load()) && this.parameters.autoCreate) { await this.rotate(); } return this; } /** * */ async serveJWKS(context) { context.write({ keys: Object.keys(this.keys).map(k => { if (!this.jwks[k]) { /* when Node >= 16 this.jwks[k] = createPublicKey(this.keys[k].publicKey).export({ format: "jwk" }); and remove pem-jwk */ this.jwks[k] = pem2jwk(this.keys[k].publicKey); } return { kty: "RSA", kid: k, n: this.jwks[k].n, e: this.jwks[k].e }; }) }); } /** * Load keys from registry */ async load() { let load = await this.registry.get("keys"); if (!load) { return false; } this.keys = {}; Object.keys(load) .filter(k => k.startsWith("key_")) .forEach(k => { this.keys[k.substring(4)] = load[k]; }); this.current = load.current.startsWith("init-") ? undefined : load.current; this.age = parseInt(this.current, 36); return true; } /** * Generate asymetric key * @returns */ generateAsymetricKeys() { const { publicKey, privateKey } = generateKeyPairSync( // @ts-ignore this.parameters.asymetricType, this.parameters.asymetricOptions); return { publicKey, privateKey }; } /** * Generate symetric key * @returns */ generateSymetricKey() { return randomBytes(this.parameters.symetricKeyLength / 8).toString("base64"); } /** * Return current key set */ async getCurrentKeys() { if (!this.keys || !this.current || !this.keys[this.current]) { throw new Error("CryptoService not initialized"); } return { keys: this.keys[this.current], id: this.current }; } /** * Retrieve a HMAC for a string * @param data * @param keyId to use * @returns */ async hmac(data, keyId) { if (typeof data !== "string") { data = JSONUtils.stringify(data); } let key = keyId ? { id: keyId, keys: this.keys[keyId] } : await this.getCurrentKeys(); return key.id + "." + createHmac("sha256", key.keys.symetric).update(data).digest("hex"); } /** * Verify a HMAC for a string * @param data * @returns */ async hmacVerify(data, hmac) { if (typeof data !== "string") { data = JSONUtils.stringify(data); } let [keyId, mac] = hmac.split("."); if (!(await this.checkKey(keyId))) { return false; } return createHmac("sha256", this.keys[keyId].symetric).update(data).digest("hex") === mac; } /** * JWT token generation */ async jwtSign(data, options) { let res = { ...this.parameters.jwt, ...options }; let key = res.secretOrPublicKey; // Default to our current private key if (!res.secretOrPublicKey) { let keyInfo = await this.getCurrentKeys(); // Depending on the algo fallback to the right key if (res.algorithm.startsWith("HS")) { key = keyInfo.keys.symetric; res.keyid = "S" + keyInfo.id; } else { key = keyInfo.keys.privateKey; res.keyid = "A" + keyInfo.id; } } delete res.secretOrPublicKey; return jwt.sign(data, key, res); } /** * * @param keyId * @returns */ async checkKey(keyId) { if (!this.keys[keyId]) { // Key is more recent than current one so try to reload if (parseInt(keyId, 36) > this.age) { await this.load(); } // Key is still not found if (!this.keys[keyId]) { return false; } } return true; } /** * Get JWT key based on kid */ async getJWTKey(header, callback) { if (!header.kid) { callback(new Error("Unknown key")); return; } let keyId = header.kid.substring(1); if (!(await this.checkKey(keyId))) { callback(new Error("Unknown key")); return; } // Check first letter that define Symetric or Asymetric if (header.kid.startsWith("S")) { callback(null, this.keys[keyId].symetric); } else if (header.kid.startsWith("A")) { callback(null, this.keys[keyId].publicKey); } else { callback(new Error("Unknown key")); } } /** * JWT token verification */ async jwtVerify(token, options) { return new Promise((resolve, reject) => { jwt.verify(token, options?.secretOrPublicKey || this.getJWTKey.bind(this), { ...options, secretOrPublicKey: undefined }, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); } /** * Encrypt data */ async encrypt(data) { let key = await this.getCurrentKeys(); // Initialization Vector let iv = randomBytes(16); let cipher = createCipheriv(this.parameters.symetricCipher, Buffer.from(key.keys.symetric, "base64"), iv); let encrypted = Buffer.concat([iv, cipher.update(Buffer.from(JSON.stringify(data))), cipher.final()]).toString("base64"); return this.jwtSign(encrypted, { keyid: `S${key.id}`, secretOrPublicKey: key.keys.symetric }); } /** * Parse the JWT header section */ getJWTHeader(token) { return JSON.parse(Buffer.from(token.split(".")[0], "base64").toString()); } /** * Encrypt configuration * @param data */ static async encryptConfiguration(data) { if (data instanceof Object) { for (let i in data) { data[i] = await CryptoService.encryptConfiguration(data[i]); } } else if (typeof data === "string") { if (data.startsWith("encrypt:") || data.startsWith("sencrypt:")) { let str = data.substring(data.indexOf(":") + 1); let type = str.substring(0, str.indexOf(":")); str = str.substring(str.indexOf(":") + 1); if (!CryptoService.encrypters[type]) { throw new Error("Unknown encrypter " + type); } if (data.startsWith("s")) { data = `scrypt:${type}:` + (await CryptoService.encrypters[type].encrypt(str)); } else { data = `crypt:${type}:` + (await CryptoService.encrypters[type].encrypt(str)); } } } return data; } /** * * @param data */ static async decryptConfiguration(data) { if (data instanceof Object) { for (let i in data) { data[i] = await CryptoService.decryptConfiguration(data[i]); } } else if (typeof data === "string") { if (data.startsWith("crypt:") || data.startsWith("scrypt:")) { let str = data.substring(data.indexOf(":") + 1); let type = str.substring(0, str.indexOf(":")); str = str.substring(str.indexOf(":") + 1); if (!CryptoService.encrypters[type]) { throw new Error("Unknown encrypter " + type); } // We keep the ability to map to a simple string for incompatible module if (data.startsWith("scrypt:")) { return await CryptoService.encrypters[type].decrypt(str); } else { return new SecretString(await CryptoService.encrypters[type].decrypt(str), type); } } } return data; } /** * Decrypt data */ async decrypt(token) { let input = Buffer.from(await this.jwtVerify(token), "base64"); let header = this.getJWTHeader(token); let iv = input.subarray(0, 16); let decipher = createDecipheriv(this.parameters.symetricCipher, Buffer.from(this.keys[header.kid.substring(1)].symetric, "base64"), iv); return JSON.parse(decipher.update(input.subarray(16)).toString() + decipher.final().toString()); } /** * Get next id */ getNextId() { // Should be good for years as 8char let age = Math.floor(Date.now() / 1000); return { age, id: age.toString(36) }; } /** * Rotate keys */ async rotate() { const { age, id } = this.getNextId(); let next = { current: id, rotationInstance: this.getWebda().getInstanceId() }; next[`key_${id}`] = { ...this.generateAsymetricKeys(), symetric: this.generateSymetricKey() }; if (!(await this.registry.exists("keys"))) { this.current = `init-${this.getWebda().getInstanceId()}`; await this.registry.put("keys", { current: this.current }); } if (await this.registry.conditionalPatch("keys", next, "current", this.current)) { this.keys ?? (this.keys = {}); this.keys[id] = next[`key_${id}`]; this.current = id; this.age = age; } else { // Reload as something else has modified await this.load(); } } } CryptoService.encrypters = {}; export default CryptoService; __decorate([ Inject("Registry") ], CryptoService.prototype, "registry", void 0); __decorate([ Route(".", ["GET"], { description: "Serve JWKS keys", get: { operationId: "getJWKS" } }) ], CryptoService.prototype, "serveJWKS", null); /** * Encrypt data with local machine id */ CryptoService.registerEncrypter("local", { encrypt: async (data) => { // Initialization Vector let iv = randomBytes(16); const key = createHash("sha256").update(Core.getMachineId()).digest(); let cipher = createCipheriv("aes-256-ctr", key, iv); return Buffer.concat([iv, cipher.update(Buffer.from(data)), cipher.final()]).toString("base64"); }, decrypt: async (data) => { let input = Buffer.from(data, "base64"); let iv = input.subarray(0, 16); const key = createHash("sha256").update(Core.getMachineId()).digest(); let decipher = createDecipheriv("aes-256-ctr", key, iv); return decipher.update(input.subarray(16)).toString() + decipher.final().toString(); } }); export { CryptoService }; //# sourceMappingURL=cryptoservice.js.map