Optimize usage of getSettings() of crypto
MODIFY
package.json
package.json
"mochatest:ocpp": "cross-env TS_NODE_FILES=true mocha test/**/OCPP*Test.ts",
"mochatest:authentication": "cross-env TS_NODE_FILES=true mocha test/**/AuthenticationTest.ts",
"mochatest:createContext": "cross-env TS_NODE_FILES=true mocha test/**/ContextBuilderRunner.ts",
- "mochatest:encryption": "cross-env TS_NODE_FILES=true mocha test/**/EncryptionSettingTest.ts",
+ "mochatest:crypto": "cross-env TS_NODE_FILES=true mocha test/**/EncryptionSettingTest.ts",
"mochatest:filter": "cross-env TS_NODE_FILES=true mocha test/**/FilterTest.ts",
"mochatest:tenantsetting": "cross-env TS_NODE_FILES=true mocha test/**/ComponentActivationTenantTest.ts",
"mochatest:billing": "cross-env TS_NODE_FILES=true mocha test/**/BillingTest.ts",

MODIFY
src/migration/tasks/MigrateCryptoSettingsFromConfigToDBTask.ts
src/migration/tasks/MigrateCryptoSettingsFromConfigToDBTask.ts
-import { CryptoKeySetting, CryptoSettingsType } from '../../types/Setting';
+import { CryptoSettings, CryptoSettingsType } from '../../types/Setting';
import Configuration from '../../utils/Configuration';
import Constants from '../../utils/Constants';
@@ -35,7 +35,7 @@ export default class MigrateCryptoSettingsFromConfigToDBTask extends MigrationTa
key: configCryptoKey,
keyProperties: configCryptoKeyProperties,
}
- } as CryptoKeySetting;
+ } as CryptoSettings;
await Cypher.saveCryptoSetting(tenant.id, keySettingToSave);
}
}

MODIFY
src/server/rest/v1/service/TenantService.ts
src/server/rest/v1/service/TenantService.ts
import { Action, Entity } from '../../../../types/Authorization';
-import { CryptoKeySetting, CryptoSettingsType, SettingDB, SettingDBContent } from '../../../../types/Setting';
+import { CryptoSettings, CryptoSettingsType, SettingDB, SettingDBContent } from '../../../../types/Setting';
import { HTTPAuthError, HTTPError } from '../../../../types/HTTPError';
import { NextFunction, Request, Response } from 'express';
import Tenant, { TenantLogo } from '../../../../types/Tenant';
@@ -181,7 +181,7 @@ export default class TenantService {
key: Utils.generateKey(),
keyProperties: Utils.getDefaultKeyProperties()
}
- } as CryptoKeySetting;
+ } as CryptoSettings;
// Save Crypto Key Settings
await Cypher.saveCryptoSetting(tenantID, keySettingToSave);
}

