mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
misc: finalized switching of project KMS
This commit is contained in:
@@ -226,7 +226,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { secretManagerKmsKey } = await server.services.kms.updateProjectKmsKey({
|
||||
const { secretManagerKmsKey } = await server.services.project.updateProjectKmsKey({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -235,6 +235,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_KMS,
|
||||
metadata: {
|
||||
secretManagerKmsKey: {
|
||||
id: secretManagerKmsKey.id,
|
||||
slug: secretManagerKmsKey.slug
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
secretManagerKmsKey
|
||||
};
|
||||
|
||||
@@ -143,7 +143,8 @@ export enum EventType {
|
||||
CREATE_KMS = "create-kms",
|
||||
UPDATE_KMS = "update-kms",
|
||||
DELETE_KMS = "delete-kms",
|
||||
GET_KMS = "get-kms"
|
||||
GET_KMS = "get-kms",
|
||||
UPDATE_PROJECT_KMS = "update-project-kms"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@@ -1212,6 +1213,16 @@ interface GetKmsEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateProjectKmsEvent {
|
||||
type: EventType.UPDATE_PROJECT_KMS;
|
||||
metadata: {
|
||||
secretManagerKmsKey: {
|
||||
id: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -1317,4 +1328,5 @@ export type Event =
|
||||
| CreateKmsEvent
|
||||
| UpdateKmsEvent
|
||||
| DeleteKmsEvent
|
||||
| GetKmsEvent;
|
||||
| GetKmsEvent
|
||||
| UpdateProjectKmsEvent;
|
||||
|
||||
@@ -72,7 +72,7 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const decryptedCrl = kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
||||
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
||||
const crl = new x509.X509Crl(decryptedCrl);
|
||||
|
||||
const base64crl = crl.toString("base64");
|
||||
|
||||
@@ -28,7 +28,8 @@ export enum ProjectPermissionSub {
|
||||
SecretRotation = "secret-rotation",
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates"
|
||||
Certificates = "certificates",
|
||||
Kms = "kms"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
@@ -60,7 +61,8 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||
|
||||
const buildAdminPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
@@ -157,6 +159,8 @@ const buildAdminPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
||||
@@ -310,8 +310,7 @@ export const registerRoutes = async (
|
||||
kmsDAL,
|
||||
internalKmsDAL,
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
permissionService
|
||||
projectDAL
|
||||
});
|
||||
const externalKmsService = externalKmsServiceFactory({
|
||||
kmsDAL,
|
||||
@@ -627,7 +626,8 @@ export const registerRoutes = async (
|
||||
certificateDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
keyStore
|
||||
keyStore,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const projectEnvService = projectEnvServiceFactory({
|
||||
|
||||
@@ -78,7 +78,7 @@ export const getCaCredentials = async ({
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
const decryptedPrivateKey = kmsDecryptor({
|
||||
const decryptedPrivateKey = await kmsDecryptor({
|
||||
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
@@ -129,13 +129,13 @@ export const getCaCertChain = async ({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const decryptedCaCert = kmsDecryptor({
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
|
||||
const decryptedChain = kmsDecryptor({
|
||||
const decryptedChain = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificateChain
|
||||
});
|
||||
|
||||
@@ -176,7 +176,7 @@ export const rebuildCaCrl = async ({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const privateKey = kmsDecryptor({
|
||||
const privateKey = await kmsDecryptor({
|
||||
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
@@ -210,7 +210,7 @@ export const rebuildCaCrl = async ({
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({
|
||||
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||
});
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export const certificateAuthorityQueueFactory = ({
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
const privateKey = kmsDecryptor({
|
||||
const privateKey = await kmsDecryptor({
|
||||
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
@@ -125,7 +125,7 @@ export const certificateAuthorityQueueFactory = ({
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({
|
||||
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||
});
|
||||
|
||||
|
||||
@@ -181,11 +181,11 @@ export const certificateAuthorityServiceFactory = ({
|
||||
]
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(cert.rawData))
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.alloc(0)
|
||||
});
|
||||
|
||||
@@ -209,7 +209,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
signingKey: keys.privateKey
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({
|
||||
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||
});
|
||||
|
||||
@@ -224,7 +224,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
|
||||
const skObj = KeyObject.from(keys.privateKey);
|
||||
|
||||
const { cipherTextBlob: encryptedPrivateKey } = kmsEncryptor({
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||
plainText: skObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
@@ -458,7 +458,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
const decryptedCaCert = kmsDecryptor({
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
@@ -615,11 +615,11 @@ export const certificateAuthorityServiceFactory = ({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(certObj.rawData))
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificateChain)
|
||||
});
|
||||
|
||||
@@ -693,7 +693,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaCert = kmsDecryptor({
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
@@ -803,7 +803,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||
});
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ export const certificateServiceFactory = ({
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKeyId
|
||||
});
|
||||
const decryptedCert = kmsDecryptor({
|
||||
const decryptedCert = await kmsDecryptor({
|
||||
cipherTextBlob: certBody.encryptedCertificate
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
try {
|
||||
const result = await (tx || db.replicaNode())(TableName.KmsKey)
|
||||
.where({ [`${TableName.KmsKey}.id` as "id"]: id })
|
||||
.join(TableName.Organization, `${TableName.KmsKey}.orgId`, `${TableName.Organization}.id`)
|
||||
.leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
|
||||
.leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`)
|
||||
.first()
|
||||
@@ -31,11 +32,19 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"),
|
||||
db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"),
|
||||
db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails")
|
||||
)
|
||||
.select(
|
||||
db.ref("kmsDefaultKeyId").withSchema(TableName.Organization).as("orgKmsDefaultKeyId"),
|
||||
db.ref("kmsEncryptedDataKey").withSchema(TableName.Organization).as("orgKmsEncryptedDataKey")
|
||||
);
|
||||
|
||||
const data = {
|
||||
...KmsKeysSchema.parse(result),
|
||||
isExternal: Boolean(result?.externalKmsId),
|
||||
orgKms: {
|
||||
id: result?.orgKmsDefaultKeyId,
|
||||
encryptedDataKey: result?.orgKmsEncryptedDataKey
|
||||
},
|
||||
externalKms: result?.externalKmsId
|
||||
? {
|
||||
id: result.externalKmsId,
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
|
||||
import {
|
||||
ExternalKmsAwsSchema,
|
||||
KmsProviders,
|
||||
TExternalKmsProviderFns
|
||||
} from "@app/ee/services/external-kms/providers/model";
|
||||
import { KeyStorePrefixes, 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 { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
@@ -24,8 +25,7 @@ import {
|
||||
TDecryptWithKmsDTO,
|
||||
TEncryptionWithKeyDTO,
|
||||
TEncryptWithKmsDTO,
|
||||
TGenerateKMSDTO,
|
||||
TUpdateProjectKmsDTO
|
||||
TGenerateKMSDTO
|
||||
} from "./kms-types";
|
||||
|
||||
type TKmsServiceFactoryDep = {
|
||||
@@ -34,12 +34,12 @@ type TKmsServiceFactoryDep = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findById" | "updateById" | "transaction">;
|
||||
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
internalKmsDAL: Pick<TInternalKmsDALFactory, "create">;
|
||||
};
|
||||
|
||||
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
|
||||
|
||||
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";
|
||||
@@ -54,8 +54,7 @@ export const kmsServiceFactory = ({
|
||||
keyStore,
|
||||
internalKmsDAL,
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
permissionService
|
||||
projectDAL
|
||||
}: TKmsServiceFactoryDep) => {
|
||||
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
||||
|
||||
@@ -91,22 +90,6 @@ export const kmsServiceFactory = ({
|
||||
return doc;
|
||||
};
|
||||
|
||||
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(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);
|
||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, 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 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);
|
||||
@@ -119,19 +102,6 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const decryptWithKmsKey = async ({ kmsId }: Omit<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
|
||||
return decryptedBlob;
|
||||
};
|
||||
};
|
||||
|
||||
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
|
||||
@@ -146,7 +116,7 @@ export const kmsServiceFactory = ({
|
||||
const keyId = await orgDAL.transaction(async (tx) => {
|
||||
const org = await orgDAL.findById(orgId, tx);
|
||||
if (!org) {
|
||||
throw new BadRequestError({ message: "Org not found" });
|
||||
throw new NotFoundError({ message: "Org not found" });
|
||||
}
|
||||
|
||||
if (!org.kmsDefaultKeyId) {
|
||||
@@ -174,23 +144,154 @@ export const kmsServiceFactory = ({
|
||||
return keyId;
|
||||
};
|
||||
|
||||
const decryptWithKmsKey = async ({ kmsId }: Omit<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: "KMS ID not found" });
|
||||
}
|
||||
|
||||
if (kmsDoc.externalKms) {
|
||||
let externalKms: TExternalKmsProviderFns;
|
||||
|
||||
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
|
||||
throw new Error("Invalid organization KMS");
|
||||
}
|
||||
|
||||
const orgKmsDecryptor = await decryptWithKmsKey({
|
||||
kmsId: kmsDoc.orgKms.id
|
||||
});
|
||||
|
||||
// fetch encryptedDataKey straight from kmsDoc by joining it in query :D
|
||||
const orgKmsDataKey = await orgKmsDecryptor({
|
||||
cipherTextBlob: kmsDoc.orgKms.encryptedDataKey
|
||||
});
|
||||
|
||||
const kmsDecryptor = await decryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = kmsDecryptor({
|
||||
cipherTextBlob: kmsDoc.externalKms.encryptedProviderInput
|
||||
});
|
||||
|
||||
switch (kmsDoc.externalKms.provider) {
|
||||
case KmsProviders.Aws: {
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||
);
|
||||
|
||||
externalKms = await AwsKmsProviderFactory({
|
||||
inputs: decryptedProviderInput
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error("Invalid KMS provider.");
|
||||
}
|
||||
|
||||
return async ({ cipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
const { data } = await externalKms.decrypt(cipherTextBlob);
|
||||
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
// internal KMS
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
|
||||
return Promise.resolve(decryptedBlob);
|
||||
};
|
||||
};
|
||||
|
||||
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: "KMS ID not found" });
|
||||
}
|
||||
|
||||
if (kmsDoc.externalKms) {
|
||||
let externalKms: TExternalKmsProviderFns;
|
||||
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
|
||||
throw new Error("Invalid organization KMS");
|
||||
}
|
||||
|
||||
const orgKmsDecryptor = await decryptWithKmsKey({
|
||||
kmsId: kmsDoc.orgKms.id
|
||||
});
|
||||
|
||||
const orgKmsDataKey = await orgKmsDecryptor({
|
||||
cipherTextBlob: kmsDoc.orgKms.encryptedDataKey
|
||||
});
|
||||
|
||||
const kmsDecryptor = await decryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = kmsDecryptor({
|
||||
cipherTextBlob: kmsDoc.externalKms.encryptedProviderInput
|
||||
});
|
||||
|
||||
switch (kmsDoc.externalKms.provider) {
|
||||
case KmsProviders.Aws: {
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||
);
|
||||
|
||||
externalKms = await AwsKmsProviderFactory({
|
||||
inputs: decryptedProviderInput
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error("Invalid KMS provider.");
|
||||
}
|
||||
|
||||
return async ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const { encryptedBlob } = await externalKms.encrypt(plainText);
|
||||
|
||||
return { cipherTextBlob: encryptedBlob };
|
||||
};
|
||||
}
|
||||
|
||||
// internal KMS
|
||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, 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 Promise.resolve({ cipherTextBlob });
|
||||
};
|
||||
};
|
||||
|
||||
const getOrgKmsDataKey = async (orgId: string) => {
|
||||
const kmsKeyId = await getOrgKmsKeyId(orgId);
|
||||
const orgKmsDataKey = await orgDAL.transaction(async (tx) => {
|
||||
const org = await orgDAL.findById(orgId, tx);
|
||||
|
||||
if (!org) {
|
||||
throw new BadRequestError({ message: "Org not found" });
|
||||
throw new NotFoundError({ message: "Org not found" });
|
||||
}
|
||||
|
||||
let encryptedDataKey = org.kmsEncryptedDataKey;
|
||||
if (!encryptedDataKey) {
|
||||
const dataKey = crypto.randomBytes(32);
|
||||
const kmsEncryptor = await encryptWithKmsKey({
|
||||
kmsId: kmsKeyId
|
||||
});
|
||||
const dataKey = randomSecureBytes();
|
||||
const kmsEncryptor = await encryptWithKmsKey(
|
||||
{
|
||||
kmsId: kmsKeyId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const { cipherTextBlob } = kmsEncryptor({
|
||||
const { cipherTextBlob } = await kmsEncryptor({
|
||||
plainText: dataKey
|
||||
});
|
||||
|
||||
@@ -221,11 +322,10 @@ export const kmsServiceFactory = ({
|
||||
const getProjectSecretManagerKmsKeyId = async (projectId: string) => {
|
||||
let project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new BadRequestError({ message: "Project not found" });
|
||||
throw new NotFoundError({ message: "Project not found" });
|
||||
}
|
||||
|
||||
if (!project.kmsSecretManagerKeyId) {
|
||||
// create default kms key for certificate service
|
||||
const lock = await keyStore
|
||||
.acquireLock([KeyStorePrefixes.KmsProjectKeyCreation, projectId], 3000, { retryCount: 3 })
|
||||
.catch(() => null);
|
||||
@@ -309,7 +409,7 @@ export const kmsServiceFactory = ({
|
||||
kmsId: kmsKeyId
|
||||
});
|
||||
|
||||
const { cipherTextBlob } = kmsEncryptor({
|
||||
const { cipherTextBlob } = await kmsEncryptor({
|
||||
plainText: dataKey
|
||||
});
|
||||
|
||||
@@ -344,17 +444,38 @@ export const kmsServiceFactory = ({
|
||||
|
||||
const updateProjectSecretManagerKmsKey = async (projectId: string, kmsId: string) => {
|
||||
const currentKms = await getProjectSecretManagerKmsKey(projectId);
|
||||
const dataKey = await getProjectSecretManagerKmsDataKey(projectId);
|
||||
|
||||
if (currentKms.isReserved && kmsId === "internal") {
|
||||
if ((currentKms.isReserved && kmsId === INTERNAL_KMS_KEY_ID) || currentKms.id === kmsId) {
|
||||
return currentKms;
|
||||
}
|
||||
|
||||
if (kmsId !== INTERNAL_KMS_KEY_ID) {
|
||||
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." });
|
||||
}
|
||||
|
||||
if (kmsDoc.orgId !== project.orgId) {
|
||||
throw new BadRequestError({
|
||||
message: "KMS ID does not belong in the organization."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const dataKey = await getProjectSecretManagerKmsDataKey(projectId);
|
||||
|
||||
return kmsDAL.transaction(async (tx) => {
|
||||
const project = await projectDAL.findById(projectId, tx);
|
||||
let newKmsId = kmsId;
|
||||
|
||||
if (newKmsId === "internal") {
|
||||
if (newKmsId === INTERNAL_KMS_KEY_ID) {
|
||||
const key = await generateKmsKey({
|
||||
isReserved: true,
|
||||
orgId: project.orgId,
|
||||
@@ -364,8 +485,8 @@ export const kmsServiceFactory = ({
|
||||
newKmsId = key.id;
|
||||
}
|
||||
|
||||
const kmsEncryptor = await encryptWithKmsKey({ kmsId: newKmsId });
|
||||
const { cipherTextBlob } = kmsEncryptor({ plainText: dataKey });
|
||||
const kmsEncryptor = await encryptWithKmsKey({ kmsId: newKmsId }, tx);
|
||||
const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey });
|
||||
await projectDAL.updateById(
|
||||
projectId,
|
||||
{
|
||||
@@ -379,45 +500,10 @@ export const kmsServiceFactory = ({
|
||||
await kmsDAL.deleteById(currentKms.id, tx);
|
||||
}
|
||||
|
||||
return kmsDAL.findByIdWithAssociatedKms(newKmsId);
|
||||
return kmsDAL.findByIdWithAssociatedKms(newKmsId, tx);
|
||||
});
|
||||
};
|
||||
|
||||
const updateProjectKmsKey = async ({
|
||||
projectId,
|
||||
secretManagerKmsKeyId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TUpdateProjectKmsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
if (secretManagerKmsKeyId !== "internal") {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(secretManagerKmsKeyId);
|
||||
if (!kmsDoc) {
|
||||
throw new BadRequestError({ message: "KMS ID not found." });
|
||||
}
|
||||
|
||||
if (kmsDoc.orgId !== actorOrgId) {
|
||||
throw new BadRequestError({
|
||||
message: "KMS ID does not belong in the organization."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
secretManagerKmsKey: await updateProjectSecretManagerKmsKey(projectId, secretManagerKmsKeyId)
|
||||
};
|
||||
};
|
||||
|
||||
const startService = async () => {
|
||||
const appCfg = getConfig();
|
||||
// This will switch to a seal process and HMS flow in future
|
||||
@@ -475,7 +561,6 @@ export const kmsServiceFactory = ({
|
||||
getOrgKmsDataKey,
|
||||
getProjectSecretManagerKmsDataKey,
|
||||
getProjectSecretManagerKmsKey,
|
||||
updateProjectKmsKey,
|
||||
updateProjectSecretManagerKmsKey
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TGenerateKMSDTO = {
|
||||
orgId: string;
|
||||
isReserved?: boolean;
|
||||
@@ -28,7 +26,3 @@ export type TDecryptWithKeyDTO = {
|
||||
key: Buffer;
|
||||
cipherTextBlob: Buffer;
|
||||
};
|
||||
|
||||
export type TUpdateProjectKmsDTO = {
|
||||
secretManagerKmsKeyId: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { TCertificateAuthorityDALFactory } from "../certificate-authority/certif
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
TToggleProjectAutoCapitalizationDTO,
|
||||
TUpdateAuditLogsRetentionDTO,
|
||||
TUpdateProjectDTO,
|
||||
TUpdateProjectKmsDTO,
|
||||
TUpdateProjectNameDTO,
|
||||
TUpdateProjectVersionLimitDTO,
|
||||
TUpgradeProjectDTO
|
||||
@@ -76,6 +78,7 @@ type TProjectServiceFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||
kmsService: Pick<TKmsServiceFactory, "updateProjectSecretManagerKmsKey">;
|
||||
};
|
||||
|
||||
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||
@@ -100,7 +103,8 @@ export const projectServiceFactory = ({
|
||||
identityProjectMembershipRoleDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
keyStore
|
||||
keyStore,
|
||||
kmsService
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
/*
|
||||
* Create workspace. Make user the admin
|
||||
@@ -664,6 +668,31 @@ export const projectServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const updateProjectKmsKey = async ({
|
||||
projectId,
|
||||
secretManagerKmsKeyId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TUpdateProjectKmsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
|
||||
|
||||
const secretManagerKmsKey = await kmsService.updateProjectSecretManagerKmsKey(projectId, secretManagerKmsKeyId);
|
||||
|
||||
return {
|
||||
secretManagerKmsKey
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createProject,
|
||||
deleteProject,
|
||||
@@ -677,6 +706,7 @@ export const projectServiceFactory = ({
|
||||
listProjectCas,
|
||||
listProjectCertificates,
|
||||
updateVersionLimit,
|
||||
updateAuditLogsRetention
|
||||
updateAuditLogsRetention,
|
||||
updateProjectKmsKey
|
||||
};
|
||||
};
|
||||
|
||||
@@ -103,3 +103,7 @@ export type TListProjectCertsDTO = {
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateProjectKmsDTO = {
|
||||
secretManagerKmsKeyId: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
@@ -26,7 +26,8 @@ export enum ProjectPermissionSub {
|
||||
SecretRotation = "secret-rotation",
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates"
|
||||
Certificates = "certificates",
|
||||
Kms = "kms"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, ContentLoader, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { Button, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
@@ -19,30 +20,41 @@ const formSchema = z.object({
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
const INTERNAL_KMS_KEY_ID = "internal";
|
||||
|
||||
export const EncryptionTab = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: externalKmsList, isLoading: isExternalKmsListLoading } = useGetExternalKmsList(
|
||||
currentOrg?.id!
|
||||
);
|
||||
const { data: activeKms, isLoading: isActiveKmsLoading } = useGetActiveProjectKms(
|
||||
currentWorkspace?.id!
|
||||
);
|
||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
|
||||
const { data: activeKms } = useGetActiveProjectKms(currentWorkspace?.id!);
|
||||
|
||||
const { mutateAsync: updateProjectKms } = useUpdateProjectKms(currentWorkspace?.id!);
|
||||
const [kmsKeyId, setKmsKeyId] = useState("");
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting }
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
kmsKeyId: activeKms?.isExternal ? activeKms?.id : "internal"
|
||||
}
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (activeKms) {
|
||||
setKmsKeyId(activeKms.isExternal ? activeKms.id : INTERNAL_KMS_KEY_ID);
|
||||
} else {
|
||||
setKmsKeyId(INTERNAL_KMS_KEY_ID);
|
||||
}
|
||||
}, [activeKms]);
|
||||
|
||||
useEffect(() => {
|
||||
if (kmsKeyId) {
|
||||
setValue("kmsKeyId", kmsKeyId);
|
||||
}
|
||||
}, [kmsKeyId]);
|
||||
|
||||
const onFormSubmit = async (data: TForm) => {
|
||||
try {
|
||||
await updateProjectKms({
|
||||
@@ -68,41 +80,42 @@ export const EncryptionTab = () => {
|
||||
Select which Key Management System to use for encrypting your project data
|
||||
</p>
|
||||
<div className="mb-6 max-w-md">
|
||||
{isExternalKmsListLoading || isActiveKmsLoading ? (
|
||||
<ContentLoader />
|
||||
) : (
|
||||
<Controller
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-3/4 bg-mineshaft-600"
|
||||
>
|
||||
<SelectItem value="internal" key="kms-internal">
|
||||
Default Infisical KMS
|
||||
</SelectItem>
|
||||
{externalKmsList?.map((kms) => (
|
||||
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||
{kms.slug}
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Kms}>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
{...field}
|
||||
isDisabled={!isAllowed}
|
||||
onValueChange={(e) => {
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-3/4 bg-mineshaft-600"
|
||||
>
|
||||
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
||||
Default Infisical KMS
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="kmsKeyId"
|
||||
/>
|
||||
)}
|
||||
{externalKmsList?.map((kms) => (
|
||||
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||
{kms.slug}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="kmsKeyId"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isDisabled={!isAllowed || isSubmitting}
|
||||
isDisabled={!isAllowed || isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
|
||||
Reference in New Issue
Block a user