HSB-473 feat: encrypt sensitive data before storing in db (#4212)

* feat: encryption added on onMuduleInit

* feat: encryption changes added on sh admin mutations and query

* chore: fetch minimum column from DB

* feat: data encryption added on account table

* test: infra config test case update

* chore: env example modified

* chore: update variable name

* chore: refactor the code

* feat: client-ids made encrypted

* chore: encrypted auth client id's

---------

Co-authored-by: Balu Babu <balub997@gmail.com>
This commit is contained in:
Mir Arif Hasan
2024-08-14 13:34:12 +06:00
committed by GitHub
parent 3f78bf1062
commit 67f0324521
8 changed files with 213 additions and 26 deletions

View File

@@ -13,6 +13,9 @@ SESSION_SECRET='add some secret here'
# Note: Some auth providers may not support http requests # Note: Some auth providers may not support http requests
ALLOW_SECURE_COOKIES=true ALLOW_SECURE_COOKIES=true
# Sensitive Data Encryption Key while storing in Database (32 character)
DATA_ENCRYPTION_KEY="data encryption key with 32 char"
# Hoppscotch App Domain Config # Hoppscotch App Domain Config
REDIRECT_URL="http://localhost:3000" REDIRECT_URL="http://localhost:3000"
WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100" WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100"

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "InfraConfig" ADD COLUMN "isEncrypted" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -214,12 +214,13 @@ enum TeamMemberRole {
} }
model InfraConfig { model InfraConfig {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String @unique
value String? value String?
active Boolean @default(true) // Use case: Let's say, Admin wants to disable Google SSO, but doesn't want to delete the config isEncrypted Boolean @default(false) // Use case: Let's say, Admin wants to store a Secret Key, but doesn't want to store it in plain text in `value` column
createdOn DateTime @default(now()) @db.Timestamp(3) active Boolean @default(true) // Use case: Let's say, Admin wants to disable Google SSO, but doesn't want to delete the config
updatedOn DateTime @updatedAt @db.Timestamp(3) createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
} }
model PersonalAccessToken { model PersonalAccessToken {

View File

@@ -5,7 +5,7 @@ import {
} from 'src/errors'; } from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfigEnum } from 'src/types/InfraConfig'; import { InfraConfigEnum } from 'src/types/InfraConfig';
import { throwErr } from 'src/utils'; import { decrypt, encrypt, throwErr } from 'src/utils';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
export enum ServiceStatus { export enum ServiceStatus {
@@ -60,7 +60,11 @@ export async function loadInfraConfiguration() {
let environmentObject: Record<string, any> = {}; let environmentObject: Record<string, any> = {};
infraConfigs.forEach((infraConfig) => { infraConfigs.forEach((infraConfig) => {
environmentObject[infraConfig.name] = infraConfig.value; if (infraConfig.isEncrypted) {
environmentObject[infraConfig.name] = decrypt(infraConfig.value);
} else {
environmentObject[infraConfig.name] = infraConfig.value;
}
}); });
return { INFRA: environmentObject }; return { INFRA: environmentObject };
@@ -76,119 +80,150 @@ export async function loadInfraConfiguration() {
* @returns Array of default infra configs * @returns Array of default infra configs
*/ */
export async function getDefaultInfraConfigs(): Promise< export async function getDefaultInfraConfigs(): Promise<
{ name: InfraConfigEnum; value: string }[] { name: InfraConfigEnum; value: string; isEncrypted: boolean }[]
> { > {
const prisma = new PrismaService(); const prisma = new PrismaService();
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name' // Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [ const infraConfigDefaultObjs: {
name: InfraConfigEnum;
value: string;
isEncrypted: boolean;
}[] = [
{ {
name: InfraConfigEnum.MAILER_SMTP_ENABLE, name: InfraConfigEnum.MAILER_SMTP_ENABLE,
value: process.env.MAILER_SMTP_ENABLE ?? 'true', value: process.env.MAILER_SMTP_ENABLE ?? 'true',
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS, name: InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS,
value: process.env.MAILER_USE_CUSTOM_CONFIGS ?? 'false', value: process.env.MAILER_USE_CUSTOM_CONFIGS ?? 'false',
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.MAILER_SMTP_URL, name: InfraConfigEnum.MAILER_SMTP_URL,
value: process.env.MAILER_SMTP_URL, value: encrypt(process.env.MAILER_SMTP_URL),
isEncrypted: true,
}, },
{ {
name: InfraConfigEnum.MAILER_ADDRESS_FROM, name: InfraConfigEnum.MAILER_ADDRESS_FROM,
value: process.env.MAILER_ADDRESS_FROM, value: process.env.MAILER_ADDRESS_FROM,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.MAILER_SMTP_HOST, name: InfraConfigEnum.MAILER_SMTP_HOST,
value: process.env.MAILER_SMTP_HOST, value: process.env.MAILER_SMTP_HOST,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.MAILER_SMTP_PORT, name: InfraConfigEnum.MAILER_SMTP_PORT,
value: process.env.MAILER_SMTP_PORT, value: process.env.MAILER_SMTP_PORT,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.MAILER_SMTP_SECURE, name: InfraConfigEnum.MAILER_SMTP_SECURE,
value: process.env.MAILER_SMTP_SECURE, value: process.env.MAILER_SMTP_SECURE,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.MAILER_SMTP_USER, name: InfraConfigEnum.MAILER_SMTP_USER,
value: process.env.MAILER_SMTP_USER, value: process.env.MAILER_SMTP_USER,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.MAILER_SMTP_PASSWORD, name: InfraConfigEnum.MAILER_SMTP_PASSWORD,
value: process.env.MAILER_SMTP_PASSWORD, value: encrypt(process.env.MAILER_SMTP_PASSWORD),
isEncrypted: true,
}, },
{ {
name: InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED, name: InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
value: process.env.MAILER_TLS_REJECT_UNAUTHORIZED, value: process.env.MAILER_TLS_REJECT_UNAUTHORIZED,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.GOOGLE_CLIENT_ID, name: InfraConfigEnum.GOOGLE_CLIENT_ID,
value: process.env.GOOGLE_CLIENT_ID, value: encrypt(process.env.GOOGLE_CLIENT_ID),
isEncrypted: true,
}, },
{ {
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET, name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
value: process.env.GOOGLE_CLIENT_SECRET, value: encrypt(process.env.GOOGLE_CLIENT_SECRET),
isEncrypted: true,
}, },
{ {
name: InfraConfigEnum.GOOGLE_CALLBACK_URL, name: InfraConfigEnum.GOOGLE_CALLBACK_URL,
value: process.env.GOOGLE_CALLBACK_URL, value: process.env.GOOGLE_CALLBACK_URL,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.GOOGLE_SCOPE, name: InfraConfigEnum.GOOGLE_SCOPE,
value: process.env.GOOGLE_SCOPE, value: process.env.GOOGLE_SCOPE,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.GITHUB_CLIENT_ID, name: InfraConfigEnum.GITHUB_CLIENT_ID,
value: process.env.GITHUB_CLIENT_ID, value: encrypt(process.env.GITHUB_CLIENT_ID),
isEncrypted: true,
}, },
{ {
name: InfraConfigEnum.GITHUB_CLIENT_SECRET, name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
value: process.env.GITHUB_CLIENT_SECRET, value: encrypt(process.env.GITHUB_CLIENT_SECRET),
isEncrypted: true,
}, },
{ {
name: InfraConfigEnum.GITHUB_CALLBACK_URL, name: InfraConfigEnum.GITHUB_CALLBACK_URL,
value: process.env.GITHUB_CALLBACK_URL, value: process.env.GITHUB_CALLBACK_URL,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.GITHUB_SCOPE, name: InfraConfigEnum.GITHUB_SCOPE,
value: process.env.GITHUB_SCOPE, value: process.env.GITHUB_SCOPE,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.MICROSOFT_CLIENT_ID, name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
value: process.env.MICROSOFT_CLIENT_ID, value: encrypt(process.env.MICROSOFT_CLIENT_ID),
isEncrypted: true,
}, },
{ {
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET, name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
value: process.env.MICROSOFT_CLIENT_SECRET, value: encrypt(process.env.MICROSOFT_CLIENT_SECRET),
isEncrypted: true,
}, },
{ {
name: InfraConfigEnum.MICROSOFT_CALLBACK_URL, name: InfraConfigEnum.MICROSOFT_CALLBACK_URL,
value: process.env.MICROSOFT_CALLBACK_URL, value: process.env.MICROSOFT_CALLBACK_URL,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.MICROSOFT_SCOPE, name: InfraConfigEnum.MICROSOFT_SCOPE,
value: process.env.MICROSOFT_SCOPE, value: process.env.MICROSOFT_SCOPE,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.MICROSOFT_TENANT, name: InfraConfigEnum.MICROSOFT_TENANT,
value: process.env.MICROSOFT_TENANT, value: process.env.MICROSOFT_TENANT,
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
value: getConfiguredSSOProviders(), value: getConfiguredSSOProviders(),
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION, name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
value: false.toString(), value: false.toString(),
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.ANALYTICS_USER_ID, name: InfraConfigEnum.ANALYTICS_USER_ID,
value: generateAnalyticsUserId(), value: generateAnalyticsUserId(),
isEncrypted: false,
}, },
{ {
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP, name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
value: (await prisma.infraConfig.count()) === 0 ? 'true' : 'false', value: (await prisma.infraConfig.count()) === 0 ? 'true' : 'false',
isEncrypted: false,
}, },
]; ];
@@ -214,12 +249,33 @@ export async function getMissingInfraConfigEntries() {
return missingEntries; return missingEntries;
} }
/**
* Get the encryption required entries in the 'infra_config' table
* @returns Array of InfraConfig
*/
export async function getEncryptionRequiredInfraConfigEntries() {
const prisma = new PrismaService();
const [dbInfraConfigs, infraConfigDefaultObjs] = await Promise.all([
prisma.infraConfig.findMany(),
getDefaultInfraConfigs(),
]);
const requiredEncryption = dbInfraConfigs.filter((dbConfig) => {
const defaultConfig = infraConfigDefaultObjs.find(
(config) => config.name === dbConfig.name,
);
if (!defaultConfig) return false;
return defaultConfig.isEncrypted !== dbConfig.isEncrypted;
});
return requiredEncryption;
}
/** /**
* Verify if 'infra_config' table is loaded with all entries * Verify if 'infra_config' table is loaded with all entries
* @returns boolean * @returns boolean
*/ */
export async function isInfraConfigTablePopulated(): Promise<boolean> { export async function isInfraConfigTablePopulated(): Promise<boolean> {
const prisma = new PrismaService();
try { try {
const propsRemainingToInsert = await getMissingInfraConfigEntries(); const propsRemainingToInsert = await getMissingInfraConfigEntries();

View File

@@ -28,6 +28,7 @@ const dbInfraConfigs: dbInfraConfig[] = [
id: '3', id: '3',
name: InfraConfigEnum.GOOGLE_CLIENT_ID, name: InfraConfigEnum.GOOGLE_CLIENT_ID,
value: 'abcdefghijkl', value: 'abcdefghijkl',
isEncrypted: false,
active: true, active: true,
createdOn: INITIALIZED_DATE_CONST, createdOn: INITIALIZED_DATE_CONST,
updatedOn: INITIALIZED_DATE_CONST, updatedOn: INITIALIZED_DATE_CONST,
@@ -36,6 +37,7 @@ const dbInfraConfigs: dbInfraConfig[] = [
id: '4', id: '4',
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
value: 'google', value: 'google',
isEncrypted: false,
active: true, active: true,
createdOn: INITIALIZED_DATE_CONST, createdOn: INITIALIZED_DATE_CONST,
updatedOn: INITIALIZED_DATE_CONST, updatedOn: INITIALIZED_DATE_CONST,
@@ -62,10 +64,15 @@ describe('InfraConfigService', () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID; const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true'; const value = 'true';
// @ts-ignore
mockPrisma.infraConfig.findUnique.mockResolvedValueOnce({
isEncrypted: false,
});
mockPrisma.infraConfig.update.mockResolvedValueOnce({ mockPrisma.infraConfig.update.mockResolvedValueOnce({
id: '', id: '',
name, name,
value, value,
isEncrypted: false,
active: true, active: true,
createdOn: new Date(), createdOn: new Date(),
updatedOn: new Date(), updatedOn: new Date(),
@@ -82,10 +89,15 @@ describe('InfraConfigService', () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID; const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true'; const value = 'true';
// @ts-ignore
mockPrisma.infraConfig.findUnique.mockResolvedValueOnce({
isEncrypted: false,
});
mockPrisma.infraConfig.update.mockResolvedValueOnce({ mockPrisma.infraConfig.update.mockResolvedValueOnce({
id: '', id: '',
name, name,
value, value,
isEncrypted: false,
active: true, active: true,
createdOn: new Date(), createdOn: new Date(),
updatedOn: new Date(), updatedOn: new Date(),
@@ -102,10 +114,15 @@ describe('InfraConfigService', () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID; const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true'; const value = 'true';
// @ts-ignore
mockPrisma.infraConfig.findUnique.mockResolvedValueOnce({
isEncrypted: false,
});
mockPrisma.infraConfig.update.mockResolvedValueOnce({ mockPrisma.infraConfig.update.mockResolvedValueOnce({
id: '', id: '',
name, name,
value, value,
isEncrypted: false,
active: true, active: true,
createdOn: new Date(), createdOn: new Date(),
updatedOn: new Date(), updatedOn: new Date(),
@@ -120,6 +137,11 @@ describe('InfraConfigService', () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID; const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true'; const value = 'true';
// @ts-ignore
mockPrisma.infraConfig.findUnique.mockResolvedValueOnce({
isEncrypted: false,
});
jest.spyOn(helper, 'stopApp').mockReturnValueOnce(); jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
await infraConfigService.update(name, value); await infraConfigService.update(name, value);
@@ -151,6 +173,7 @@ describe('InfraConfigService', () => {
id: '', id: '',
name, name,
value, value,
isEncrypted: false,
active: true, active: true,
createdOn: new Date(), createdOn: new Date(),
updatedOn: new Date(), updatedOn: new Date(),

View File

@@ -15,6 +15,8 @@ import {
INFRA_CONFIG_OPERATION_NOT_ALLOWED, INFRA_CONFIG_OPERATION_NOT_ALLOWED,
} from 'src/errors'; } from 'src/errors';
import { import {
decrypt,
encrypt,
throwErr, throwErr,
validateSMTPEmail, validateSMTPEmail,
validateSMTPUrl, validateSMTPUrl,
@@ -24,6 +26,7 @@ import { ConfigService } from '@nestjs/config';
import { import {
ServiceStatus, ServiceStatus,
getDefaultInfraConfigs, getDefaultInfraConfigs,
getEncryptionRequiredInfraConfigEntries,
getMissingInfraConfigEntries, getMissingInfraConfigEntries,
stopApp, stopApp,
} from './helper'; } from './helper';
@@ -62,10 +65,30 @@ export class InfraConfigService implements OnModuleInit {
*/ */
async initializeInfraConfigTable() { async initializeInfraConfigTable() {
try { try {
// Adding missing InfraConfigs to the database (with encrypted values)
const propsToInsert = await getMissingInfraConfigEntries(); const propsToInsert = await getMissingInfraConfigEntries();
if (propsToInsert.length > 0) { if (propsToInsert.length > 0) {
await this.prisma.infraConfig.createMany({ data: propsToInsert }); await this.prisma.infraConfig.createMany({ data: propsToInsert });
}
// Encrypting previous InfraConfigs that are required to be encrypted
const encryptionRequiredEntries =
await getEncryptionRequiredInfraConfigEntries();
if (encryptionRequiredEntries.length > 0) {
const dbOperations = encryptionRequiredEntries.map((dbConfig) => {
return this.prisma.infraConfig.update({
where: { name: dbConfig.name },
data: { value: encrypt(dbConfig.value), isEncrypted: true },
});
});
await Promise.allSettled(dbOperations);
}
// Restart the app if needed
if (propsToInsert.length > 0 || encryptionRequiredEntries.length > 0) {
stopApp(); stopApp();
} }
} catch (error) { } catch (error) {
@@ -76,6 +99,7 @@ export class InfraConfigService implements OnModuleInit {
// Prisma error code for 'Table does not exist' // Prisma error code for 'Table does not exist'
throwErr(DATABASE_TABLE_NOT_EXIST); throwErr(DATABASE_TABLE_NOT_EXIST);
} else { } else {
console.log(error);
throwErr(error); throwErr(error);
} }
} }
@@ -87,9 +111,13 @@ export class InfraConfigService implements OnModuleInit {
* @returns InfraConfig model * @returns InfraConfig model
*/ */
cast(dbInfraConfig: DBInfraConfig) { cast(dbInfraConfig: DBInfraConfig) {
const plainValue = dbInfraConfig.isEncrypted
? decrypt(dbInfraConfig.value)
: dbInfraConfig.value;
return <InfraConfig>{ return <InfraConfig>{
name: dbInfraConfig.name, name: dbInfraConfig.name,
value: dbInfraConfig.value ?? '', value: plainValue ?? '',
}; };
} }
@@ -99,10 +127,16 @@ export class InfraConfigService implements OnModuleInit {
*/ */
async getInfraConfigsMap() { async getInfraConfigsMap() {
const infraConfigs = await this.prisma.infraConfig.findMany(); const infraConfigs = await this.prisma.infraConfig.findMany();
const infraConfigMap: Record<string, string> = {}; const infraConfigMap: Record<string, string> = {};
infraConfigs.forEach((config) => { infraConfigs.forEach((config) => {
infraConfigMap[config.name] = config.value; if (config.isEncrypted) {
infraConfigMap[config.name] = decrypt(config.value);
} else {
infraConfigMap[config.name] = config.value;
}
}); });
return infraConfigMap; return infraConfigMap;
} }
@@ -118,9 +152,14 @@ export class InfraConfigService implements OnModuleInit {
if (E.isLeft(isValidate)) return E.left(isValidate.left); if (E.isLeft(isValidate)) return E.left(isValidate.left);
try { try {
const { isEncrypted } = await this.prisma.infraConfig.findUnique({
where: { name },
select: { isEncrypted: true },
});
const infraConfig = await this.prisma.infraConfig.update({ const infraConfig = await this.prisma.infraConfig.update({
where: { name }, where: { name },
data: { value }, data: { value: isEncrypted ? encrypt(value) : value },
}); });
if (restartEnabled) stopApp(); if (restartEnabled) stopApp();
@@ -146,11 +185,23 @@ export class InfraConfigService implements OnModuleInit {
if (E.isLeft(isValidate)) return E.left(isValidate.left); if (E.isLeft(isValidate)) return E.left(isValidate.left);
try { try {
const dbInfraConfig = await this.prisma.infraConfig.findMany({
select: { name: true, isEncrypted: true },
});
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
for (let i = 0; i < infraConfigs.length; i++) { for (let i = 0; i < infraConfigs.length; i++) {
const isEncrypted = dbInfraConfig.find(
(p) => p.name === infraConfigs[i].name,
)?.isEncrypted;
await tx.infraConfig.update({ await tx.infraConfig.update({
where: { name: infraConfigs[i].name }, where: { name: infraConfigs[i].name },
data: { value: infraConfigs[i].value }, data: {
value: isEncrypted
? encrypt(infraConfigs[i].value)
: infraConfigs[i].value,
},
}); });
} }
}); });

View File

@@ -16,7 +16,7 @@ import {
import { SessionType, User } from './user.model'; import { SessionType, User } from './user.model';
import { USER_UPDATE_FAILED } from 'src/errors'; import { USER_UPDATE_FAILED } from 'src/errors';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { stringToJson, taskEitherValidateArraySeq } from 'src/utils'; import { encrypt, stringToJson, taskEitherValidateArraySeq } from 'src/utils';
import { UserDataHandler } from './user.data.handler'; import { UserDataHandler } from './user.data.handler';
import { User as DbUser } from '@prisma/client'; import { User as DbUser } from '@prisma/client';
import { OffsetPaginationArgs } from 'src/types/input-types.args'; import { OffsetPaginationArgs } from 'src/types/input-types.args';
@@ -208,8 +208,8 @@ export class UserService {
data: { data: {
provider: profile.provider, provider: profile.provider,
providerAccountId: profile.id, providerAccountId: profile.id,
providerRefreshToken: refreshToken ? refreshToken : null, providerRefreshToken: refreshToken ? encrypt(refreshToken) : null,
providerAccessToken: accessToken ? accessToken : null, providerAccessToken: accessToken ? encrypt(accessToken) : null,
user: { user: {
connect: { connect: {
uid: user.uid, uid: user.uid,

View File

@@ -17,6 +17,7 @@ import {
} from './errors'; } from './errors';
import { TeamMemberRole } from './team/team.model'; import { TeamMemberRole } from './team/team.model';
import { RESTError } from './types/RESTError'; import { RESTError } from './types/RESTError';
import * as crypto from 'crypto';
/** /**
* A workaround to throw an exception in an expression. * A workaround to throw an exception in an expression.
@@ -316,3 +317,53 @@ export function transformCollectionData(
? collectionData ? collectionData
: JSON.stringify(collectionData); : JSON.stringify(collectionData);
} }
// Encrypt and Decrypt functions. InfraConfig and Account table uses these functions to encrypt and decrypt the data.
const ENCRYPTION_ALGORITHM = 'aes-256-cbc';
/**
* Encrypts a text using a key
* @param text The text to encrypt
* @param key The key to use for encryption
* @returns The encrypted text
*/
export function encrypt(text: string, key = process.env.DATA_ENCRYPTION_KEY) {
if (text === null || text === undefined) return text;
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(
ENCRYPTION_ALGORITHM,
Buffer.from(key),
iv,
);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');
}
/**
* Decrypts a text using a key
* @param text The text to decrypt
* @param key The key to use for decryption
* @returns The decrypted text
*/
export function decrypt(
encryptedData: string,
key = process.env.DATA_ENCRYPTION_KEY,
) {
if (encryptedData === null || encryptedData === undefined) {
return encryptedData;
}
const textParts = encryptedData.split(':');
const iv = Buffer.from(textParts.shift(), 'hex');
const encryptedText = Buffer.from(textParts.join(':'), 'hex');
const decipher = crypto.createDecipheriv(
ENCRYPTION_ALGORITHM,
Buffer.from(key),
iv,
);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}