MODIFY
src/storage/mongodb/SettingStorage.ts
src/storage/mongodb/SettingStorage.ts
-import { AnalyticsSettings, AnalyticsSettingsType, AssetSettings, AssetSettingsType, BillingSettings, BillingSettingsType, CryptoKeySetting, CryptoSetting, CryptoSettingsType, PricingSettings, PricingSettingsType, RefundSettings, RefundSettingsType, RoamingSettings, SettingDB, SmartChargingSettings, SmartChargingSettingsType } from '../../types/Setting';
+import { AnalyticsSettings, AnalyticsSettingsType, AssetSettings, AssetSettingsType, BillingSettings, BillingSettingsType, CryptoSetting, CryptoSettings, CryptoSettingsType, PricingSettings, PricingSettingsType, RefundSettings, RefundSettingsType, RoamingSettings, SettingDB, SmartChargingSettings, SmartChargingSettingsType } from '../../types/Setting';
import global, { FilterParams } from '../../types/GlobalType';
import BackendError from '../../exception/BackendError';
@@ -313,7 +313,7 @@ export default class SettingStorage {
}
}
- public static async getCryptoSettings(tenantID: string): Promise<CryptoKeySetting> {
+ public static async getCryptoSettings(tenantID: string): Promise<CryptoSettings> {
// Get the Crypto Key settings
const settings = await SettingStorage.getSettings(tenantID,
{ identifier: TenantComponents.CRYPTO },

MODIFY
src/types/Setting.ts
src/types/Setting.ts
@@ -247,7 +247,7 @@ export enum CryptoSettingsType {
CRYPTO = 'crypto'
}
-export interface CryptoKeySetting extends Setting {
+export interface CryptoSettings extends Setting {
identifier: TenantComponents.CRYPTO;
type: CryptoSettingsType;
crypto: CryptoSetting;

MODIFY
src/utils/Cypher.ts
src/utils/Cypher.ts
-import { CryptoKeySetting, CryptoSetting, SettingDB } from '../types/Setting';
+import { CryptoSetting, CryptoSettings, SettingDB } from '../types/Setting';
import BackendError from '../exception/BackendError';
import Constants from './Constants';
import { LockEntity } from '../types/Locking';
import LockingManager from '../locking/LockingManager';
import SettingStorage from '../storage/mongodb/SettingStorage';
+import TenantComponents from '../types/TenantComponents';
import Utils from './Utils';
import _ from 'lodash';
import crypto from 'crypto';
@@ -14,9 +15,11 @@ const MODULE_NAME = 'Cypher';
export default class Cypher {
- public static async encrypt(tenantID: string, data: string, useFormerKey = false): Promise<string> {
+ public static async encrypt(tenantID: string, data: string, useFormerKey = false, cryptoSetting?: CryptoSetting): Promise<string> {
const iv = crypto.randomBytes(IV_LENGTH);
- const cryptoSetting = await Cypher.getCryptoSetting(tenantID);
+ if (!cryptoSetting) {
+ cryptoSetting = (await Cypher.getCryptoSettings(tenantID)).crypto;
+ }
const algo = useFormerKey ? Utils.buildAlgorithm(cryptoSetting.formerKeyProperties) : Utils.buildAlgorithm(cryptoSetting.keyProperties);
const key = useFormerKey ? Buffer.from(cryptoSetting.formerKey) : Buffer.from(cryptoSetting.key);
const cipher = crypto.createCipheriv(algo, key, iv);
@@ -25,11 +28,13 @@ export default class Cypher {
return iv.toString('hex') + ':' + encryptedData.toString('hex');
}
- public static async decrypt(tenantID: string, data: string, useFormerKey = false): Promise<string> {
+ public static async decrypt(tenantID: string, data: string, useFormerKey = false, cryptoSetting?: CryptoSetting): Promise<string> {
const dataParts = data.split(':');
const iv = Buffer.from(dataParts.shift(), 'hex');
const encryptedData = Buffer.from(dataParts.join(':'), 'hex');
- const cryptoSetting = await Cypher.getCryptoSetting(tenantID);
+ if (!cryptoSetting) {
+ cryptoSetting = (await Cypher.getCryptoSettings(tenantID)).crypto;
+ }
const algo = useFormerKey ? Utils.buildAlgorithm(cryptoSetting.formerKeyProperties) : Utils.buildAlgorithm(cryptoSetting.keyProperties);
const key = useFormerKey ? Buffer.from(cryptoSetting.formerKey) : Buffer.from(cryptoSetting.key);
const decipher = crypto.createDecipheriv(algo, key , iv);
@@ -42,66 +47,47 @@ export default class Cypher {
return crypto.createHash('sha256').update(data).digest('hex');
}
- public static async encryptSensitiveDataInJSON(tenantID: string, obj: Record<string, any>, useFormerKey = false): Promise<void> {
- if (typeof obj !== 'object') {
+ public static async encryptSensitiveDataInJSON(tenantID: string, data: Record<string, any>, useFormerKey = false, cryptoSetting?: CryptoSetting): Promise<void> {
+ if (typeof data !== 'object') {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
module: MODULE_NAME,
method: 'encryptSensitiveDataInJSON',
- message: `The parameter ${obj} is not an object`
+ message: `The parameter ${data} is not an object`
});
}
- if ('sensitiveData' in obj) {
- // Check that sensitive data is an array
- if (!Array.isArray(obj.sensitiveData)) {
- throw new BackendError({
- source: Constants.CENTRAL_SERVER,
- module: MODULE_NAME,
- method: 'encryptSensitiveDataInJSON',
- message: 'The property \'sensitiveData\' is not an array'
- });
+ if (Utils.isEmptyArray(data.sensitiveData)) {
+ data.sensitiveData = [];
}
- for (const property of obj.sensitiveData as string[]) {
+ for (const property of data.sensitiveData as string[]) {
// Check that the property does exist otherwise skip to the next property
- if (_.has(obj, property)) {
- const value = _.get(obj, property);
+ if (Utils.objectHasProperty(data, property)) {
+ const value = _.get(data, property);
// If the value is undefined, null or empty then do nothing and skip to the next property
if (value && value.length > 0) {
- _.set(obj, property, await Cypher.encrypt(tenantID, value, useFormerKey));
- }
+ _.set(data, property, await Cypher.encrypt(tenantID, value, useFormerKey, cryptoSetting));
}
}
- } else {
- obj.sensitiveData = [];
}
}
- public static async decryptSensitiveDataInJSON(tenantID: string, obj: Record<string, any>, useFormerKey = false): Promise<void> {
- if (typeof obj !== 'object') {
- throw new BackendError({
- source: Constants.CENTRAL_SERVER,
- module: MODULE_NAME,
- method: 'decryptSensitiveDataInJSON',
- message: `The parameter ${obj} is not an object`
- });
- }
- if ('sensitiveData' in obj) {
- // Check that sensitive data is an array
- if (!Array.isArray(obj.sensitiveData)) {
+ public static async decryptSensitiveDataInJSON(tenantID: string, data: Record<string, any>, useFormerKey = false, cryptoSetting?: CryptoSetting): Promise<void> {
+ if (typeof data !== 'object') {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
module: MODULE_NAME,
method: 'decryptSensitiveDataInJSON',
- message: 'The property \'sensitiveData\' is not an array'
+ message: `The parameter ${data} is not an object`
});
}
- for (const property of obj.sensitiveData as string[]) {
+ if (!Utils.isEmptyArray(data.sensitiveData)) {
+ for (const property of data.sensitiveData as string[]) {
// Check that the property does exist otherwise skip to the next property
- if (_.has(obj, property)) {
- const value = _.get(obj, property);
+ if (Utils.objectHasProperty(data, property)) {
+ const value = _.get(data, property);
// If the value is undefined, null or empty then do nothing and skip to the next property
if (value && value.length > 0) {
- _.set(obj, property, await Cypher.decrypt(tenantID, value, useFormerKey));
+ _.set(data, property, await Cypher.decrypt(tenantID, value, useFormerKey, cryptoSetting));
}
}
}
@@ -145,11 +131,15 @@ export default class Cypher {
const createDatabaseLock = LockingManager.createExclusiveLock(tenantID, LockEntity.DATABASE, 'migrate-settings-sensitive-data');
if (await LockingManager.acquire(createDatabaseLock)) {
try {
- await Cypher.migrate(tenantID);
- await Cypher.cleanupFormerSensitiveData(tenantID);
- const keySettings = await SettingStorage.getCryptoSettings(tenantID);
- keySettings.crypto.migrationToBeDone = false;
- await Cypher.saveCryptoSetting(tenantID, keySettings);
+ // Get the crypto key
+ const cryptoSettings = await Cypher.getCryptoSettings(tenantID);
+ // Migrate Settings
+ await Cypher.migrateSettings(tenantID, cryptoSettings.crypto);
+ // Cleanup
+ await Cypher.cleanupBackupSensitiveData(tenantID);
+ // Flag the migration as done
+ cryptoSettings.crypto.migrationToBeDone = false;
+ await Cypher.saveCryptoSetting(tenantID, cryptoSettings);
} catch (err) {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
@@ -164,65 +154,11 @@ export default class Cypher {
}
}
- public static async migrate(tenantID: string): Promise<void> {
- const cryptoSetting = await Cypher.getCryptoSetting(tenantID);
- await Cypher.migrateSettings(tenantID, cryptoSetting);
- }
-
- public static async getSettingsWithSensitiveData(tenantID: string): Promise<SettingDB[]> {
- // Get all settings per tenant
- const settings = await SettingStorage.getSettings(tenantID, {},
- Constants.DB_PARAMS_MAX_LIMIT);
- // Filter settings with sensitiveData
- return settings.result.filter((value: SettingDB) => {
- if (value?.sensitiveData && !Utils.isEmptyArray(value?.sensitiveData)) {
- return true;
- }
- });
- }
-
- public static async migrateSettings(tenantID: string, cryptoSetting: CryptoSetting): Promise<void> {
-
- const settingsToMigrate = await Cypher.getSettingsWithSensitiveData(tenantID);
- // If tenant has settings with sensitive data, migrate them
- if (!Utils.isEmptyArray(settingsToMigrate)) {
- // Migrate
- for (const setting of settingsToMigrate) {
- if (!setting.formerSensitiveData && Utils.isEmptyArray(setting.formerSensitiveData)) {
- // Save former senitive data in setting
- const formerSensitiveData = Cypher.prepareFormerSenitiveData(setting);
- formerSensitiveData.push(Cypher.hash(cryptoSetting.formerKey));
- setting.formerSensitiveData = formerSensitiveData;
- // Decrypt sensitive data with former key and key properties
- await Cypher.decryptSensitiveDataInJSON(tenantID,setting, true);
- // Encrypt sensitive data with new key and key properties
- await Cypher.encryptSensitiveDataInJSON(tenantID, setting);
- // Save setting with sensitive data encrypted with new key
- await SettingStorage.saveSettings(tenantID, setting);
- }
- }
- }
- }
-
- public static async cleanupFormerSensitiveData(tenantID: string): Promise<void> {
- const settingsToCleanup = await Cypher.getSettingsWithSensitiveData(tenantID);
- // If tenant has settings with sensitive data, clean them
- if (!Utils.isEmptyArray(settingsToCleanup)) {
- // Cleanup
- for (const setting of settingsToCleanup) {
- if (setting.formerSensitiveData) {
- delete setting.formerSensitiveData;
- await SettingStorage.saveSettings(tenantID, setting);
- }
- }
- }
- }
-
- public static async saveCryptoSetting(tenantID: string, cryptoSettingToSave: CryptoKeySetting): Promise<void> {
+ public static async saveCryptoSetting(tenantID: string, cryptoSettingToSave: CryptoSettings): Promise<void> {
// Build internal structure
const settingsToSave = {
id: cryptoSettingToSave.id,
- identifier: 'crypto',
+ identifier: TenantComponents.CRYPTO,
lastChangedOn: new Date(),
content: {
crypto: cryptoSettingToSave.crypto
@@ -232,7 +168,7 @@ export default class Cypher {
await SettingStorage.saveSettings(tenantID, settingsToSave);
}
- private static async getCryptoSetting(tenantID: string): Promise<CryptoSetting> {
+ private static async getCryptoSettings(tenantID: string): Promise<CryptoSettings> {
const cryptoSettings = await SettingStorage.getCryptoSettings(tenantID);
if (!cryptoSettings || !cryptoSettings.crypto) {
throw new BackendError({
@@ -242,15 +178,50 @@ export default class Cypher {
message: `Tenant ID '${tenantID}' does not have crypto settings.`
});
}
- return cryptoSettings.crypto;
+ return cryptoSettings;
+ }
+
+ private static async migrateSettings(tenantID: string, cryptoSetting: CryptoSetting): Promise<void> {
+ const settingsToMigrate = await Cypher.getSettingsWithSensitiveData(tenantID);
+ // If tenant has settings with sensitive data, migrate them
+ if (!Utils.isEmptyArray(settingsToMigrate)) {
+ // Migrate
+ for (const settingToMigrate of settingsToMigrate) {
+ // Migration already done?
+ if (Utils.isEmptyArray(settingToMigrate.formerSensitiveData)) {
+ // Save former senitive data in setting
+ const formerSensitiveData = Cypher.backupSensitiveData(settingToMigrate);
+ formerSensitiveData.push(Cypher.hash(cryptoSetting.formerKey));
+ settingToMigrate.formerSensitiveData = formerSensitiveData;
+ // Decrypt sensitive data with former key and key properties
+ await Cypher.decryptSensitiveDataInJSON(tenantID, settingToMigrate, true, cryptoSetting);
+ // Encrypt sensitive data with new key and key properties
+ await Cypher.encryptSensitiveDataInJSON(tenantID, settingToMigrate, false, cryptoSetting);
+ // Save setting with sensitive data encrypted with new key
+ await SettingStorage.saveSettings(tenantID, settingToMigrate);
+ }
+ }
+ }
}
- private static prepareFormerSenitiveData(setting: SettingDB): string[] {
+ private static async getSettingsWithSensitiveData(tenantID: string): Promise<SettingDB[]> {
+ // Get all settings per tenant
+ const settings = await SettingStorage.getSettings(tenantID, {},
+ Constants.DB_PARAMS_MAX_LIMIT);
+ // Filter settings with sensitiveData
+ return settings.result.filter((value: SettingDB) => {
+ if (value?.sensitiveData && !Utils.isEmptyArray(value?.sensitiveData)) {
+ return true;
+ }
+ });
+ }
+
+ private static backupSensitiveData(setting: SettingDB): string[] {
const formerSensitiveData: string[] = [];
for (const property of setting.sensitiveData) {
// Check that the property does exist otherwise skip to the next property
- if (_.has(setting, property)) {
- const value: string = _.get(setting, property);
+ if (Utils.objectHasProperty(setting, property)) {
+ const value = _.get(setting, property) as string;
// If the value is undefined, null or empty then do nothing and skip to the next property
if (value && value.length > 0) {
formerSensitiveData.push(value);
@@ -259,4 +230,19 @@ export default class Cypher {
}
return formerSensitiveData;
}
+
+ private static async cleanupBackupSensitiveData(tenantID: string): Promise<void> {
+ // Cleanup former encrypted data
+ const settingsToCleanup = await Cypher.getSettingsWithSensitiveData(tenantID);
+ // If tenant has settings with sensitive data, clean them
+ if (!Utils.isEmptyArray(settingsToCleanup)) {
+ // Cleanup
+ for (const setting of settingsToCleanup) {
+ if (Utils.objectHasProperty(setting, 'formerSensitiveData')) {
+ delete setting.formerSensitiveData;
+ await SettingStorage.saveSettings(tenantID, setting);
+ }
+ }
+ }
+ }
}

Optimize usage of getSettings() of crypto