@webda/core
Version:
Expose API with Lambda
434 lines • 14.8 kB
JavaScript
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