mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 12:27:28 +00:00
Merge pull request #1917 from akhilmhdh/feat/internal-kms
Internal functions for KMS
This commit is contained in:
@@ -40,13 +40,14 @@ jobs:
|
|||||||
REDIS_URL: redis://172.17.0.1:6379
|
REDIS_URL: redis://172.17.0.1:6379
|
||||||
DB_CONNECTION_URI: postgres://infisical:[email protected]:5432/infisical?sslmode=disable
|
DB_CONNECTION_URI: postgres://infisical:[email protected]:5432/infisical?sslmode=disable
|
||||||
JWT_AUTH_SECRET: something-random
|
JWT_AUTH_SECRET: something-random
|
||||||
|
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21.5'
|
go-version: '1.21.5'
|
||||||
- name: Wait for container to be stable and check logs
|
- name: Wait for container to be stable and check logs
|
||||||
run: |
|
run: |
|
||||||
SECONDS=0
|
SECONDS=0
|
||||||
HEALTHY=0
|
r HEALTHY=0
|
||||||
while [ $SECONDS -lt 60 ]; do
|
while [ $SECONDS -lt 60 ]; do
|
||||||
if docker ps | grep infisical-api | grep -q healthy; then
|
if docker ps | grep infisical-api | grep -q healthy; then
|
||||||
echo "Container is healthy."
|
echo "Container is healthy."
|
||||||
@@ -73,4 +74,4 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker-compose -f "docker-compose.dev.yml" down
|
docker-compose -f "docker-compose.dev.yml" down
|
||||||
docker stop infisical-api
|
docker stop infisical-api
|
||||||
docker remove infisical-api
|
docker remove infisical-api
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { Lock } from "@app/lib/red-lock";
|
||||||
|
|
||||||
export const mockKeyStore = (): TKeyStoreFactory => {
|
export const mockKeyStore = (): TKeyStoreFactory => {
|
||||||
const store: Record<string, string | number | Buffer> = {};
|
const store: Record<string, string | number | Buffer> = {};
|
||||||
@@ -27,7 +28,10 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
|||||||
return 1;
|
return 1;
|
||||||
},
|
},
|
||||||
acquireLock: () => {
|
acquireLock: () => {
|
||||||
throw new Error("Not implemented");
|
return Promise.resolve({
|
||||||
}
|
release: () => {}
|
||||||
|
}) as Promise<Lock>;
|
||||||
|
},
|
||||||
|
waitTillReady: async () => {}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ const getZodPrimitiveType = (type: string) => {
|
|||||||
return "z.coerce.number()";
|
return "z.coerce.number()";
|
||||||
case "text":
|
case "text":
|
||||||
return "z.string()";
|
return "z.string()";
|
||||||
|
case "bytea":
|
||||||
|
return "zodBuffer";
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid type: ${type}`);
|
throw new Error(`Invalid type: ${type}`);
|
||||||
}
|
}
|
||||||
@@ -96,10 +98,15 @@ const main = async () => {
|
|||||||
const columnNames = Object.keys(columns);
|
const columnNames = Object.keys(columns);
|
||||||
|
|
||||||
let schema = "";
|
let schema = "";
|
||||||
|
const zodImportSet = new Set<string>();
|
||||||
for (let colNum = 0; colNum < columnNames.length; colNum++) {
|
for (let colNum = 0; colNum < columnNames.length; colNum++) {
|
||||||
const columnName = columnNames[colNum];
|
const columnName = columnNames[colNum];
|
||||||
const colInfo = columns[columnName];
|
const colInfo = columns[columnName];
|
||||||
let ztype = getZodPrimitiveType(colInfo.type);
|
let ztype = getZodPrimitiveType(colInfo.type);
|
||||||
|
if (["zodBuffer"].includes(ztype)) {
|
||||||
|
zodImportSet.add(ztype);
|
||||||
|
}
|
||||||
|
|
||||||
// don't put optional on id
|
// don't put optional on id
|
||||||
if (colInfo.defaultValue && columnName !== "id") {
|
if (colInfo.defaultValue && columnName !== "id") {
|
||||||
const { defaultValue } = colInfo;
|
const { defaultValue } = colInfo;
|
||||||
@@ -121,6 +128,8 @@ const main = async () => {
|
|||||||
.split("_")
|
.split("_")
|
||||||
.reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, "");
|
.reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, "");
|
||||||
|
|
||||||
|
const zodImports = Array.from(zodImportSet);
|
||||||
|
|
||||||
// the insert and update are changed to zod input type to use default cases
|
// the insert and update are changed to zod input type to use default cases
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
||||||
@@ -131,6 +140,8 @@ const main = async () => {
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
${zodImports.length ? `import { ${zodImports.join(",")} } from \"@app/lib/zod\";` : ""}
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const ${pascalCase}Schema = z.object({${schema}});
|
export const ${pascalCase}Schema = z.object({${schema}});
|
||||||
|
|||||||
Vendored
+20
-1
@@ -98,6 +98,15 @@ import {
|
|||||||
TIntegrations,
|
TIntegrations,
|
||||||
TIntegrationsInsert,
|
TIntegrationsInsert,
|
||||||
TIntegrationsUpdate,
|
TIntegrationsUpdate,
|
||||||
|
TKmsKeys,
|
||||||
|
TKmsKeysInsert,
|
||||||
|
TKmsKeysUpdate,
|
||||||
|
TKmsKeyVersions,
|
||||||
|
TKmsKeyVersionsInsert,
|
||||||
|
TKmsKeyVersionsUpdate,
|
||||||
|
TKmsRootConfig,
|
||||||
|
TKmsRootConfigInsert,
|
||||||
|
TKmsRootConfigUpdate,
|
||||||
TLdapConfigs,
|
TLdapConfigs,
|
||||||
TLdapConfigsInsert,
|
TLdapConfigsInsert,
|
||||||
TLdapConfigsUpdate,
|
TLdapConfigsUpdate,
|
||||||
@@ -176,6 +185,9 @@ import {
|
|||||||
TSecretImports,
|
TSecretImports,
|
||||||
TSecretImportsInsert,
|
TSecretImportsInsert,
|
||||||
TSecretImportsUpdate,
|
TSecretImportsUpdate,
|
||||||
|
TSecretReferences,
|
||||||
|
TSecretReferencesInsert,
|
||||||
|
TSecretReferencesUpdate,
|
||||||
TSecretRotationOutputs,
|
TSecretRotationOutputs,
|
||||||
TSecretRotationOutputsInsert,
|
TSecretRotationOutputsInsert,
|
||||||
TSecretRotationOutputsUpdate,
|
TSecretRotationOutputsUpdate,
|
||||||
@@ -240,7 +252,6 @@ import {
|
|||||||
TWebhooksInsert,
|
TWebhooksInsert,
|
||||||
TWebhooksUpdate
|
TWebhooksUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { TSecretReferences, TSecretReferencesInsert, TSecretReferencesUpdate } from "@app/db/schemas/secret-references";
|
|
||||||
|
|
||||||
declare module "knex/types/tables" {
|
declare module "knex/types/tables" {
|
||||||
interface Tables {
|
interface Tables {
|
||||||
@@ -514,5 +525,13 @@ declare module "knex/types/tables" {
|
|||||||
TSecretVersionTagJunctionInsert,
|
TSecretVersionTagJunctionInsert,
|
||||||
TSecretVersionTagJunctionUpdate
|
TSecretVersionTagJunctionUpdate
|
||||||
>;
|
>;
|
||||||
|
// KMS service
|
||||||
|
[TableName.KmsServerRootConfig]: Knex.CompositeTableType<
|
||||||
|
TKmsRootConfig,
|
||||||
|
TKmsRootConfigInsert,
|
||||||
|
TKmsRootConfigUpdate
|
||||||
|
>;
|
||||||
|
[TableName.KmsKey]: Knex.CompositeTableType<TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate>;
|
||||||
|
[TableName.KmsKeyVersion]: Knex.CompositeTableType<TKmsKeyVersions, TKmsKeyVersionsInsert, TKmsKeyVersionsUpdate>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.KmsServerRootConfig))) {
|
||||||
|
await knex.schema.createTable(TableName.KmsServerRootConfig, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.binary("encryptedRootKey").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.KmsServerRootConfig);
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.KmsKey))) {
|
||||||
|
await knex.schema.createTable(TableName.KmsKey, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.binary("encryptedKey").notNullable();
|
||||||
|
t.string("encryptionAlgorithm").notNullable();
|
||||||
|
t.integer("version").defaultTo(1).notNullable();
|
||||||
|
t.string("description");
|
||||||
|
t.boolean("isDisabled").defaultTo(false);
|
||||||
|
t.boolean("isReserved").defaultTo(true);
|
||||||
|
t.string("projectId");
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.uuid("orgId");
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.KmsKey);
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.KmsKeyVersion))) {
|
||||||
|
await knex.schema.createTable(TableName.KmsKeyVersion, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.binary("encryptedKey").notNullable();
|
||||||
|
t.integer("version").notNullable();
|
||||||
|
t.uuid("kmsKeyId").notNullable();
|
||||||
|
t.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.KmsKeyVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.KmsServerRootConfig);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.KmsServerRootConfig);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.KmsKeyVersion);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.KmsKeyVersion);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.KmsKey);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.KmsKey);
|
||||||
|
}
|
||||||
@@ -30,6 +30,9 @@ export * from "./identity-universal-auths";
|
|||||||
export * from "./incident-contacts";
|
export * from "./incident-contacts";
|
||||||
export * from "./integration-auths";
|
export * from "./integration-auths";
|
||||||
export * from "./integrations";
|
export * from "./integrations";
|
||||||
|
export * from "./kms-key-versions";
|
||||||
|
export * from "./kms-keys";
|
||||||
|
export * from "./kms-root-config";
|
||||||
export * from "./ldap-configs";
|
export * from "./ldap-configs";
|
||||||
export * from "./ldap-group-maps";
|
export * from "./ldap-group-maps";
|
||||||
export * from "./models";
|
export * from "./models";
|
||||||
@@ -57,6 +60,7 @@ export * from "./secret-blind-indexes";
|
|||||||
export * from "./secret-folder-versions";
|
export * from "./secret-folder-versions";
|
||||||
export * from "./secret-folders";
|
export * from "./secret-folders";
|
||||||
export * from "./secret-imports";
|
export * from "./secret-imports";
|
||||||
|
export * from "./secret-references";
|
||||||
export * from "./secret-rotation-outputs";
|
export * from "./secret-rotation-outputs";
|
||||||
export * from "./secret-rotations";
|
export * from "./secret-rotations";
|
||||||
export * from "./secret-scanning-git-risks";
|
export * from "./secret-scanning-git-risks";
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const KmsKeyVersionsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
encryptedKey: zodBuffer,
|
||||||
|
version: z.number(),
|
||||||
|
kmsKeyId: z.string().uuid()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TKmsKeyVersions = z.infer<typeof KmsKeyVersionsSchema>;
|
||||||
|
export type TKmsKeyVersionsInsert = Omit<z.input<typeof KmsKeyVersionsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TKmsKeyVersionsUpdate = Partial<Omit<z.input<typeof KmsKeyVersionsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const KmsKeysSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
encryptedKey: zodBuffer,
|
||||||
|
encryptionAlgorithm: z.string(),
|
||||||
|
version: z.number().default(1),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
isDisabled: z.boolean().default(false).nullable().optional(),
|
||||||
|
isReserved: z.boolean().default(true).nullable().optional(),
|
||||||
|
projectId: z.string().nullable().optional(),
|
||||||
|
orgId: z.string().uuid().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||||
|
export type TKmsKeysInsert = Omit<z.input<typeof KmsKeysSchema>, TImmutableDBKeys>;
|
||||||
|
export type TKmsKeysUpdate = Partial<Omit<z.input<typeof KmsKeysSchema>, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const KmsRootConfigSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
encryptedRootKey: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;
|
||||||
|
export type TKmsRootConfigInsert = Omit<z.input<typeof KmsRootConfigSchema>, TImmutableDBKeys>;
|
||||||
|
export type TKmsRootConfigUpdate = Partial<Omit<z.input<typeof KmsRootConfigSchema>, TImmutableDBKeys>>;
|
||||||
@@ -81,7 +81,11 @@ export enum TableName {
|
|||||||
DynamicSecretLease = "dynamic_secret_leases",
|
DynamicSecretLease = "dynamic_secret_leases",
|
||||||
// junction tables with tags
|
// junction tables with tags
|
||||||
JnSecretTag = "secret_tag_junction",
|
JnSecretTag = "secret_tag_junction",
|
||||||
SecretVersionTag = "secret_version_tag_junction"
|
SecretVersionTag = "secret_version_tag_junction",
|
||||||
|
// KMS Service
|
||||||
|
KmsServerRootConfig = "kms_root_config",
|
||||||
|
KmsKey = "kms_keys",
|
||||||
|
KmsKeyVersion = "kms_key_versions"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ export enum KeyStorePrefixes {
|
|||||||
SecretReplication = "secret-replication-import-lock"
|
SecretReplication = "secret-replication-import-lock"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TWaitTillReady = {
|
||||||
|
key: string;
|
||||||
|
waitingCb?: () => void;
|
||||||
|
keyCheckCb: (val: string | null) => boolean;
|
||||||
|
waitIteration?: number;
|
||||||
|
delay?: number;
|
||||||
|
jitter?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export const keyStoreFactory = (redisUrl: string) => {
|
export const keyStoreFactory = (redisUrl: string) => {
|
||||||
const redis = new Redis(redisUrl);
|
const redis = new Redis(redisUrl);
|
||||||
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
|
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
|
||||||
@@ -29,6 +38,29 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
|
|
||||||
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
||||||
|
|
||||||
|
const waitTillReady = async ({
|
||||||
|
key,
|
||||||
|
waitingCb,
|
||||||
|
keyCheckCb,
|
||||||
|
waitIteration = 10,
|
||||||
|
delay = 1000,
|
||||||
|
jitter = 200
|
||||||
|
}: TWaitTillReady) => {
|
||||||
|
let attempts = 0;
|
||||||
|
let isReady = keyCheckCb(await getItem(key));
|
||||||
|
while (!isReady) {
|
||||||
|
if (attempts > waitIteration) return;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
waitingCb?.();
|
||||||
|
setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
|
||||||
|
});
|
||||||
|
attempts += 1;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
isReady = keyCheckCb(await getItem(key, "wait_till_ready"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setItem,
|
setItem,
|
||||||
getItem,
|
getItem,
|
||||||
@@ -37,6 +69,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
incrementBy,
|
incrementBy,
|
||||||
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
||||||
return redisLock.acquire(resources, duration, settings);
|
return redisLock.acquire(resources, duration, settings);
|
||||||
}
|
},
|
||||||
|
waitTillReady
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
|
||||||
|
|
||||||
|
const getIvLength = () => {
|
||||||
|
return 12;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTagLength = () => {
|
||||||
|
return 16;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
|
||||||
|
const IV_LENGTH = getIvLength();
|
||||||
|
const TAG_LENGTH = getTagLength();
|
||||||
|
|
||||||
|
const encrypt = (text: Buffer, key: Buffer) => {
|
||||||
|
const iv = crypto.randomBytes(IV_LENGTH);
|
||||||
|
const cipher = crypto.createCipheriv(type, key, iv);
|
||||||
|
|
||||||
|
let encrypted = cipher.update(text);
|
||||||
|
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||||
|
|
||||||
|
// Get the authentication tag
|
||||||
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
// Concatenate IV, encrypted text, and tag into a single buffer
|
||||||
|
const ciphertextBlob = Buffer.concat([iv, encrypted, tag]);
|
||||||
|
return ciphertextBlob;
|
||||||
|
};
|
||||||
|
|
||||||
|
const decrypt = (ciphertextBlob: Buffer, key: Buffer) => {
|
||||||
|
// Extract the IV, encrypted text, and tag from the buffer
|
||||||
|
const iv = ciphertextBlob.subarray(0, IV_LENGTH);
|
||||||
|
const tag = ciphertextBlob.subarray(-TAG_LENGTH);
|
||||||
|
const encrypted = ciphertextBlob.subarray(IV_LENGTH, -TAG_LENGTH);
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv(type, key, iv);
|
||||||
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
|
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
||||||
|
return decrypted;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
encrypt,
|
||||||
|
decrypt
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { symmetricCipherService } from "./cipher";
|
||||||
|
export { SymmetricEncryption } from "./types";
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export enum SymmetricEncryption {
|
||||||
|
AES_GCM_256 = "aes-256-gcm",
|
||||||
|
AES_GCM_128 = "aes-128-gcm"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TSymmetricEncryptionFns = {
|
||||||
|
encrypt: (text: Buffer, key: Buffer) => Buffer;
|
||||||
|
decrypt: (blob: Buffer, key: Buffer) => Buffer;
|
||||||
|
};
|
||||||
@@ -11,6 +11,8 @@ import { getConfig } from "../config/env";
|
|||||||
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
|
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
|
||||||
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
|
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
|
||||||
|
|
||||||
|
export const randomSecureBytes = (length = 32) => crypto.randomBytes(length);
|
||||||
|
|
||||||
export type TDecryptSymmetricInput = {
|
export type TDecryptSymmetricInput = {
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export {
|
|||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
encryptSymmetric128BitHexKeyUTF8,
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
generateAsymmetricKeyPair
|
generateAsymmetricKeyPair,
|
||||||
|
randomSecureBytes
|
||||||
} from "./encryption";
|
} from "./encryption";
|
||||||
export {
|
export {
|
||||||
decryptIntegrationAuths,
|
decryptIntegrationAuths,
|
||||||
|
|||||||
@@ -7,3 +7,7 @@ export const zpStr = <T extends ZodTypeAny>(schema: T, opt: { stripNull: boolean
|
|||||||
if (typeof val !== "string") return val;
|
if (typeof val !== "string") return val;
|
||||||
return val.trim() || undefined;
|
return val.trim() || undefined;
|
||||||
}, schema);
|
}, schema);
|
||||||
|
|
||||||
|
export const zodBuffer = z.custom<Buffer>((data) => Buffer.isBuffer(data) || data instanceof Uint8Array, {
|
||||||
|
message: "Expected binary data (Buffer Or Uint8Array)"
|
||||||
|
});
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ import { integrationDALFactory } from "@app/services/integration/integration-dal
|
|||||||
import { integrationServiceFactory } from "@app/services/integration/integration-service";
|
import { integrationServiceFactory } from "@app/services/integration/integration-service";
|
||||||
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
|
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
|
||||||
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||||
|
import { kmsDALFactory } from "@app/services/kms/kms-dal";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
|
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||||
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||||
@@ -261,6 +264,9 @@ export const registerRoutes = async (
|
|||||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||||
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
||||||
|
|
||||||
|
const kmsDAL = kmsDALFactory(db);
|
||||||
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||||
|
|
||||||
const permissionService = permissionServiceFactory({
|
const permissionService = permissionServiceFactory({
|
||||||
permissionDAL,
|
permissionDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@@ -269,6 +275,12 @@ export const registerRoutes = async (
|
|||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
||||||
|
const kmsService = kmsServiceFactory({
|
||||||
|
kmsRootConfigDAL,
|
||||||
|
keyStore,
|
||||||
|
kmsDAL
|
||||||
|
});
|
||||||
|
|
||||||
const trustedIpService = trustedIpServiceFactory({
|
const trustedIpService = trustedIpServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -823,6 +835,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
await telemetryQueue.startTelemetryCheck();
|
await telemetryQueue.startTelemetryCheck();
|
||||||
await dailyResourceCleanUp.startCleanUp();
|
await dailyResourceCleanUp.startCleanUp();
|
||||||
|
await kmsService.startService();
|
||||||
|
|
||||||
// inject all services
|
// inject all services
|
||||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TKmsDALFactory = ReturnType<typeof kmsDALFactory>;
|
||||||
|
|
||||||
|
export const kmsDALFactory = (db: TDbClient) => {
|
||||||
|
const kmsOrm = ormify(db, TableName.KmsKey);
|
||||||
|
return kmsOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;
|
||||||
|
|
||||||
|
export const kmsRootConfigDALFactory = (db: TDbClient) => {
|
||||||
|
const kmsOrm = ormify(db, TableName.KmsServerRootConfig);
|
||||||
|
return kmsOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { randomSecureBytes } from "@app/lib/crypto";
|
||||||
|
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TKmsDALFactory } from "./kms-dal";
|
||||||
|
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
||||||
|
import { TDecryptWithKmsDTO, TEncryptWithKmsDTO, TGenerateKMSDTO } from "./kms-types";
|
||||||
|
|
||||||
|
type TKmsServiceFactoryDep = {
|
||||||
|
kmsDAL: TKmsDALFactory;
|
||||||
|
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
|
||||||
|
|
||||||
|
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
|
||||||
|
const KMS_ROOT_CREATION_WAIT_TIME = 10;
|
||||||
|
|
||||||
|
// akhilmhdh: Don't edit this value. This is measured for blob concatination in kms
|
||||||
|
const KMS_VERSION = "v01";
|
||||||
|
const KMS_VERSION_BLOB_LENGTH = 3;
|
||||||
|
export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsServiceFactoryDep) => {
|
||||||
|
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
||||||
|
|
||||||
|
// this is used symmetric encryption
|
||||||
|
const generateKmsKey = async ({ scopeId, scopeType, isReserved = true }: TGenerateKMSDTO) => {
|
||||||
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
|
const kmsKeyMaterial = randomSecureBytes(32);
|
||||||
|
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
const { encryptedKey, ...doc } = await kmsDAL.create({
|
||||||
|
version: 1,
|
||||||
|
encryptedKey: encryptedKeyMaterial,
|
||||||
|
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
|
||||||
|
isReserved,
|
||||||
|
orgId: scopeType === "org" ? scopeId : undefined,
|
||||||
|
projectId: scopeType === "project" ? scopeId : undefined
|
||||||
|
});
|
||||||
|
return doc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const encrypt = async ({ kmsId, plainText }: TEncryptWithKmsDTO) => {
|
||||||
|
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||||
|
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
|
||||||
|
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||||
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
|
|
||||||
|
const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
|
||||||
|
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
|
||||||
|
|
||||||
|
// Buffer#1 encrypted text + Buffer#2 version number
|
||||||
|
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
|
||||||
|
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
|
||||||
|
return { cipherTextBlob };
|
||||||
|
};
|
||||||
|
|
||||||
|
const decrypt = async ({ cipherTextBlob: versionedCipherTextBlob, kmsId }: TDecryptWithKmsDTO) => {
|
||||||
|
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||||
|
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
|
||||||
|
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||||
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
|
const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||||
|
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
|
||||||
|
return decryptedBlob;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startService = async () => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
// This will switch to a seal process and HMS flow in future
|
||||||
|
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
|
||||||
|
// if root key its base64 encoded
|
||||||
|
const isBase64 = Boolean(appCfg.ROOT_ENCRYPTION_KEY);
|
||||||
|
if (!encryptionKey) throw new Error("Root encryption key not found for KMS service.");
|
||||||
|
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
|
||||||
|
|
||||||
|
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
|
||||||
|
if (!lock) {
|
||||||
|
await keyStore.waitTillReady({
|
||||||
|
key: KMS_ROOT_CREATION_WAIT_KEY,
|
||||||
|
keyCheckCb: (val) => val === "true",
|
||||||
|
waitingCb: () => logger.info("KMS. Waiting for leader to finish creation of KMS Root Key")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if KMS root key was already generated and saved in DB
|
||||||
|
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
|
||||||
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
|
if (kmsRootConfig) {
|
||||||
|
if (lock) await lock.release();
|
||||||
|
logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting.");
|
||||||
|
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
||||||
|
// set the flag so that other instancen nodes can start
|
||||||
|
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
||||||
|
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||||
|
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("KMS: Generating ROOT Key");
|
||||||
|
const newRootKey = randomSecureBytes(32);
|
||||||
|
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
|
||||||
|
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
|
||||||
|
await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID });
|
||||||
|
|
||||||
|
// set the flag so that other instancen nodes can start
|
||||||
|
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
||||||
|
logger.info("KMS: Saved and loaded ROOT Key into memory");
|
||||||
|
if (lock) await lock.release();
|
||||||
|
ROOT_ENCRYPTION_KEY = newRootKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
startService,
|
||||||
|
generateKmsKey,
|
||||||
|
encrypt,
|
||||||
|
decrypt
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export type TGenerateKMSDTO = {
|
||||||
|
scopeType: "project" | "org";
|
||||||
|
scopeId: string;
|
||||||
|
isReserved?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TEncryptWithKmsDTO = {
|
||||||
|
kmsId: string;
|
||||||
|
plainText: Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDecryptWithKmsDTO = {
|
||||||
|
kmsId: string;
|
||||||
|
cipherTextBlob: Buffer;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user