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:
@@ -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"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "InfraConfig" ADD COLUMN "isEncrypted" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user