Skip to content

Commit

Permalink
feat: add encrypter and configuration encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
loopingz committed Dec 20, 2023
1 parent bf78ca9 commit b53611c
Show file tree
Hide file tree
Showing 12 changed files with 593 additions and 18 deletions.
8 changes: 4 additions & 4 deletions packages/core/src/core.ts
Expand Up @@ -1285,7 +1285,7 @@ export class Core<E extends CoreEvents = CoreEvents> extends events.EventEmitter
return value;
}

getMachineId() {
static getMachineId() {
try {
return process.env["WEBDA_MACHINE_ID"] || machineIdSync();
/* c8 ignore next 4 */
Expand All @@ -1305,7 +1305,7 @@ export class Core<E extends CoreEvents = CoreEvents> extends events.EventEmitter
type: "Webda/MemoryStore",
persistence: {
path: ".registry",
key: this.getMachineId()
key: Core.getMachineId()
}
};
this.createService(this.configuration.services, "Registry");
Expand Down Expand Up @@ -1635,8 +1635,8 @@ export class Core<E extends CoreEvents = CoreEvents> extends events.EventEmitter
export type MetricConfiguration<T = Counter | Gauge | Histogram, K extends string = string> = T extends Counter
? CounterConfiguration<K>
: T extends Gauge
? GaugeConfiguration<K>
: HistogramConfiguration<K>;
? GaugeConfiguration<K>
: HistogramConfiguration<K>;

/**
* Export a Registry type alias
Expand Down
11 changes: 7 additions & 4 deletions packages/core/src/services/configuration.ts
Expand Up @@ -43,8 +43,9 @@ export class ConfigurationServiceParameters extends ServiceParameters {
}

export type ConfigurationEvents = {
"Configuration.Applied": undefined;
"Configuration.Applying": undefined;
"Configuration.Applied": any;
"Configuration.Applying": any;
"Configuration.Loaded": any;
};

/**
Expand Down Expand Up @@ -204,9 +205,10 @@ export default class ConfigurationService<

this.log("DEBUG", "Refreshing configuration");
const newConfig = (await this.loadConfiguration()) || this.parameters.default;
this.emit("Configuration.Loaded", newConfig);
const serializedConfig = JSON.stringify(newConfig);
if (serializedConfig !== this.serializedConfiguration) {
this.emit("Configuration.Applying", undefined);
this.emit("Configuration.Applying", newConfig);
this.log("DEBUG", "Apply new configuration");
this.serializedConfiguration = serializedConfig;
this.configuration = newConfig;
Expand All @@ -228,7 +230,7 @@ export default class ConfigurationService<
}
});
await Promise.all(promises);
this.emit("Configuration.Applied", undefined);
this.emit("Configuration.Applied", newConfig);
}
// If the ConfigurationProvider cannot trigger we check at interval
if (this.interval) {
Expand All @@ -249,6 +251,7 @@ export default class ConfigurationService<
*/
async loadAndStoreConfiguration(): Promise<{ [key: string]: any }> {
let res = await this.loadConfiguration();
this.emit("Configuration.Loaded", res);
this.serializedConfiguration = JSON.stringify(res);
return res;
}
Expand Down
38 changes: 37 additions & 1 deletion packages/core/src/services/cryptoservice.spec.ts
Expand Up @@ -4,7 +4,7 @@ import jwt from "jsonwebtoken";
import * as sinon from "sinon";
import { WebdaTest } from "../test";
import { JSONUtils } from "../utils/serializers";
import CryptoService from "./cryptoservice";
import CryptoService, { SecretString } from "./cryptoservice";

/**
*
Expand Down Expand Up @@ -124,3 +124,39 @@ class CryptoServiceTest extends WebdaTest {
await assert.rejects(() => this.webda.getCrypto().getCurrentKeys(), /not initialized/);
}
}

@suite
class CryptoConfigurationTest {
@test
async nominal() {
CryptoService.registerEncrypter("test", {
encrypt: async (data: any) => {
return data;
},
decrypt: async (data: any) => {
return data;
}
});
let data = {
key: {
secret: "encrypt:local:plop",
port: 21
},
anotherSecret: "encrypt:local:plop2",
notEncrypted: "plop",
alreadyEncrypted: "crypt:test:plop3"
};
await CryptoService.encryptConfiguration(data);
assert.ok(data.key.secret.startsWith("crypt:local:"));
assert.ok(!data.key.secret.includes("plop"));
assert.ok(data.anotherSecret.startsWith("crypt:local:"));
assert.ok(!data.anotherSecret.includes("plop"));
assert.strictEqual(data.alreadyEncrypted, "crypt:test:plop3");
let decrypted = await CryptoService.decryptConfiguration(JSONUtils.duplicate(data));
assert.strictEqual(decrypted.anotherSecret.getValue(), "plop2");
assert.strictEqual(decrypted.anotherSecret.toString(), "********");
assert.strictEqual(SecretString.from(decrypted.alreadyEncrypted), "plop3");
assert.strictEqual(SecretString.from(decrypted.key.secret), "plop");
assert.strictEqual(`Test: ${decrypted.alreadyEncrypted}`, "Test: ********");
}
}
155 changes: 150 additions & 5 deletions packages/core/src/services/cryptoservice.ts
@@ -1,9 +1,41 @@
import { createCipheriv, createDecipheriv, createHmac, generateKeyPairSync, randomBytes } from "crypto";
import { createCipheriv, createDecipheriv, createHash, createHmac, generateKeyPairSync, randomBytes } from "crypto";
import jwt from "jsonwebtoken";
import { pem2jwk } from "pem-jwk";
import { OperationContext, RegistryEntry, Store } from "../index";
import * as util from "util";
import { Core, OperationContext, RegistryEntry, Store } from "../index";
import { JSONUtils } from "../utils/serializers";
import { DeepPartial, Inject, Route, Service, ServiceParameters } from "./service";

export class SecretString {
constructor(
protected str: string,
protected encrypter: string
) {}

static from(value: string | SecretString, path?: string): string {
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(): string {
return this.str;
}
toString(): string {
return "********";
}
[util.inspect.custom](depth, options, inspect) {
return "********";
}
}

export interface KeysRegistry {
/**
* Contains the instanceId of the last
Expand All @@ -24,6 +56,24 @@ export interface KeysRegistry {
current: string;
}

/**
* Encrypt/Decrypt string
*/
export interface StringEncrypter {
/**
* Encrypt a string
* @param data
* @returns
*/
encrypt(data: string, options?: any): Promise<string>;
/**
* Decrypt a string
* @param data
* @returns
*/
decrypt(data: string, options?: any): Promise<string>;
}

/**
* JWT Options
*/
Expand Down Expand Up @@ -176,7 +226,26 @@ interface KeysDefinition {
/**
* @WebdaModda
*/
export default class CryptoService<T extends CryptoServiceParameters = CryptoServiceParameters> extends Service<T> {
export default class CryptoService<T extends CryptoServiceParameters = CryptoServiceParameters>
extends Service<T>
implements StringEncrypter
{
private static encrypters: { [key: string]: StringEncrypter } = {};

/**
* Register an encrypter for configuration
* @param name
* @param encrypter
*/
static registerEncrypter(
name: string,
encrypter: { encrypt: (data: string) => Promise<string>; decrypt: (data: string) => Promise<string> }
) {
if (CryptoService.encrypters[name]) {
console.error("Encrypter", name, "already registered");
}
CryptoService.encrypters[name] = encrypter;
}
currentSymetricKey: string;
currentAsymetricKey: { publicKey: string; privateKey: string };
current: string;
Expand Down Expand Up @@ -209,6 +278,7 @@ export default class CryptoService<T extends CryptoServiceParameters = CryptoSer
*/
async init(): Promise<this> {
await super.init();
CryptoService.encrypters["self"] = this;
// Load keys
if (!(await this.load()) && this.parameters.autoCreate) {
await this.rotate();
Expand Down Expand Up @@ -437,19 +507,74 @@ export default class CryptoService<T extends CryptoServiceParameters = CryptoSer
return JSON.parse(Buffer.from(token.split(".")[0], "base64").toString());
}

/**
* Encrypt configuration
* @param data
*/
public static async encryptConfiguration(data: any) {
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
*/
public static async decryptConfiguration(data: any): Promise<any> {
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
*/
public async decrypt(token: string): Promise<any> {
let input = Buffer.from(await this.jwtVerify(token), "base64");
let header = this.getJWTHeader(token);
let iv = input.slice(0, 16);
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.slice(16)).toString() + decipher.final().toString());
return JSON.parse(decipher.update(input.subarray(16)).toString() + decipher.final().toString());
}

/**
Expand Down Expand Up @@ -490,4 +615,24 @@ export default class CryptoService<T extends CryptoServiceParameters = CryptoSer
}
}

/**
* Encrypt data with local machine id
*/
CryptoService.registerEncrypter("local", {
encrypt: async (data: string) => {
// 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: string) => {
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 };

0 comments on commit b53611c

Please sign in to comment.