mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 11:44:31 +00:00
feat: HSM support
This commit is contained in:
7
backend/package-lock.json
generated
7
backend/package-lock.json
generated
@@ -90,6 +90,7 @@
|
|||||||
"safe-regex": "^2.1.1",
|
"safe-regex": "^2.1.1",
|
||||||
"scim-patch": "^0.8.3",
|
"scim-patch": "^0.8.3",
|
||||||
"scim2-parse-filter": "^0.2.10",
|
"scim2-parse-filter": "^0.2.10",
|
||||||
|
"secrets.js-grempe": "^2.0.0",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"snowflake-sdk": "^1.14.0",
|
"snowflake-sdk": "^1.14.0",
|
||||||
@@ -18365,6 +18366,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
||||||
"integrity": "sha512-k5TgGSuQEbR4jXRgw/GPAYVL9fMp1pWA2abLF5z3q9IGWSuZTqbrZBOSUezvc+rtViXr+czSZjg3eAN4QSTvxQ=="
|
"integrity": "sha512-k5TgGSuQEbR4jXRgw/GPAYVL9fMp1pWA2abLF5z3q9IGWSuZTqbrZBOSUezvc+rtViXr+czSZjg3eAN4QSTvxQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/secrets.js-grempe": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/secrets.js-grempe/-/secrets.js-grempe-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-4xkOIaDAg998dTFXZUJTOoVbdLHfB818SMeLJ69ABccgGEKokxsoRFupAFfAImloUSKv4QUGNMgKVbKMf6z0Ug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/secure-json-parse": {
|
"node_modules/secure-json-parse": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
||||||
|
|||||||
@@ -195,6 +195,7 @@
|
|||||||
"safe-regex": "^2.1.1",
|
"safe-regex": "^2.1.1",
|
||||||
"scim-patch": "^0.8.3",
|
"scim-patch": "^0.8.3",
|
||||||
"scim2-parse-filter": "^0.2.10",
|
"scim2-parse-filter": "^0.2.10",
|
||||||
|
"secrets.js-grempe": "^2.0.0",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"snowflake-sdk": "^1.14.0",
|
"snowflake-sdk": "^1.14.0",
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import { TableName } from "../schemas";
|
|||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy");
|
const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy");
|
||||||
|
const hasExported = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "exported");
|
||||||
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
|
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
|
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
|
||||||
if (!hasEncryptionStrategy) t.string("encryptionStrategy").defaultTo("BASIC");
|
if (!hasEncryptionStrategy) t.string("encryptionStrategy").defaultTo("BASIC");
|
||||||
|
if (!hasExported) t.boolean("exported").defaultTo(false);
|
||||||
if (!hasTimestampsCol) t.timestamps(true, true, true);
|
if (!hasTimestampsCol) t.timestamps(true, true, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -15,9 +17,11 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy");
|
const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy");
|
||||||
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
|
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
|
||||||
|
const hasExported = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "exported");
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
|
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
|
||||||
if (hasEncryptionStrategy) t.dropColumn("encryptionStrategy");
|
if (hasEncryptionStrategy) t.dropColumn("encryptionStrategy");
|
||||||
if (hasTimestampsCol) t.dropTimestamps(true);
|
if (hasTimestampsCol) t.dropTimestamps(true);
|
||||||
|
if (hasExported) t.dropColumn("exported");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const KmsRootConfigSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedRootKey: zodBuffer,
|
encryptedRootKey: zodBuffer,
|
||||||
encryptionStrategy: z.string(),
|
encryptionStrategy: z.string(),
|
||||||
|
exported: z.boolean(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ const envSchema = z
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.transform((val) => {
|
.transform((val) => {
|
||||||
if (process.env.NODE_ENV === "development") return "/usr/local/lib/softhsm/libsofthsm2.so";
|
// if (process.env.NODE_ENV === "development") return "/usr/local/lib/softhsm/libsofthsm2.so";
|
||||||
return val;
|
return val;
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
@@ -201,6 +201,11 @@ const envSchema = z
|
|||||||
HSM_SLOT: z.coerce.number().optional().default(0),
|
HSM_SLOT: z.coerce.number().optional().default(0),
|
||||||
HSM_MECHANISM: zpStr(z.string().optional().default("AES_GCM"))
|
HSM_MECHANISM: zpStr(z.string().optional().default("AES_GCM"))
|
||||||
})
|
})
|
||||||
|
// To ensure that basic encryption is always possible.
|
||||||
|
.refine(
|
||||||
|
(data) => data.ENCRYPTION_KEY != null || data.ROOT_ENCRYPTION_KEY != null,
|
||||||
|
"Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined."
|
||||||
|
)
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
...data,
|
...data,
|
||||||
|
|
||||||
|
|||||||
@@ -18,5 +18,6 @@ export {
|
|||||||
decryptSecrets,
|
decryptSecrets,
|
||||||
decryptSecretVersions
|
decryptSecretVersions
|
||||||
} from "./secret-encryption";
|
} from "./secret-encryption";
|
||||||
|
export { shamirsService } from "./shamirs";
|
||||||
export { verifyOfflineLicense } from "./signing";
|
export { verifyOfflineLicense } from "./signing";
|
||||||
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
||||||
|
|||||||
38
backend/src/lib/crypto/shamirs.ts
Normal file
38
backend/src/lib/crypto/shamirs.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import shamirs from "secrets.js-grempe";
|
||||||
|
|
||||||
|
import { getConfig } from "../config/env";
|
||||||
|
import { symmetricCipherService, SymmetricEncryption } from "./cipher";
|
||||||
|
|
||||||
|
export const shamirsService = () => {
|
||||||
|
const $generateBasicEncryptionKey = () => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
|
||||||
|
const isBase64 = !appCfg.ENCRYPTION_KEY;
|
||||||
|
if (!encryptionKey)
|
||||||
|
throw new Error(
|
||||||
|
"Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?"
|
||||||
|
);
|
||||||
|
|
||||||
|
return Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
|
||||||
|
};
|
||||||
|
|
||||||
|
const share = (secretBuffer: Buffer, partsCount: number, thresholdCount: number) => {
|
||||||
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
|
const hexSecret = Buffer.from(cipher.encrypt(secretBuffer, $generateBasicEncryptionKey())).toString("hex");
|
||||||
|
|
||||||
|
const secretParts = shamirs.share(hexSecret, partsCount, thresholdCount);
|
||||||
|
return secretParts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const combine = (parts: string[]) => {
|
||||||
|
const encryptedSecret = shamirs.combine(parts);
|
||||||
|
|
||||||
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
|
const decryptedSecret = cipher.decrypt(Buffer.from(encryptedSecret, "hex"), $generateBasicEncryptionKey());
|
||||||
|
|
||||||
|
return decryptedSecret;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { share, combine };
|
||||||
|
};
|
||||||
@@ -572,6 +572,7 @@ export const registerRoutes = async (
|
|||||||
userDAL,
|
userDAL,
|
||||||
authService: loginService,
|
authService: loginService,
|
||||||
serverCfgDAL: superAdminDAL,
|
serverCfgDAL: superAdminDAL,
|
||||||
|
kmsRootConfigDAL,
|
||||||
orgService,
|
orgService,
|
||||||
keyStore,
|
keyStore,
|
||||||
licenseService,
|
licenseService,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
@@ -195,6 +196,107 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/kms-export",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretParts: z.array(z.string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async () => {
|
||||||
|
const keyParts = await server.services.superAdmin.exportPlainKmsKey();
|
||||||
|
|
||||||
|
return {
|
||||||
|
secretParts: keyParts
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/kms-import",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
secretParts: z.array(z.string())
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.superAdmin.importPlainKmsKey(req.body.secretParts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/root-kms-config",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
strategies: z
|
||||||
|
.object({
|
||||||
|
strategy: z.nativeEnum(RootKeyEncryptionStrategy),
|
||||||
|
name: z.string(),
|
||||||
|
enabled: z.boolean()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
keyExported: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handler: async () => {
|
||||||
|
const encryptionDetails = await server.services.superAdmin.getConfiguredEncryptionStrategies();
|
||||||
|
return encryptionDetails;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/encryption-strategies",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
strategy: z.nativeEnum(RootKeyEncryptionStrategy)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.superAdmin.updateRootEncryptionStrategy(req.body.strategy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/signup",
|
url: "/signup",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
|
|
||||||
|
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
|
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
|
||||||
switch (encryptionAlgorithm) {
|
switch (encryptionAlgorithm) {
|
||||||
case SymmetricEncryption.AES_GCM_128:
|
case SymmetricEncryption.AES_GCM_128:
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import {
|
|||||||
} from "@app/ee/services/external-kms/providers/model";
|
} from "@app/ee/services/external-kms/providers/model";
|
||||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { randomSecureBytes } from "@app/lib/crypto";
|
import { randomSecureBytes, shamirsService } from "@app/lib/crypto";
|
||||||
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
import { generateHash } from "@app/lib/crypto/encryption";
|
import { generateHash } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { getByteLengthForAlgorithm } from "@app/services/kms/kms-fns";
|
import { getByteLengthForAlgorithm, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns";
|
||||||
|
|
||||||
import { THsmServiceFactory } from "../hsm/hsm-service";
|
import { THsmServiceFactory } from "../hsm/hsm-service";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
@@ -50,8 +50,6 @@ type TKmsServiceFactoryDep = {
|
|||||||
|
|
||||||
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
|
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_KEY = "wait_till_ready_kms_root_key";
|
||||||
const KMS_ROOT_CREATION_WAIT_TIME = 10;
|
const KMS_ROOT_CREATION_WAIT_TIME = 10;
|
||||||
|
|
||||||
@@ -614,7 +612,7 @@ export const kmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const $createBasicEncryptionKey = () => {
|
const $getBasicEncryptionKey = () => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
|
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
|
||||||
@@ -642,7 +640,7 @@ export const kmsServiceFactory = ({
|
|||||||
// case 2: root key is encrypted with basic encryption
|
// case 2: root key is encrypted with basic encryption
|
||||||
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Basic) {
|
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Basic) {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
const encryptionKeyBuffer = $createBasicEncryptionKey();
|
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||||
|
|
||||||
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
||||||
}
|
}
|
||||||
@@ -660,7 +658,7 @@ export const kmsServiceFactory = ({
|
|||||||
|
|
||||||
if (strategy === RootKeyEncryptionStrategy.Basic) {
|
if (strategy === RootKeyEncryptionStrategy.Basic) {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
const encryptionKeyBuffer = $createBasicEncryptionKey();
|
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||||
|
|
||||||
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
|
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
|
||||||
}
|
}
|
||||||
@@ -669,6 +667,31 @@ export const kmsServiceFactory = ({
|
|||||||
throw new Error(`Invalid root key encryption strategy: ${strategy}`);
|
throw new Error(`Invalid root key encryption strategy: ${strategy}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportRootEncryptionKeyParts = () => {
|
||||||
|
if (!ROOT_ENCRYPTION_KEY) {
|
||||||
|
throw new Error("Root encryption key not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = shamirsService().share(ROOT_ENCRYPTION_KEY, 8, 4);
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const importRootEncryptionKey = async (parts: string[]) => {
|
||||||
|
const decryptedRootKey = shamirsService().combine(parts);
|
||||||
|
|
||||||
|
const encryptedRootKey = symmetricCipherService(SymmetricEncryption.AES_GCM_256).encrypt(
|
||||||
|
decryptedRootKey,
|
||||||
|
$getBasicEncryptionKey()
|
||||||
|
);
|
||||||
|
|
||||||
|
await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, {
|
||||||
|
encryptedRootKey,
|
||||||
|
encryptionStrategy: RootKeyEncryptionStrategy.Basic
|
||||||
|
});
|
||||||
|
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||||
|
};
|
||||||
|
|
||||||
// by keeping the decrypted data key in inner scope
|
// by keeping the decrypted data key in inner scope
|
||||||
// none of the entities outside can interact directly or expose the data key
|
// none of the entities outside can interact directly or expose the data key
|
||||||
// NOTICE: If changing here update migrations/utils/kms
|
// NOTICE: If changing here update migrations/utils/kms
|
||||||
@@ -853,9 +876,6 @@ export const kmsServiceFactory = ({
|
|||||||
|
|
||||||
// akhilmhdh: a copy of this is made in migrations/utils/kms
|
// akhilmhdh: a copy of this is made in migrations/utils/kms
|
||||||
const startService = async () => {
|
const startService = async () => {
|
||||||
const appCfg = getConfig();
|
|
||||||
const hsmEnabled = hsmService.isActive();
|
|
||||||
|
|
||||||
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
|
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
|
||||||
if (!lock) {
|
if (!lock) {
|
||||||
await keyStore.waitTillReady({
|
await keyStore.waitTillReady({
|
||||||
@@ -875,34 +895,11 @@ export const kmsServiceFactory = ({
|
|||||||
|
|
||||||
const decryptedRootKey = await $decryptRootKey(kmsRootConfig).catch((err) => {
|
const decryptedRootKey = await $decryptRootKey(kmsRootConfig).catch((err) => {
|
||||||
logger.error(err, `KMS: Failed to decrypt ROOT Key [strategy=${kmsRootConfig.encryptionStrategy}]`);
|
logger.error(err, `KMS: Failed to decrypt ROOT Key [strategy=${kmsRootConfig.encryptionStrategy}]`);
|
||||||
throw err;
|
// We do not want to throw on startup. If the HSM has issues, this will throw an error, causing the entire API to shut down.
|
||||||
|
// If the API shuts down, the user will have no way to do recovery by importing their backup decryption key and rolling back to basic encryption.
|
||||||
|
return Buffer.alloc(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedEncryptionStrategy = appCfg.ROOT_KEY_ENCRYPTION_STRATEGY;
|
|
||||||
|
|
||||||
// case: the users selected encryption key strategy does not match the one in the DB
|
|
||||||
// in this case we need to re-encrypt the key with the selected strategy, and update the strategy and key in the DB
|
|
||||||
if (selectedEncryptionStrategy !== kmsRootConfig.encryptionStrategy) {
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
newStrategy: selectedEncryptionStrategy,
|
|
||||||
configuredStrategy: kmsRootConfig.encryptionStrategy
|
|
||||||
},
|
|
||||||
"KMS: Change in root encryption key strategy detected. Re-encrypting ROOT Key with selected strategy"
|
|
||||||
);
|
|
||||||
const encryptedRootKey = await $encryptRootKey(decryptedRootKey, selectedEncryptionStrategy);
|
|
||||||
|
|
||||||
if (!encryptedRootKey) {
|
|
||||||
logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy");
|
|
||||||
throw new Error("Failed to re-encrypt ROOT Key with selected strategy");
|
|
||||||
}
|
|
||||||
|
|
||||||
await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, {
|
|
||||||
encryptedRootKey,
|
|
||||||
encryptionStrategy: selectedEncryptionStrategy
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the flag so that other instance nodes can start
|
// set the flag so that other instance nodes can start
|
||||||
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
||||||
logger.info("KMS: Loading ROOT Key into Memory.");
|
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||||
@@ -910,12 +907,11 @@ export const kmsServiceFactory = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("KMS: Generating ROOT Key");
|
// case 2: no config is found, so we create a new root key with basic encryption
|
||||||
const selectedEncryptionStrategy = appCfg.ROOT_KEY_ENCRYPTION_STRATEGY;
|
logger.info("KMS: Generating new ROOT Key");
|
||||||
|
|
||||||
const newRootKey = randomSecureBytes(32);
|
const newRootKey = randomSecureBytes(32);
|
||||||
const encryptedRootKey = await $encryptRootKey(newRootKey, selectedEncryptionStrategy).catch((err) => {
|
const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Basic).catch((err) => {
|
||||||
logger.error({ hsmEnabled }, "KMS: Failed to encrypt ROOT Key");
|
logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key");
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -923,7 +919,7 @@ export const kmsServiceFactory = ({
|
|||||||
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
|
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
|
||||||
id: KMS_ROOT_CONFIG_UUID,
|
id: KMS_ROOT_CONFIG_UUID,
|
||||||
encryptedRootKey,
|
encryptedRootKey,
|
||||||
encryptionStrategy: selectedEncryptionStrategy
|
encryptionStrategy: RootKeyEncryptionStrategy.Basic
|
||||||
});
|
});
|
||||||
|
|
||||||
// set the flag so that other instance nodes can start
|
// set the flag so that other instance nodes can start
|
||||||
@@ -933,6 +929,32 @@ export const kmsServiceFactory = ({
|
|||||||
ROOT_ENCRYPTION_KEY = newRootKey;
|
ROOT_ENCRYPTION_KEY = newRootKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => {
|
||||||
|
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
|
||||||
|
if (!kmsRootConfig) {
|
||||||
|
throw new NotFoundError({ message: "KMS root config not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kmsRootConfig.encryptionStrategy === strategy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
|
||||||
|
const encryptedRootKey = await $encryptRootKey(decryptedRootKey, strategy);
|
||||||
|
|
||||||
|
if (!encryptedRootKey) {
|
||||||
|
logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy");
|
||||||
|
throw new Error("Failed to re-encrypt ROOT Key with selected strategy");
|
||||||
|
}
|
||||||
|
|
||||||
|
await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, {
|
||||||
|
encryptedRootKey,
|
||||||
|
encryptionStrategy: strategy
|
||||||
|
});
|
||||||
|
|
||||||
|
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startService,
|
startService,
|
||||||
generateKmsKey,
|
generateKmsKey,
|
||||||
@@ -944,11 +966,14 @@ export const kmsServiceFactory = ({
|
|||||||
encryptWithRootKey,
|
encryptWithRootKey,
|
||||||
decryptWithRootKey,
|
decryptWithRootKey,
|
||||||
getOrgKmsKeyId,
|
getOrgKmsKeyId,
|
||||||
|
updateEncryptionStrategy,
|
||||||
getProjectSecretManagerKmsKeyId,
|
getProjectSecretManagerKmsKeyId,
|
||||||
updateProjectSecretManagerKmsKey,
|
updateProjectSecretManagerKmsKey,
|
||||||
getProjectKeyBackup,
|
getProjectKeyBackup,
|
||||||
loadProjectKeyBackup,
|
loadProjectKeyBackup,
|
||||||
getKmsById,
|
getKmsById,
|
||||||
createCipherPairWithDataKey
|
createCipherPairWithDataKey,
|
||||||
|
exportRootEncryptionKeyParts,
|
||||||
|
importRootEncryptionKey
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
|
|
||||||
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||||
import { AuthMethod } from "../auth/auth-type";
|
import { AuthMethod } from "../auth/auth-type";
|
||||||
|
import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns";
|
||||||
|
import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { RootKeyEncryptionStrategy } from "../kms/kms-types";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
||||||
@@ -20,7 +23,15 @@ type TSuperAdminServiceFactoryDep = {
|
|||||||
serverCfgDAL: TSuperAdminDALFactory;
|
serverCfgDAL: TSuperAdminDALFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey">;
|
kmsService: Pick<
|
||||||
|
TKmsServiceFactory,
|
||||||
|
| "encryptWithRootKey"
|
||||||
|
| "decryptWithRootKey"
|
||||||
|
| "exportRootEncryptionKeyParts"
|
||||||
|
| "importRootEncryptionKey"
|
||||||
|
| "updateEncryptionStrategy"
|
||||||
|
>;
|
||||||
|
kmsRootConfigDAL: TKmsRootConfigDALFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||||
@@ -47,6 +58,7 @@ export const superAdminServiceFactory = ({
|
|||||||
authService,
|
authService,
|
||||||
orgService,
|
orgService,
|
||||||
keyStore,
|
keyStore,
|
||||||
|
kmsRootConfigDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
licenseService
|
licenseService
|
||||||
}: TSuperAdminServiceFactoryDep) => {
|
}: TSuperAdminServiceFactoryDep) => {
|
||||||
@@ -150,6 +162,35 @@ export const superAdminServiceFactory = ({
|
|||||||
return updatedServerCfg;
|
return updatedServerCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportPlainKmsKey = async () => {
|
||||||
|
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
|
||||||
|
|
||||||
|
if (!kmsRootConfig) {
|
||||||
|
throw new NotFoundError({ name: "KmsRootConfig", message: "KMS root configuration not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kmsRootConfig.exported) {
|
||||||
|
throw new BadRequestError({ name: "KmsRootConfig", message: "KMS root configuration already exported" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { exported: true });
|
||||||
|
return kmsService.exportRootEncryptionKeyParts();
|
||||||
|
};
|
||||||
|
|
||||||
|
const importPlainKmsKey = async (secretParts: string[]) => {
|
||||||
|
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
|
||||||
|
|
||||||
|
if (!kmsRootConfig) {
|
||||||
|
throw new NotFoundError({ name: "KmsRootConfig", message: "KMS root configuration not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kmsRootConfig.exported) {
|
||||||
|
throw new BadRequestError({ name: "KmsRootConfig", message: "KMS root configuration was never exported" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await kmsService.importRootEncryptionKey(secretParts);
|
||||||
|
};
|
||||||
|
|
||||||
const adminSignUp = async ({
|
const adminSignUp = async ({
|
||||||
lastName,
|
lastName,
|
||||||
firstName,
|
firstName,
|
||||||
@@ -288,12 +329,69 @@ export const superAdminServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getConfiguredEncryptionStrategies = async () => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const kmsRootCfg = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
|
||||||
|
|
||||||
|
if (!kmsRootCfg) {
|
||||||
|
throw new NotFoundError({ name: "KmsRootConfig", message: "KMS root configuration not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedStrategy = kmsRootCfg.encryptionStrategy;
|
||||||
|
const enabledStrategies: { enabled: boolean; strategy: RootKeyEncryptionStrategy; name: string }[] = [];
|
||||||
|
|
||||||
|
if (appCfg.ROOT_ENCRYPTION_KEY || appCfg.ENCRYPTION_KEY) {
|
||||||
|
const basicStrategy = RootKeyEncryptionStrategy.Basic;
|
||||||
|
|
||||||
|
enabledStrategies.push({
|
||||||
|
name: "Regular Encryption",
|
||||||
|
enabled: selectedStrategy === basicStrategy,
|
||||||
|
strategy: basicStrategy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (appCfg.isHsmConfigured) {
|
||||||
|
const hsmStrategy = RootKeyEncryptionStrategy.Hsm;
|
||||||
|
|
||||||
|
enabledStrategies.push({
|
||||||
|
name: "Hardware Security Module (HSM)",
|
||||||
|
enabled: selectedStrategy === hsmStrategy,
|
||||||
|
strategy: hsmStrategy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
strategies: enabledStrategies,
|
||||||
|
keyExported: kmsRootCfg.exported
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRootEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => {
|
||||||
|
const configuredStrategies = await getConfiguredEncryptionStrategies();
|
||||||
|
|
||||||
|
const foundStrategy = configuredStrategies.strategies.find((s) => s.strategy === strategy);
|
||||||
|
|
||||||
|
if (!foundStrategy) {
|
||||||
|
throw new BadRequestError({ message: "Invalid encryption strategy" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundStrategy.enabled) {
|
||||||
|
throw new BadRequestError({ message: "The selected encryption strategy is already enabled" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await kmsService.updateEncryptionStrategy(strategy);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initServerCfg,
|
initServerCfg,
|
||||||
updateServerCfg,
|
updateServerCfg,
|
||||||
adminSignUp,
|
adminSignUp,
|
||||||
getUsers,
|
getUsers,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
getAdminSlackConfig
|
getAdminSlackConfig,
|
||||||
|
updateRootEncryptionStrategy,
|
||||||
|
getConfiguredEncryptionStrategies,
|
||||||
|
exportPlainKmsKey,
|
||||||
|
importPlainKmsKey
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -525,3 +525,40 @@ func CallUpdateRawSecretsV3(httpClient *resty.Client, request UpdateRawSecretByN
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CallExportKmsRootEncryptionKey(httpClient *resty.Client) (ExportKmsRootKeyResponse, error) {
|
||||||
|
var exportKmsKeyResponse ExportKmsRootKeyResponse
|
||||||
|
response, err := httpClient.
|
||||||
|
R().
|
||||||
|
SetResult(&exportKmsKeyResponse).
|
||||||
|
SetHeader("User-Agent", USER_AGENT).
|
||||||
|
Post(fmt.Sprintf("%v/v1/admin/kms-export", config.INFISICAL_URL))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ExportKmsRootKeyResponse{}, fmt.Errorf("CallSuperAdminExportKmsKey: Unable to complete api request [err=%w]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IsError() {
|
||||||
|
return ExportKmsRootKeyResponse{}, fmt.Errorf("CallSuperAdminExportKmsKey: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return exportKmsKeyResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallImportKmsRootEncryptionKey(httpClient *resty.Client, request ImportKmsRootKeyRequest) error {
|
||||||
|
response, err := httpClient.
|
||||||
|
R().
|
||||||
|
SetHeader("User-Agent", USER_AGENT).
|
||||||
|
SetBody(request).
|
||||||
|
Post(fmt.Sprintf("%v/v1/admin/kms-import", config.INFISICAL_URL))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CallSuperAdminImportKmsKey: Unable to complete api request [err=%w]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IsError() {
|
||||||
|
return fmt.Errorf("CallSuperAdminImportKmsKey: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ type GetOrganizationsResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SelectOrganizationResponse struct {
|
type SelectOrganizationResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
MfaEnabled bool `json:"isMfaEnabled"`
|
MfaEnabled bool `json:"isMfaEnabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectOrganizationRequest struct {
|
type SelectOrganizationRequest struct {
|
||||||
@@ -617,3 +617,11 @@ type GetRawSecretV3ByNameResponse struct {
|
|||||||
} `json:"secret"`
|
} `json:"secret"`
|
||||||
ETag string
|
ETag string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExportKmsRootKeyResponse struct {
|
||||||
|
SecretParts []string `json:"secretParts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImportKmsRootKeyRequest struct {
|
||||||
|
SecretParts []string `json:"secretParts"`
|
||||||
|
}
|
||||||
|
|||||||
128
cli/packages/cmd/kms.go
Normal file
128
cli/packages/cmd/kms.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2023 Infisical Inc.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Infisical/infisical-merge/packages/api"
|
||||||
|
"github.com/Infisical/infisical-merge/packages/util"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var kmsCmd = &cobra.Command{
|
||||||
|
Use: "kms",
|
||||||
|
Short: "Manage your Infisical KMS encryption keys",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Example: "infisical kms",
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
util.RequireLogin()
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportCmd represents the export command
|
||||||
|
var exportKeyCmd = &cobra.Command{
|
||||||
|
Use: "export",
|
||||||
|
Short: "Used to export your Infisical root encryption key parts, to be used for recovery (infisical import-key [...parts])",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Example: "infisical kms export",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
loggedInDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !loggedInDetails.IsUserLoggedIn || loggedInDetails.LoginExpired {
|
||||||
|
util.HandleError(fmt.Errorf("You must be logged in to run this command"))
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := resty.New()
|
||||||
|
httpClient.SetAuthToken(loggedInDetails.UserCredentials.JTWToken).
|
||||||
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
|
res, err := api.CallExportKmsRootEncryptionKey(httpClient)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
if strings.Contains(err.Error(), "configuration already exported") {
|
||||||
|
util.HandleError(fmt.Errorf("This KMS encryption key has already been exported. You can only export the decryption key once."))
|
||||||
|
} else {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boldGreen := color.New(color.FgGreen).Add(color.Bold)
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
boldGreen.Printf(">>>> Successfully exported KMS encryption key\n\n")
|
||||||
|
|
||||||
|
plainBold := color.New(color.Bold)
|
||||||
|
|
||||||
|
for i, part := range res.SecretParts {
|
||||||
|
plainBold.Printf("Part %d: %v\n", i+1, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
boldYellow := color.New(color.FgYellow).Add(color.Bold)
|
||||||
|
boldYellow.Printf("\nPlease store these parts in a secure location. You will need them to recover your KMS encryption key.\nYou will not be able to export these credentials again in the future.\n\n")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var importKeyCmd = &cobra.Command{
|
||||||
|
Use: "import",
|
||||||
|
Short: "Used to import your Infisical root encryption key parts, to be used for recovery (infisical import-key [...parts])",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Example: "infisical kms import",
|
||||||
|
Args: cobra.MinimumNArgs(6),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
loggedInDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !loggedInDetails.IsUserLoggedIn || loggedInDetails.LoginExpired {
|
||||||
|
util.HandleError(fmt.Errorf("You must be logged in to run this command"))
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := resty.New()
|
||||||
|
httpClient.SetAuthToken(loggedInDetails.UserCredentials.JTWToken).
|
||||||
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
|
err = api.CallImportKmsRootEncryptionKey(httpClient, api.ImportKmsRootKeyRequest{
|
||||||
|
SecretParts: args,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "configuration was never exported") {
|
||||||
|
util.HandleError(fmt.Errorf("This KMS encryption key has not been exported yet. You must export the key first before you can import it."))
|
||||||
|
} else {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boldGreen := color.New(color.FgGreen).Add(color.Bold)
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
boldGreen.Printf(">>>> Successfully imported KMS encryption key\n\n")
|
||||||
|
|
||||||
|
boldYellow := color.New(color.FgYellow).Add(color.Bold)
|
||||||
|
boldYellow.Printf("Important: Make sure to set the `ROOT_KEY_ENCRYPTION_STRATEGY` environment variable to `BASIC` on your Infisical instance.\nNot doing this will likely result in having to re-import the key on the next instance restart.\n\n")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
kmsCmd.AddCommand(exportKeyCmd)
|
||||||
|
kmsCmd.AddCommand(importKeyCmd)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(kmsCmd)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
export {
|
export {
|
||||||
useAdminDeleteUser,
|
useAdminDeleteUser,
|
||||||
useCreateAdminUser,
|
useCreateAdminUser,
|
||||||
|
useExportServerDecryptionKey,
|
||||||
|
useImportServerDecryptionKey,
|
||||||
useUpdateAdminSlackConfig,
|
useUpdateAdminSlackConfig,
|
||||||
useUpdateServerConfig
|
useUpdateServerConfig,
|
||||||
|
useUpdateServerEncryptionStrategy
|
||||||
} from "./mutation";
|
} from "./mutation";
|
||||||
export { useAdminGetUsers, useGetAdminSlackConfig, useGetServerConfig } from "./queries";
|
export {
|
||||||
|
useAdminGetUsers,
|
||||||
|
useGetAdminSlackConfig,
|
||||||
|
useGetServerConfig,
|
||||||
|
useGetServerRootKmsEncryptionDetails
|
||||||
|
} from "./queries";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { User } from "../users/types";
|
|||||||
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
AdminSlackConfig,
|
AdminSlackConfig,
|
||||||
|
RootKeyEncryptionStrategy,
|
||||||
TCreateAdminUserDTO,
|
TCreateAdminUserDTO,
|
||||||
TServerConfig,
|
TServerConfig,
|
||||||
TUpdateAdminSlackConfigDTO
|
TUpdateAdminSlackConfigDTO
|
||||||
@@ -85,3 +86,36 @@ export const useUpdateAdminSlackConfig = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdateServerEncryptionStrategy = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (strategy: RootKeyEncryptionStrategy) => {
|
||||||
|
await apiRequest.post("/api/v1/admin/encryption-strategies", { strategy });
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(adminQueryKeys.getServerEncryptionStrategies());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExportServerDecryptionKey = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const { data } = await apiRequest.post<{ secretParts: string[] }>("/api/v1/admin/kms-export");
|
||||||
|
return data.secretParts;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useImportServerDecryptionKey = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (secretParts: string[]) => {
|
||||||
|
await apiRequest.post("/api/v1/admin/kms-import", { secretParts });
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(adminQueryKeys.serverConfig());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-que
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { User } from "../types";
|
import { User } from "../types";
|
||||||
import { AdminGetUsersFilters, AdminSlackConfig, TServerConfig } from "./types";
|
import {
|
||||||
|
AdminGetUsersFilters,
|
||||||
|
AdminSlackConfig,
|
||||||
|
TGetServerRootKmsEncryptionDetails,
|
||||||
|
TServerConfig
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const adminStandaloneKeys = {
|
export const adminStandaloneKeys = {
|
||||||
getUsers: "get-users"
|
getUsers: "get-users"
|
||||||
@@ -12,7 +17,8 @@ export const adminStandaloneKeys = {
|
|||||||
export const adminQueryKeys = {
|
export const adminQueryKeys = {
|
||||||
serverConfig: () => ["server-config"] as const,
|
serverConfig: () => ["server-config"] as const,
|
||||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
||||||
getAdminSlackConfig: () => ["admin-slack-config"] as const
|
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
||||||
|
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchServerConfig = async () => {
|
const fetchServerConfig = async () => {
|
||||||
@@ -61,8 +67,8 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetAdminSlackConfig = () =>
|
export const useGetAdminSlackConfig = () => {
|
||||||
useQuery({
|
return useQuery({
|
||||||
queryKey: adminQueryKeys.getAdminSlackConfig(),
|
queryKey: adminQueryKeys.getAdminSlackConfig(),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<AdminSlackConfig>(
|
const { data } = await apiRequest.get<AdminSlackConfig>(
|
||||||
@@ -72,3 +78,17 @@ export const useGetAdminSlackConfig = () =>
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetServerRootKmsEncryptionDetails = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: adminQueryKeys.getServerEncryptionStrategies(),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TGetServerRootKmsEncryptionDetails>(
|
||||||
|
"/api/v1/admin/root-kms-config"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -54,3 +54,17 @@ export type AdminSlackConfig = {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetServerRootKmsEncryptionDetails = {
|
||||||
|
strategies: {
|
||||||
|
strategy: RootKeyEncryptionStrategy;
|
||||||
|
enabled: boolean;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
keyExported: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum RootKeyEncryptionStrategy {
|
||||||
|
Basic = "BASIC",
|
||||||
|
Hsm = "HSM"
|
||||||
|
}
|
||||||
|
|||||||
13
frontend/src/hooks/useFileDownload.tsx
Normal file
13
frontend/src/hooks/useFileDownload.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
export const useFileDownload = () => {
|
||||||
|
return useCallback((content: string, filename: string) => {
|
||||||
|
const downloadUrl = `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`;
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = downloadUrl;
|
||||||
|
link.setAttribute("download", filename);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
@@ -22,15 +22,21 @@ import {
|
|||||||
Tabs
|
Tabs
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useServerConfig, useUser } from "@app/context";
|
import { useOrganization, useServerConfig, useUser } from "@app/context";
|
||||||
import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api";
|
import {
|
||||||
|
useGetOrganizations,
|
||||||
|
useGetServerRootKmsEncryptionDetails,
|
||||||
|
useUpdateServerConfig
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
import { AuthPanel } from "./AuthPanel";
|
import { AuthPanel } from "./AuthPanel";
|
||||||
|
import { EncryptionPanel } from "./EncryptionPanel";
|
||||||
import { IntegrationPanel } from "./IntegrationPanel";
|
import { IntegrationPanel } from "./IntegrationPanel";
|
||||||
import { RateLimitPanel } from "./RateLimitPanel";
|
import { RateLimitPanel } from "./RateLimitPanel";
|
||||||
import { UserPanel } from "./UserPanel";
|
import { UserPanel } from "./UserPanel";
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
Settings = "settings",
|
Settings = "settings",
|
||||||
|
Encryption = "encryption",
|
||||||
Auth = "auth",
|
Auth = "auth",
|
||||||
RateLimit = "rate-limit",
|
RateLimit = "rate-limit",
|
||||||
Integrations = "integrations",
|
Integrations = "integrations",
|
||||||
@@ -55,6 +61,7 @@ type TDashboardForm = z.infer<typeof formSchema>;
|
|||||||
export const AdminDashboardPage = () => {
|
export const AdminDashboardPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const data = useServerConfig();
|
const data = useServerConfig();
|
||||||
|
const { data: serverRootKmsDetails } = useGetServerRootKmsEncryptionDetails();
|
||||||
const { config } = data;
|
const { config } = data;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -137,6 +144,7 @@ export const AdminDashboardPage = () => {
|
|||||||
<TabList>
|
<TabList>
|
||||||
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||||
<Tab value={TabSections.Settings}>General</Tab>
|
<Tab value={TabSections.Settings}>General</Tab>
|
||||||
|
{!!serverRootKmsDetails && <Tab value={TabSections.Encryption}>Encryption</Tab>}
|
||||||
<Tab value={TabSections.Auth}>Authentication</Tab>
|
<Tab value={TabSections.Auth}>Authentication</Tab>
|
||||||
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
|
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
|
||||||
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
||||||
@@ -321,6 +329,11 @@ export const AdminDashboardPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
{!!serverRootKmsDetails && (
|
||||||
|
<TabPanel value={TabSections.Encryption}>
|
||||||
|
<EncryptionPanel rootKmsDetails={serverRootKmsDetails} />
|
||||||
|
</TabPanel>
|
||||||
|
)}
|
||||||
<TabPanel value={TabSections.Auth}>
|
<TabPanel value={TabSections.Auth}>
|
||||||
<AuthPanel />
|
<AuthPanel />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|||||||
182
frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx
Normal file
182
frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, FormControl, Modal, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useUpdateServerEncryptionStrategy } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
RootKeyEncryptionStrategy,
|
||||||
|
TGetServerRootKmsEncryptionDetails
|
||||||
|
} from "@app/hooks/api/admin/types";
|
||||||
|
|
||||||
|
import { ExportRootKmsKeyModalContent } from "./components/ExportRootKmsKeyModalContent";
|
||||||
|
import { RestoreRootKmsKeyModalContent } from "./components/RestoreRootKmsKeyModalContent";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
encryptionStrategy: z.nativeEnum(RootKeyEncryptionStrategy)
|
||||||
|
});
|
||||||
|
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
rootKmsDetails: TGetServerRootKmsEncryptionDetails;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EncryptionPanel = ({ rootKmsDetails }: Props) => {
|
||||||
|
const { mutateAsync: updateEncryptionStrategy } = useUpdateServerEncryptionStrategy();
|
||||||
|
const { handlePopUpToggle, handlePopUpOpen, popUp } = usePopUp([
|
||||||
|
"exportKey",
|
||||||
|
"restoreKey"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
values: {
|
||||||
|
encryptionStrategy:
|
||||||
|
rootKmsDetails?.strategies?.find((s) => s.enabled)?.strategy ??
|
||||||
|
RootKeyEncryptionStrategy.Basic
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = useCallback(async (formData: TForm) => {
|
||||||
|
try {
|
||||||
|
await updateEncryptionStrategy(formData.encryptionStrategy);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!rootKmsDetails.keyExported &&
|
||||||
|
formData.encryptionStrategy !== RootKeyEncryptionStrategy.Basic
|
||||||
|
) {
|
||||||
|
handlePopUpOpen("exportKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Encryption strategy updated successfully"
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update encryption strategy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col justify-start">
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
<div className="mb-2 text-xl font-semibold text-mineshaft-100">
|
||||||
|
KMS Encryption Strategy
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
{!rootKmsDetails.keyExported && (
|
||||||
|
<div className="mb-2 text-sm">
|
||||||
|
<FontAwesomeIcon icon={faExclamationCircle} className="mr-1 text-red-500" />
|
||||||
|
You have not exported the KMS root encryption key. Switch to HSM encryption or
|
||||||
|
run the{" "}
|
||||||
|
<code>
|
||||||
|
<span className="mt-2 rounded-md bg-mineshaft-600 p-1 text-xs text-primary-500">
|
||||||
|
infisical kms export
|
||||||
|
</span>
|
||||||
|
</code>{" "}
|
||||||
|
CLI command to export the key parts.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<br />
|
||||||
|
If you experience issues with accessing projects while not using Regular
|
||||||
|
Encryption (default), you can restore the KMS root encryption key by using your
|
||||||
|
exported key parts.
|
||||||
|
<br /> <br />
|
||||||
|
If you do not have the exported key parts, you can export them by using the CLI
|
||||||
|
command
|
||||||
|
<br />
|
||||||
|
<code>
|
||||||
|
<span className="mt-2 rounded-md bg-mineshaft-600 p-1 text-xs text-primary-500">
|
||||||
|
infisical kms export
|
||||||
|
</span>
|
||||||
|
</code>
|
||||||
|
. <br />
|
||||||
|
<br />
|
||||||
|
<span className="font-bold">
|
||||||
|
Please keep in mind that you can only export the key parts once.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
isDisabled={!rootKmsDetails.keyExported}
|
||||||
|
onClick={() => handlePopUpToggle("restoreKey", true)}
|
||||||
|
>
|
||||||
|
Restore Root KMS Encryption Key
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
|
||||||
|
Select which type of encryption strategy you want to use for your KMS root key. HSM is
|
||||||
|
supported on Enterprise plans.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="encryptionStrategy"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl className="max-w-sm" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
className="w-full bg-mineshaft-700"
|
||||||
|
dropdownContainerClassName="bg-mineshaft-800"
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
{...field}
|
||||||
|
>
|
||||||
|
{rootKmsDetails.strategies?.map((strategy) => (
|
||||||
|
<SelectItem key={strategy.strategy} value={strategy.strategy}>
|
||||||
|
{strategy.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mt-2"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.exportKey.isOpen}
|
||||||
|
onOpenChange={(state) => handlePopUpToggle("exportKey", state)}
|
||||||
|
>
|
||||||
|
<ExportRootKmsKeyModalContent handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.restoreKey.isOpen}
|
||||||
|
onOpenChange={(state) => handlePopUpToggle("restoreKey", state)}
|
||||||
|
>
|
||||||
|
<RestoreRootKmsKeyModalContent handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { Button, ModalContent } from "@app/components/v2";
|
||||||
|
import { useExportServerDecryptionKey } from "@app/hooks/api";
|
||||||
|
import { useFileDownload } from "@app/hooks/useFileDownload";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["exportKey"]>, state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExportRootKmsKeyModalContent = ({ handlePopUpToggle }: Props) => {
|
||||||
|
const { mutateAsync: exportKey, isLoading } = useExportServerDecryptionKey();
|
||||||
|
const downloadFile = useFileDownload();
|
||||||
|
const [downloaded, setDownloaded] = useState(false);
|
||||||
|
|
||||||
|
const onExport = useCallback(async () => {
|
||||||
|
const keyParts = await exportKey();
|
||||||
|
downloadFile(keyParts.join("\n\n"), "infisical-encryption-key-parts.txt");
|
||||||
|
setDownloaded(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent
|
||||||
|
title="Export Root KMS Encryption Key"
|
||||||
|
subTitle="We highly recommend exporting the KMS root encryption key and storing it in a secure location. Incase of a disaster, you can use our CLI to recover your projects with zero loss."
|
||||||
|
>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
{!downloaded ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => handlePopUpToggle("exportKey", false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button isLoading={isLoading} className="ml-2" onClick={onExport}>
|
||||||
|
Download Key
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex max-w-fit flex-col overflow-clip break-words px-2 text-sm font-normal text-gray-400">
|
||||||
|
The key parts have been downloaded. Please store them in a safe place. You will need
|
||||||
|
these keys incase you need to recovery the KMS root encryption key. Please consult our
|
||||||
|
documentation for further instructions.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { useMemo } 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 { Button, FormControl, Input, ModalContent } from "@app/components/v2";
|
||||||
|
import { useImportServerDecryptionKey } from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["restoreKey"]>, state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
keyParts: z
|
||||||
|
.array(z.string())
|
||||||
|
.refine((data) => data.length === 4 && data.every((part) => part.length > 0), {
|
||||||
|
message: "Enter at least 4 key parts in order to restore the KMS root decryption key."
|
||||||
|
})
|
||||||
|
});
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const RestoreRootKmsKeyModalContent = ({ handlePopUpToggle }: Props) => {
|
||||||
|
const { mutateAsync: importKmsRootKey } = useImportServerDecryptionKey();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting, errors, isLoading, isValid }
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
values: {
|
||||||
|
keyParts: ["", "", "", ""]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyParts = useMemo(() => watch("keyParts"), []);
|
||||||
|
|
||||||
|
console.log("lol", watch("keyParts"));
|
||||||
|
console.log("isVal", isValid);
|
||||||
|
console.log("errors", errors);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent
|
||||||
|
title="Export Root KMS Encryption Key"
|
||||||
|
subTitle="Recover the KMS root encryption key by entering the key parts. You can recover the key if you have 4 out of 8 key parts."
|
||||||
|
footerContent={
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => handlePopUpToggle("restoreKey", false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isDisabled={!!errors.keyParts || !isValid}
|
||||||
|
isLoading={isSubmitting || isLoading}
|
||||||
|
className="ml-2"
|
||||||
|
onClick={handleSubmit(async (data) => {
|
||||||
|
await importKmsRootKey(data.keyParts);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
title: "Successfully restored KMS root key",
|
||||||
|
text: "The KMS root key has been successfully restored."
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpToggle("restoreKey", false);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Restore Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form>
|
||||||
|
<div className="flex w-full flex-col justify-end">
|
||||||
|
{keyParts.map((_, index) => (
|
||||||
|
<Controller
|
||||||
|
key={`key-part-${index + 1}`}
|
||||||
|
name={`keyParts.${index}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div>
|
||||||
|
<FormControl label={`Key Part ${index + 1}`}>
|
||||||
|
<Input {...field} placeholder={`Enter key part ${index + 1}`} />
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{errors.keyParts && (
|
||||||
|
<div className="mt-2 text-sm font-normal text-red-500">{errors.keyParts.message}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -7,7 +7,8 @@
|
|||||||
"name": "infisical",
|
"name": "infisical",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-radio-group": "^1.1.3"
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
|
"secrets.js-grempe": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
@@ -1392,6 +1393,12 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/secrets.js-grempe": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/secrets.js-grempe/-/secrets.js-grempe-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-4xkOIaDAg998dTFXZUJTOoVbdLHfB818SMeLJ69ABccgGEKokxsoRFupAFfAImloUSKv4QUGNMgKVbKMf6z0Ug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -2457,6 +2464,11 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"secrets.js-grempe": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/secrets.js-grempe/-/secrets.js-grempe-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-4xkOIaDAg998dTFXZUJTOoVbdLHfB818SMeLJ69ABccgGEKokxsoRFupAFfAImloUSKv4QUGNMgKVbKMf6z0Ug=="
|
||||||
|
},
|
||||||
"shebang-command": {
|
"shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"husky": "^8.0.3"
|
"husky": "^8.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-radio-group": "^1.1.3"
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
|
"secrets.js-grempe": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user