mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: updated kms service to return only kms details and some more minor changes
This commit is contained in:
@@ -7,6 +7,7 @@ import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { KmsType } from "@app/services/kms/kms-types";
|
||||
|
||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -194,7 +195,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const kmsKeys = await server.services.project.getProjectKmsKeys({
|
||||
const kmsKey = await server.services.project.getProjectKmsKeys({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -202,7 +203,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
return kmsKeys;
|
||||
return kmsKey;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -217,7 +218,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
secretManagerKmsKeyId: z.string()
|
||||
kms: z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(KmsType.Internal) }),
|
||||
z.object({ type: z.literal(KmsType.External), kmsId: z.string() })
|
||||
])
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@@ -23,10 +24,7 @@ import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model";
|
||||
|
||||
type TExternalKmsServiceFactoryDep = {
|
||||
externalKmsDAL: TExternalKmsDALFactory;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
"getOrgKmsKeyId" | "decryptWithInputKey" | "encryptWithInputKey" | "getOrgKmsDataKey"
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "getOrgKmsKeyId" | "createCipherPairWithDataKey">;
|
||||
kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@@ -84,12 +82,12 @@ export const externalKmsServiceFactory = ({
|
||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||
}
|
||||
|
||||
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId);
|
||||
const kmsEncryptor = await kmsService.encryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({
|
||||
const { cipherTextBlob: encryptedProviderInputs } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
||||
});
|
||||
|
||||
@@ -150,13 +148,13 @@ export const externalKmsServiceFactory = ({
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
let sanitizedProviderInput = "";
|
||||
if (provider) {
|
||||
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
|
||||
const kmsDecryptor = await kmsService.decryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
|
||||
await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = kmsDecryptor({
|
||||
if (provider) {
|
||||
const decryptedProviderInputBlob = orgDataKeyDecryptor({
|
||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||
});
|
||||
|
||||
@@ -179,11 +177,7 @@ export const externalKmsServiceFactory = ({
|
||||
|
||||
let encryptedProviderInputs: Buffer | undefined;
|
||||
if (sanitizedProviderInput) {
|
||||
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId);
|
||||
const kmsEncryptor = await kmsService.encryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
});
|
||||
const { cipherTextBlob } = kmsEncryptor({
|
||||
const { cipherTextBlob } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
||||
});
|
||||
encryptedProviderInputs = cipherTextBlob;
|
||||
@@ -266,12 +260,12 @@ export const externalKmsServiceFactory = ({
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
|
||||
const kmsDecryptor = await kmsService.decryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = kmsDecryptor({
|
||||
const decryptedProviderInputBlob = orgDataKeyDecryptor({
|
||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||
});
|
||||
switch (externalKmsDoc.provider) {
|
||||
@@ -306,12 +300,12 @@ export const externalKmsServiceFactory = ({
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
|
||||
const kmsDecryptor = await kmsService.decryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = kmsDecryptor({
|
||||
const decryptedProviderInputBlob = orgDataKeyDecryptor({
|
||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
@@ -89,10 +90,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
"getProjectSecretManagerKmsDataKey" | "encryptWithInputKey" | "decryptWithInputKey"
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||
secretV2BridgeDAL: Pick<
|
||||
TSecretV2BridgeDALFactory,
|
||||
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
|
||||
@@ -214,8 +212,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
let secrets;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const secretManagerDataKey = await kmsService.getProjectSecretManagerKmsDataKey(projectId);
|
||||
const secretManagerDecryptor = await kmsService.decryptWithInputKey({ key: secretManagerDataKey });
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
|
||||
secretApprovalRequest.id
|
||||
);
|
||||
@@ -427,8 +427,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
);
|
||||
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
|
||||
|
||||
const secretManagerDataKey = await kmsService.getProjectSecretManagerKmsDataKey(projectId);
|
||||
const secretManagerDecryptor = await kmsService.decryptWithInputKey({ key: secretManagerDataKey });
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
|
||||
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
|
||||
@@ -1074,8 +1076,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const commits: Omit<TSecretApprovalRequestsSecretsV2Insert, "requestId">[] = [];
|
||||
const commitTagIds: Record<string, string[]> = {};
|
||||
|
||||
const secretManagerDataKey = await kmsService.getProjectSecretManagerKmsDataKey(projectId);
|
||||
const secretManagerEncryptor = await kmsService.encryptWithInputKey({ key: secretManagerDataKey });
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
// for created secret approval change
|
||||
const createdSecrets = data[SecretOperations.Create];
|
||||
|
||||
@@ -10,6 +10,8 @@ export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>;
|
||||
export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
const kmsOrm = ormify(db, TableName.KmsKey);
|
||||
|
||||
// akhilmhdh: this function should never be called outside kms service
|
||||
// why: because the encrypted key should never be shared with another service
|
||||
const findByIdWithAssociatedKms = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db.replicaNode())(TableName.KmsKey)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { KmsKeysSchema } from "@app/db/schemas";
|
||||
import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
|
||||
import {
|
||||
ExternalKmsAwsSchema,
|
||||
@@ -23,12 +25,14 @@ import { TKmsKeyDALFactory } from "./kms-key-dal";
|
||||
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
||||
import {
|
||||
KmsDataKey,
|
||||
KmsType,
|
||||
TDecryptWithKeyDTO,
|
||||
TDecryptWithKmsDTO,
|
||||
TEncryptionWithKeyDTO,
|
||||
TEncryptWithKmsDataKeyDTO,
|
||||
TEncryptWithKmsDTO,
|
||||
TGenerateKMSDTO
|
||||
TGenerateKMSDTO,
|
||||
TUpdateProjectSecretManagerKmsKeyDTO
|
||||
} from "./kms-types";
|
||||
|
||||
type TKmsServiceFactoryDep = {
|
||||
@@ -42,7 +46,6 @@ type TKmsServiceFactoryDep = {
|
||||
|
||||
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
|
||||
|
||||
export const INTERNAL_KMS_KEY_ID = "internal";
|
||||
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
|
||||
@@ -51,6 +54,8 @@ 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;
|
||||
const KmsSanitizedSchema = KmsKeysSchema.extend({ isExternal: z.boolean() });
|
||||
|
||||
export const kmsServiceFactory = ({
|
||||
kmsDAL,
|
||||
kmsRootConfigDAL,
|
||||
@@ -61,7 +66,11 @@ export const kmsServiceFactory = ({
|
||||
}: TKmsServiceFactoryDep) => {
|
||||
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
||||
|
||||
// this is used symmetric encryption
|
||||
/*
|
||||
* Generate KMS Key
|
||||
* This function is responsibile for generating the infisical internal KMS for various entities
|
||||
* Like for secret manager, cert manager or for organization
|
||||
*/
|
||||
const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const kmsKeyMaterial = randomSecureBytes(32);
|
||||
@@ -93,6 +102,11 @@ export const kmsServiceFactory = ({
|
||||
return doc;
|
||||
};
|
||||
|
||||
/*
|
||||
* Simple encryption service function to do all the encryption tasks in infisical
|
||||
* This can be even later exposed directly as api for encryption as function
|
||||
* The encrypted binary even has everything into it. The IV, the version etc
|
||||
*/
|
||||
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
@@ -105,6 +119,10 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Simple decryption service function to do all the encryption tasks in infisical
|
||||
* This can be even later exposed directly as api for encryption as function
|
||||
*/
|
||||
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
|
||||
@@ -115,6 +133,13 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Function to generate a KMS for an org
|
||||
* We handle concurrent with redis locking and waitReady
|
||||
* What happens is first we check kms is assigned else first we acquire lock and create the kms with connection
|
||||
* In mean time the rest of the request will wait until creation is finished followed by getting the created on
|
||||
* In real time this would be milliseconds
|
||||
*/
|
||||
const getOrgKmsKeyId = async (orgId: string) => {
|
||||
let org = await orgDAL.findById(orgId);
|
||||
|
||||
@@ -176,7 +201,12 @@ export const kmsServiceFactory = ({
|
||||
return org.kmsDefaultKeyId;
|
||||
};
|
||||
|
||||
const decryptWithKmsKey = async ({ kmsId }: Omit<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
const decryptWithKmsKey = async ({
|
||||
kmsId,
|
||||
depth = 0
|
||||
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number }) => {
|
||||
if (depth > 2) throw new BadRequestError({ message: "KMS depth max limit" });
|
||||
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: "KMS ID not found" });
|
||||
@@ -189,8 +219,12 @@ export const kmsServiceFactory = ({
|
||||
throw new Error("Invalid organization KMS");
|
||||
}
|
||||
|
||||
// The idea is external kms connection info is encrypted by an org default KMS
|
||||
// This could be external kms(in future) but at the end of the day, the end KMS will be an infisical internal one
|
||||
// we put a limit of depth to avoid too many cycles
|
||||
const orgKmsDecryptor = await decryptWithKmsKey({
|
||||
kmsId: kmsDoc.orgKms.id
|
||||
kmsId: kmsDoc.orgKms.id,
|
||||
depth: depth + 1
|
||||
});
|
||||
|
||||
const orgKmsDataKey = await orgKmsDecryptor({
|
||||
@@ -303,7 +337,7 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getOrgKmsDataKey = async (orgId: string) => {
|
||||
const $getOrgKmsDataKey = async (orgId: string) => {
|
||||
const kmsKeyId = await getOrgKmsKeyId(orgId);
|
||||
let org = await orgDAL.findById(orgId);
|
||||
|
||||
@@ -313,7 +347,7 @@ export const kmsServiceFactory = ({
|
||||
|
||||
if (!org.kmsEncryptedDataKey) {
|
||||
const lock = await keyStore
|
||||
.acquireLock([KeyStorePrefixes.KmsOrgDataKeyCreation, orgId], 3000, { retryCount: 3 })
|
||||
.acquireLock([KeyStorePrefixes.KmsOrgDataKeyCreation, orgId], 500, { retryCount: 0 })
|
||||
.catch(() => null);
|
||||
|
||||
try {
|
||||
@@ -448,14 +482,7 @@ export const kmsServiceFactory = ({
|
||||
return project.kmsSecretManagerKeyId;
|
||||
};
|
||||
|
||||
const getProjectSecretManagerKmsKey = async (projectId: string) => {
|
||||
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
|
||||
const kmsKey = await kmsDAL.findByIdWithAssociatedKms(kmsKeyId);
|
||||
|
||||
return kmsKey;
|
||||
};
|
||||
|
||||
const getProjectSecretManagerKmsDataKey = async (projectId: string) => {
|
||||
const $getProjectSecretManagerKmsDataKey = async (projectId: string) => {
|
||||
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
|
||||
let project = await projectDAL.findById(projectId);
|
||||
|
||||
@@ -528,21 +555,58 @@ export const kmsServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const updateProjectSecretManagerKmsKey = async (projectId: string, kmsId: string) => {
|
||||
const currentKms = await getProjectSecretManagerKmsKey(projectId);
|
||||
const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO) => {
|
||||
switch (dto.type) {
|
||||
case KmsDataKey.SecretManager: {
|
||||
return $getProjectSecretManagerKmsDataKey(dto.projectId);
|
||||
}
|
||||
default: {
|
||||
return $getOrgKmsDataKey(dto.orgId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ((currentKms.isReserved && kmsId === INTERNAL_KMS_KEY_ID) || currentKms.id === kmsId) {
|
||||
return currentKms;
|
||||
// by keeping the decrypted data key in inner scope
|
||||
// none of the entities outside can interact directly or expose the data key
|
||||
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => {
|
||||
const dataKey = await $getDataKey(encryptionContext);
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
|
||||
return {
|
||||
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey);
|
||||
|
||||
// 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 };
|
||||
},
|
||||
decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey);
|
||||
return decryptedBlob;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const updateProjectSecretManagerKmsKey = async ({ projectId, kms }: TUpdateProjectSecretManagerKmsKeyDTO) => {
|
||||
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
|
||||
const currentKms = await kmsDAL.findById(kmsKeyId);
|
||||
|
||||
// case: internal kms -> internal kms. no change needed
|
||||
if (kms.type === KmsType.Internal && currentKms.isReserved) {
|
||||
return KmsSanitizedSchema.parseAsync({ isExternal: false, ...currentKms });
|
||||
}
|
||||
|
||||
if (kmsId !== INTERNAL_KMS_KEY_ID) {
|
||||
if (kms.type === KmsType.External) {
|
||||
// validate kms is scoped in org
|
||||
const { kmsId } = kms;
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found."
|
||||
});
|
||||
}
|
||||
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: "KMS ID not found." });
|
||||
@@ -555,38 +619,36 @@ export const kmsServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const dataKey = await getProjectSecretManagerKmsDataKey(projectId);
|
||||
|
||||
const dataKey = await $getProjectSecretManagerKmsDataKey(projectId);
|
||||
return kmsDAL.transaction(async (tx) => {
|
||||
const project = await projectDAL.findById(projectId, tx);
|
||||
let newKmsId = kmsId;
|
||||
|
||||
if (newKmsId === INTERNAL_KMS_KEY_ID) {
|
||||
const key = await generateKmsKey({
|
||||
let kmsId;
|
||||
if (kms.type === KmsType.Internal) {
|
||||
const internalKms = await generateKmsKey({
|
||||
isReserved: true,
|
||||
orgId: project.orgId,
|
||||
tx
|
||||
});
|
||||
|
||||
newKmsId = key.id;
|
||||
kmsId = internalKms.id;
|
||||
} else {
|
||||
kmsId = kms.kmsId;
|
||||
}
|
||||
|
||||
const kmsEncryptor = await encryptWithKmsKey({ kmsId: newKmsId }, tx);
|
||||
const kmsEncryptor = await encryptWithKmsKey({ kmsId }, tx);
|
||||
const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey });
|
||||
await projectDAL.updateById(
|
||||
projectId,
|
||||
{
|
||||
kmsSecretManagerKeyId: newKmsId,
|
||||
kmsSecretManagerKeyId: kmsId,
|
||||
kmsSecretManagerEncryptedDataKey: cipherTextBlob
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (currentKms.isReserved) {
|
||||
await kmsDAL.deleteById(currentKms.id, tx);
|
||||
}
|
||||
|
||||
return kmsDAL.findByIdWithAssociatedKms(newKmsId, tx);
|
||||
const newKms = await kmsDAL.findById(kmsId, tx);
|
||||
return KmsSanitizedSchema.parseAsync({ isExternal: !currentKms.isReserved, ...newKms });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -598,13 +660,15 @@ export const kmsServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const secretManagerDataKey = await getProjectSecretManagerKmsDataKey(projectId);
|
||||
const secretManagerDataKey = await $getProjectSecretManagerKmsDataKey(projectId);
|
||||
const kmsKeyIdForEncrypt = await getOrgKmsKeyId(project.orgId);
|
||||
const kmsEncryptor = await encryptWithKmsKey({ kmsId: kmsKeyIdForEncrypt });
|
||||
const { cipherTextBlob: encryptedSecretManagerDataKey } = await kmsEncryptor({ plainText: secretManagerDataKey });
|
||||
const { cipherTextBlob: encryptedSecretManagerDataKeyWithOrgKms } = await kmsEncryptor({
|
||||
plainText: secretManagerDataKey
|
||||
});
|
||||
|
||||
// backup format: version.projectId.kmsFunction.kmsId.Base64(encryptedDataKey).verificationHash
|
||||
let secretManagerBackup = `v1.${projectId}.secretManager.${kmsKeyIdForEncrypt}.${encryptedSecretManagerDataKey.toString(
|
||||
let secretManagerBackup = `v1.${projectId}.secretManager.${kmsKeyIdForEncrypt}.${encryptedSecretManagerDataKeyWithOrgKms.toString(
|
||||
"base64"
|
||||
)}`;
|
||||
|
||||
@@ -678,39 +742,8 @@ export const kmsServiceFactory = ({
|
||||
message: "KMS not found"
|
||||
});
|
||||
}
|
||||
|
||||
return kms;
|
||||
};
|
||||
|
||||
const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO) => {
|
||||
switch (dto.type) {
|
||||
case KmsDataKey.SecretManager: {
|
||||
return getProjectSecretManagerKmsDataKey(dto.projectId);
|
||||
}
|
||||
default: {
|
||||
return getProjectSecretManagerKmsDataKey(dto.orgId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => {
|
||||
const dataKey = await $getDataKey(encryptionContext);
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
return {
|
||||
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey);
|
||||
|
||||
// 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 };
|
||||
},
|
||||
decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey);
|
||||
return decryptedBlob;
|
||||
}
|
||||
};
|
||||
const { id, slug, orgId, isExternal } = kms;
|
||||
return { id, slug, orgId, isExternal };
|
||||
};
|
||||
|
||||
const startService = async () => {
|
||||
@@ -762,14 +795,11 @@ export const kmsServiceFactory = ({
|
||||
startService,
|
||||
generateKmsKey,
|
||||
encryptWithKmsKey,
|
||||
encryptWithInputKey,
|
||||
decryptWithKmsKey,
|
||||
encryptWithInputKey,
|
||||
decryptWithInputKey,
|
||||
getOrgKmsKeyId,
|
||||
getProjectSecretManagerKmsKeyId,
|
||||
getOrgKmsDataKey,
|
||||
getProjectSecretManagerKmsDataKey,
|
||||
getProjectSecretManagerKmsKey,
|
||||
updateProjectSecretManagerKmsKey,
|
||||
getProjectKeyBackup,
|
||||
loadProjectKeyBackup,
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
export enum KmsDataKey {
|
||||
Organization,
|
||||
SecretManager
|
||||
// CertificateManager
|
||||
}
|
||||
|
||||
export enum KmsType {
|
||||
External = "external",
|
||||
Internal = "internal"
|
||||
}
|
||||
|
||||
export type TEncryptWithKmsDataKeyDTO =
|
||||
| { type: KmsDataKey.Organization; orgId: string }
|
||||
| { type: KmsDataKey.SecretManager; projectId: string };
|
||||
// akhilmhdh: not implemented yet
|
||||
// | {
|
||||
// type: KmsDataKey.CertificateManager;
|
||||
// projectId: string;
|
||||
// };
|
||||
|
||||
export type TGenerateKMSDTO = {
|
||||
orgId: string;
|
||||
isReserved?: boolean;
|
||||
@@ -27,17 +47,7 @@ export type TDecryptWithKeyDTO = {
|
||||
cipherTextBlob: Buffer;
|
||||
};
|
||||
|
||||
export enum KmsDataKey {
|
||||
Organization,
|
||||
SecretManager
|
||||
// CertificateManager
|
||||
}
|
||||
|
||||
export type TEncryptWithKmsDataKeyDTO =
|
||||
| { type: KmsDataKey.Organization; orgId: string }
|
||||
| { type: KmsDataKey.SecretManager; projectId: string };
|
||||
// akhilmhdh: not implemented yet
|
||||
// | {
|
||||
// type: KmsDataKey.CertificateManager;
|
||||
// projectId: string;
|
||||
// };
|
||||
export type TUpdateProjectSecretManagerKmsKeyDTO = {
|
||||
projectId: string;
|
||||
kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
|
||||
};
|
||||
|
||||
@@ -650,7 +650,7 @@ export const projectServiceFactory = ({
|
||||
|
||||
const updateProjectKmsKey = async ({
|
||||
projectId,
|
||||
secretManagerKmsKeyId,
|
||||
kms,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -666,7 +666,10 @@ export const projectServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
|
||||
|
||||
const secretManagerKmsKey = await kmsService.updateProjectSecretManagerKmsKey(projectId, secretManagerKmsKeyId);
|
||||
const secretManagerKmsKey = await kmsService.updateProjectSecretManagerKmsKey({
|
||||
projectId,
|
||||
kms
|
||||
});
|
||||
|
||||
return {
|
||||
secretManagerKmsKey
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { CaStatus } from "../certificate-authority/certificate-authority-types";
|
||||
import { KmsType } from "../kms/kms-types";
|
||||
|
||||
export enum ProjectFilterType {
|
||||
ID = "id",
|
||||
@@ -106,7 +107,7 @@ export type TListProjectCertsDTO = {
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateProjectKmsDTO = {
|
||||
secretManagerKmsKeyId: string;
|
||||
kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TLoadProjectKmsBackupDTO = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { kmsKeys } from "./queries";
|
||||
import { AddExternalKmsType } from "./types";
|
||||
import { AddExternalKmsType, KmsType } from "./types";
|
||||
|
||||
export const useAddExternalKms = (orgId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -66,7 +66,9 @@ export const useRemoveExternalKms = (orgId: string) => {
|
||||
export const useUpdateProjectKms = (projectId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (updatedData: { secretManagerKmsKeyId: string }) => {
|
||||
mutationFn: async (
|
||||
updatedData: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string }
|
||||
) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/workspace/${projectId}/kms`, updatedData);
|
||||
|
||||
return data;
|
||||
|
||||
@@ -29,6 +29,11 @@ export type KmsListEntry = {
|
||||
};
|
||||
};
|
||||
|
||||
export enum KmsType {
|
||||
Internal = "internal",
|
||||
External = "external"
|
||||
}
|
||||
|
||||
export enum ExternalKmsProvider {
|
||||
AWS = "aws"
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
useUpdateProjectKms
|
||||
} from "@app/hooks/api";
|
||||
import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries";
|
||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||
import { INTERNAL_KMS_KEY_ID, KmsType } from "@app/hooks/api/kms/types";
|
||||
import { Organization, Workspace } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -207,7 +207,9 @@ export const EncryptionTab = () => {
|
||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
|
||||
const { data: activeKms } = useGetActiveProjectKms(currentWorkspace?.id!);
|
||||
|
||||
const { mutateAsync: updateProjectKms } = useUpdateProjectKms(currentWorkspace?.id!);
|
||||
const { mutateAsync: updateProjectKms, isLoading: isUpdatingProjectKms } = useUpdateProjectKms(
|
||||
currentWorkspace?.id!
|
||||
);
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([
|
||||
"createBackupConfirmation",
|
||||
"loadBackup"
|
||||
@@ -239,9 +241,11 @@ export const EncryptionTab = () => {
|
||||
|
||||
const onUpdateProjectKms = async (data: TForm) => {
|
||||
try {
|
||||
await updateProjectKms({
|
||||
secretManagerKmsKeyId: data.kmsKeyId
|
||||
});
|
||||
await updateProjectKms(
|
||||
data.kmsKeyId === INTERNAL_KMS_KEY_ID
|
||||
? { type: KmsType.Internal }
|
||||
: { type: KmsType.External, kmsId: data.kmsKeyId }
|
||||
);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated project KMS",
|
||||
@@ -287,10 +291,9 @@ export const EncryptionTab = () => {
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
{...field}
|
||||
isDisabled={!isAllowed}
|
||||
onValueChange={(e) => {
|
||||
onChange(e);
|
||||
}}
|
||||
isDisabled={!isAllowed || isUpdatingProjectKms}
|
||||
onValueChange={onChange}
|
||||
isLoading={isUpdatingProjectKms}
|
||||
className="w-3/4 bg-mineshaft-600"
|
||||
>
|
||||
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
||||
|
||||
Reference in New Issue
Block a user