diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts
index c3853d3c2f..8a9350a361 100644
--- a/backend/src/controllers/v3/secretsController.ts
+++ b/backend/src/controllers/v3/secretsController.ts
@@ -2,6 +2,240 @@ import { Request, Response } from "express";
import { Types } from "mongoose";
import { SecretService, EventService } from "../../services";
import { eventPushSecrets } from "../../events";
+import { BotService } from "../../services";
+import { repackageSecretToRaw } from "../../helpers/secrets";
+import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
+
+/**
+ * Return secrets for workspace with id [workspaceId] and environment
+ * [environment] in plaintext
+ * @param req
+ * @param res
+ */
+export const getSecretsRaw = async (req: Request, res: Response) => {
+ const workspaceId = req.query.workspaceId as string;
+ const environment = req.query.environment as string;
+ const secretPath = req.query.secretPath as string;
+
+ const secrets = await SecretService.getSecrets({
+ workspaceId: new Types.ObjectId(workspaceId),
+ environment,
+ secretPath,
+ authData: req.authData,
+ });
+
+ const key = await BotService.getWorkspaceKeyWithBot({
+ workspaceId: new Types.ObjectId(workspaceId)
+ });
+
+ return res.status(200).send({
+ secrets: secrets.map((secret) => {
+ const rep = repackageSecretToRaw({
+ secret,
+ key
+ });
+
+ return rep;
+ })
+ });
+};
+
+/**
+ * Return secret with name [secretName] in plaintext
+ * @param req
+ * @param res
+ */
+export const getSecretByNameRaw = async (req: Request, res: Response) => {
+ const { secretName } = req.params;
+ const workspaceId = req.query.workspaceId as string;
+ const environment = req.query.environment as string;
+ const secretPath = req.query.secretPath as string;
+ const type = req.query.type as "shared" | "personal" | undefined;
+
+ const secret = await SecretService.getSecret({
+ secretName,
+ workspaceId: new Types.ObjectId(workspaceId),
+ environment,
+ type,
+ secretPath,
+ authData: req.authData,
+ });
+
+ const key = await BotService.getWorkspaceKeyWithBot({
+ workspaceId: new Types.ObjectId(workspaceId)
+ });
+
+ return res.status(200).send({
+ secret: repackageSecretToRaw({
+ secret,
+ key
+ })
+ });
+};
+
+/**
+ * Create secret with name [secretName] in plaintext
+ * @param req
+ * @param res
+ */
+export const createSecretRaw = async (req: Request, res: Response) => {
+ const { secretName } = req.params;
+ const {
+ workspaceId,
+ environment,
+ type,
+ secretValue,
+ secretComment,
+ secretPath = "/"
+ } = req.body;
+
+ const key = await BotService.getWorkspaceKeyWithBot({
+ workspaceId: new Types.ObjectId(workspaceId)
+ });
+
+ const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({
+ plaintext: secretName,
+ key
+ });
+
+ const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
+ plaintext: secretValue,
+ key
+ });
+
+ const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
+ plaintext: secretComment,
+ key
+ });
+
+ const secret = await SecretService.createSecret({
+ secretName,
+ workspaceId: new Types.ObjectId(workspaceId),
+ environment,
+ type,
+ authData: req.authData,
+ secretKeyCiphertext: secretKeyEncrypted.ciphertext,
+ secretKeyIV: secretKeyEncrypted.iv,
+ secretKeyTag: secretKeyEncrypted.tag,
+ secretValueCiphertext: secretValueEncrypted.ciphertext,
+ secretValueIV: secretValueEncrypted.iv,
+ secretValueTag: secretValueEncrypted.tag,
+ secretPath,
+ secretCommentCiphertext: secretCommentEncrypted.ciphertext,
+ secretCommentIV: secretCommentEncrypted.iv,
+ secretCommentTag: secretCommentEncrypted.tag
+ });
+
+ await EventService.handleEvent({
+ event: eventPushSecrets({
+ workspaceId: new Types.ObjectId(workspaceId),
+ environment,
+ }),
+ });
+
+ const secretWithoutBlindIndex = secret.toObject();
+ delete secretWithoutBlindIndex.secretBlindIndex;
+
+ return res.status(200).send({
+ secret: repackageSecretToRaw({
+ secret: secretWithoutBlindIndex,
+ key
+ })
+ });
+}
+
+/**
+ * Update secret with name [secretName]
+ * @param req
+ * @param res
+ */
+export const updateSecretByNameRaw = async (req: Request, res: Response) => {
+ const { secretName } = req.params;
+ const {
+ workspaceId,
+ environment,
+ type,
+ secretValue,
+ secretPath = "/",
+ } = req.body;
+
+ const key = await BotService.getWorkspaceKeyWithBot({
+ workspaceId: new Types.ObjectId(workspaceId)
+ });
+
+ const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
+ plaintext: secretValue,
+ key
+ });
+
+ const secret = await SecretService.updateSecret({
+ secretName,
+ workspaceId,
+ environment,
+ type,
+ authData: req.authData,
+ secretValueCiphertext: secretValueEncrypted.ciphertext,
+ secretValueIV: secretValueEncrypted.iv,
+ secretValueTag: secretValueEncrypted.tag,
+ secretPath,
+ });
+
+ await EventService.handleEvent({
+ event: eventPushSecrets({
+ workspaceId: new Types.ObjectId(workspaceId),
+ environment,
+ }),
+ });
+
+ return res.status(200).send({
+ secret: repackageSecretToRaw({
+ secret,
+ key
+ })
+ });
+};
+
+/**
+ * Delete secret with name [secretName]
+ * @param req
+ * @param res
+ */
+export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
+ const { secretName } = req.params;
+ const {
+ workspaceId,
+ environment,
+ type,
+ secretPath = "/"
+ } = req.body;
+
+ const { secret } = await SecretService.deleteSecret({
+ secretName,
+ workspaceId,
+ environment,
+ type,
+ authData: req.authData,
+ secretPath,
+ });
+
+ await EventService.handleEvent({
+ event: eventPushSecrets({
+ workspaceId: new Types.ObjectId(workspaceId),
+ environment,
+ }),
+ });
+
+ const key = await BotService.getWorkspaceKeyWithBot({
+ workspaceId: new Types.ObjectId(workspaceId)
+ });
+
+ return res.status(200).send({
+ secret: repackageSecretToRaw({
+ secret,
+ key
+ })
+ });
+};
/**
* Get secrets for workspace with id [workspaceId] and environment
@@ -27,7 +261,7 @@ export const getSecrets = async (req: Request, res: Response) => {
};
/**
- * Get secret with name [secretName]
+ * Return secret with name [secretName]
* @param req
* @param res
*/
@@ -88,13 +322,9 @@ export const createSecret = async (req: Request, res: Response) => {
secretValueIV,
secretValueTag,
secretPath,
- ...(secretCommentCiphertext && secretCommentIV && secretCommentTag
- ? {
- secretCommentCiphertext,
- secretCommentIV,
- secretCommentTag,
- }
- : {}),
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag
});
await EventService.handleEvent({
@@ -112,6 +342,7 @@ export const createSecret = async (req: Request, res: Response) => {
});
};
+
/**
* Update secret with name [secretName]
* @param req
@@ -160,7 +391,12 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
- const { workspaceId, environment, type, secretPath = "/" } = req.body;
+ const {
+ workspaceId,
+ environment,
+ type,
+ secretPath = "/"
+ } = req.body;
const { secret } = await SecretService.deleteSecret({
secretName,
diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts
index e93ab6a8bc..439f7c531d 100644
--- a/backend/src/helpers/bot.ts
+++ b/backend/src/helpers/bot.ts
@@ -86,6 +86,18 @@ export const createBot = async ({
});
};
+/**
+ * Return whether or not workspace with id [workspaceId] is end-to-end encrypted
+ * @param {Types.ObjectId} workspaceId - id of workspace to check
+ */
+export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
+ const botKey = await BotKey.exists({
+ workspace: workspaceId
+ });
+
+ return botKey ? false : true;
+}
+
/**
* Return decrypted secrets for workspace with id [workspaceId]
* and [environment] using bot
@@ -101,7 +113,7 @@ export const getSecretsBotHelper = async ({
environment: string;
}) => {
const content = {} as any;
- const key = await getKey({ workspaceId: workspaceId.toString() });
+ const key = await getKey({ workspaceId: workspaceId });
const secrets = await Secret.find({
workspace: workspaceId,
environment,
@@ -136,7 +148,7 @@ export const getSecretsBotHelper = async ({
* @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key
*/
-export const getKey = async ({ workspaceId }: { workspaceId: string }) => {
+export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
@@ -201,7 +213,7 @@ export const encryptSymmetricHelper = async ({
workspaceId: Types.ObjectId;
plaintext: string;
}) => {
- const key = await getKey({ workspaceId: workspaceId.toString() });
+ const key = await getKey({ workspaceId: workspaceId });
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext,
key,
@@ -233,7 +245,7 @@ export const decryptSymmetricHelper = async ({
iv: string;
tag: string;
}) => {
- const key = await getKey({ workspaceId: workspaceId.toString() });
+ const key = await getKey({ workspaceId: workspaceId });
const plaintext = decryptSymmetric128BitHexKeyUTF8({
ciphertext,
iv,
diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts
index 7b866449e5..d7cc31cdae 100644
--- a/backend/src/helpers/secrets.ts
+++ b/backend/src/helpers/secrets.ts
@@ -5,7 +5,7 @@ import {
GetSecretParams,
UpdateSecretParams,
DeleteSecretParams,
-} from "../interfaces/services/SecretService";
+} from '../interfaces/services/SecretService';
import {
Secret,
ISecret,
@@ -36,9 +36,9 @@ import * as argon2 from "argon2";
import {
encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8,
-} from "../utils/crypto";
+} from '../utils/crypto';
+import { TelemetryService } from '../services';
import { getEncryptionKey, client, getRootEncryptionKey } from "../config";
-import { TelemetryService } from "../services";
import { EESecretService, EELogService } from "../ee/services";
import {
getAuthDataPayloadIdObj,
@@ -46,6 +46,60 @@ import {
} from "../utils/auth";
import { getFolderIdFromServiceToken } from "../services/FolderService";
+/**
+ * Returns an object containing secret [secret] but with its value, key, comment decrypted.
+ *
+ * Precondition: the workspace for secret [secret] must have E2EE disabled
+ * @param {ISecret} secret - secret to repackage to raw
+ * @param {String} key - symmetric key to use to decrypt secret
+ * @returns
+ */
+export const repackageSecretToRaw = ({
+ secret,
+ key
+}:{
+ secret: ISecret;
+ key: string;
+}) => {
+
+ const secretKey = decryptSymmetric128BitHexKeyUTF8({
+ ciphertext: secret.secretKeyCiphertext,
+ iv: secret.secretKeyIV,
+ tag: secret.secretKeyTag,
+ key
+ });
+
+ const secretValue = decryptSymmetric128BitHexKeyUTF8({
+ ciphertext: secret.secretValueCiphertext,
+ iv: secret.secretValueIV,
+ tag: secret.secretValueTag,
+ key
+ });
+
+ let secretComment: string = '';
+
+ if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
+ secretComment = decryptSymmetric128BitHexKeyUTF8({
+ ciphertext: secret.secretCommentCiphertext,
+ iv: secret.secretCommentIV,
+ tag: secret.secretCommentTag,
+ key
+ });
+ }
+
+ return ({
+ _id: secret._id,
+ version: secret.version,
+ workspace: secret.workspace,
+ type: secret.type,
+ environment: secret.environment,
+ user: secret.user,
+ secretKey,
+ secretValue,
+ secretComment
+ });
+}
+
/**
* Create secret blind index data containing encrypted blind index [salt]
* for workspace with id [workspaceId]
@@ -271,6 +325,7 @@ export const createSecretHelper = async ({
secretCommentTag,
secretPath = "/",
}: CreateSecretParams) => {
+
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
@@ -448,7 +503,7 @@ export const getSecretsHelper = async ({
folder: folderId,
type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData),
- });
+ }).lean();
// concat with shared secrets
secrets = secrets.concat(
@@ -460,7 +515,7 @@ export const getSecretsHelper = async ({
secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex),
},
- })
+ }).lean()
);
// (EE) create (audit) log
@@ -498,7 +553,7 @@ export const getSecretsHelper = async ({
},
});
}
-
+
return secrets;
};
@@ -546,7 +601,7 @@ export const getSecretHelper = async ({
folder: folderId,
type: type ?? SECRET_PERSONAL,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
- });
+ }).lean();
if (!secret) {
// case: failed to find personal secret matching criteria
@@ -557,7 +612,7 @@ export const getSecretHelper = async ({
environment,
folder: folderId,
type: SECRET_SHARED,
- });
+ }).lean();
}
if (!secret) throw SecretNotFoundError();
@@ -597,7 +652,7 @@ export const getSecretHelper = async ({
},
});
}
-
+
return secret;
};
@@ -788,6 +843,7 @@ export const deleteSecretHelper = async ({
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
+
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
@@ -807,7 +863,7 @@ export const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
- });
+ }).lean();
secret = await Secret.findOneAndDelete({
secretBlindIndex,
@@ -815,7 +871,7 @@ export const deleteSecretHelper = async ({
environment,
type,
folder: folderId,
- });
+ }).lean();
await Secret.deleteMany({
secretBlindIndex,
@@ -831,7 +887,7 @@ export const deleteSecretHelper = async ({
environment,
type,
...getAuthDataPayloadUserObj(authData),
- });
+ }).lean();
if (secret) {
secrets = [secret];
@@ -852,9 +908,7 @@ export const deleteSecretHelper = async ({
secretIds: secrets.map((secret) => secret._id),
});
- // (EE) take a secret snapshot
- action &&
- (await EELogService.createLog({
+ action && (await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
@@ -887,9 +941,9 @@ export const deleteSecretHelper = async ({
},
});
}
-
- return {
+
+ return ({
secrets,
- secret,
- };
+ secret
+ });
};
diff --git a/backend/src/interfaces/services/BotService/index.ts b/backend/src/interfaces/services/BotService/index.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts
index da517b998b..9833e6e566 100644
--- a/backend/src/middleware/requireWorkspaceAuth.ts
+++ b/backend/src/middleware/requireWorkspaceAuth.ts
@@ -16,13 +16,15 @@ const requireWorkspaceAuth = ({
locationWorkspaceId,
locationEnvironment = undefined,
requiredPermissions = [],
- requireBlindIndicesEnabled = false
+ requireBlindIndicesEnabled = false,
+ requireE2EEOff = false
}: {
acceptedRoles: Array<'admin' | 'member'>;
locationWorkspaceId: req;
locationEnvironment?: req | undefined;
requiredPermissions?: string[];
requireBlindIndicesEnabled?: boolean;
+ requireE2EEOff?: boolean;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId;
@@ -35,7 +37,8 @@ const requireWorkspaceAuth = ({
environment,
acceptedRoles,
requiredPermissions,
- requireBlindIndicesEnabled
+ requireBlindIndicesEnabled,
+ requireE2EEOff
});
if (membership) {
diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts
index 6a3d7af885..1f3e239be6 100644
--- a/backend/src/routes/v3/secrets.ts
+++ b/backend/src/routes/v3/secrets.ts
@@ -20,6 +20,142 @@ import {
PERMISSION_READ_SECRETS,
} from "../../variables";
+router.get(
+ "/raw",
+ query("workspaceId").exists().isString().trim(),
+ query("workspaceId").exists().isString().trim(),
+ query("environment").exists().isString().trim(),
+ query("secretPath").default("/").isString().trim(),
+ validateRequest,
+ requireAuth({
+ acceptedAuthModes: [
+ AUTH_MODE_JWT,
+ AUTH_MODE_API_KEY,
+ AUTH_MODE_SERVICE_TOKEN,
+ AUTH_MODE_SERVICE_ACCOUNT,
+ ],
+ }),
+ requireWorkspaceAuth({
+ acceptedRoles: [ADMIN, MEMBER],
+ locationWorkspaceId: "query",
+ locationEnvironment: "query",
+ requiredPermissions: [PERMISSION_READ_SECRETS],
+ requireBlindIndicesEnabled: true,
+ requireE2EEOff: true
+ }),
+ secretsController.getSecretsRaw
+);
+
+router.get(
+ "/raw/:secretName",
+ param("secretName").exists().isString().trim(),
+ query("workspaceId").exists().isString().trim(),
+ query("environment").exists().isString().trim(),
+ query("secretPath").default("/").isString().trim(),
+ query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]),
+ validateRequest,
+ requireAuth({
+ acceptedAuthModes: [
+ AUTH_MODE_JWT,
+ AUTH_MODE_API_KEY,
+ AUTH_MODE_SERVICE_TOKEN,
+ AUTH_MODE_SERVICE_ACCOUNT,
+ ],
+ }),
+ requireWorkspaceAuth({
+ acceptedRoles: [ADMIN, MEMBER],
+ locationWorkspaceId: "query",
+ locationEnvironment: "query",
+ requiredPermissions: [PERMISSION_READ_SECRETS],
+ requireBlindIndicesEnabled: true,
+ requireE2EEOff: true
+ }),
+ secretsController.getSecretByNameRaw
+);
+
+router.post(
+ "/raw/:secretName",
+ body("workspaceId").exists().isString().trim(),
+ body("environment").exists().isString().trim(),
+ body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
+ body("secretValue").exists().isString().trim(),
+ body("secretComment").default("").isString().trim(),
+ body("secretPath").default("/").isString().trim(),
+ validateRequest,
+ requireAuth({
+ acceptedAuthModes: [
+ AUTH_MODE_JWT,
+ AUTH_MODE_API_KEY,
+ AUTH_MODE_SERVICE_TOKEN,
+ AUTH_MODE_SERVICE_ACCOUNT,
+ ],
+ }),
+ requireWorkspaceAuth({
+ acceptedRoles: [ADMIN, MEMBER],
+ locationWorkspaceId: "body",
+ locationEnvironment: "body",
+ requiredPermissions: [PERMISSION_WRITE_SECRETS],
+ requireBlindIndicesEnabled: true,
+ requireE2EEOff: true
+ }),
+ secretsController.createSecretRaw
+);
+
+router.patch(
+ "/raw/:secretName",
+ param("secretName").exists().isString().trim(),
+ body("workspaceId").exists().isString().trim(),
+ body("environment").exists().isString().trim(),
+ body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
+ body("secretValue").exists().isString().trim(),
+ body("secretPath").default("/").isString().trim(),
+ validateRequest,
+ requireAuth({
+ acceptedAuthModes: [
+ AUTH_MODE_JWT,
+ AUTH_MODE_API_KEY,
+ AUTH_MODE_SERVICE_TOKEN,
+ AUTH_MODE_SERVICE_ACCOUNT,
+ ],
+ }),
+ requireWorkspaceAuth({
+ acceptedRoles: [ADMIN, MEMBER],
+ locationWorkspaceId: "body",
+ locationEnvironment: "body",
+ requiredPermissions: [PERMISSION_WRITE_SECRETS],
+ requireBlindIndicesEnabled: true,
+ requireE2EEOff: true
+ }),
+ secretsController.updateSecretByNameRaw
+);
+
+router.delete(
+ "/raw/:secretName",
+ param("secretName").exists().isString().trim(),
+ body("workspaceId").exists().isString().trim(),
+ body("environment").exists().isString().trim(),
+ body("secretPath").default("/").isString().trim(),
+ body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
+ validateRequest,
+ requireAuth({
+ acceptedAuthModes: [
+ AUTH_MODE_JWT,
+ AUTH_MODE_API_KEY,
+ AUTH_MODE_SERVICE_TOKEN,
+ AUTH_MODE_SERVICE_ACCOUNT,
+ ],
+ }),
+ requireWorkspaceAuth({
+ acceptedRoles: [ADMIN, MEMBER],
+ locationWorkspaceId: "body",
+ locationEnvironment: "body",
+ requiredPermissions: [PERMISSION_WRITE_SECRETS],
+ requireBlindIndicesEnabled: true,
+ requireE2EEOff: true
+ }),
+ secretsController.deleteSecretByNameRaw
+);
+
router.get(
"/",
query("workspaceId").exists().isString().trim(),
@@ -40,6 +176,7 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
+ requireE2EEOff: false
}),
secretsController.getSecrets
);
@@ -74,6 +211,7 @@ router.post(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
+ requireE2EEOff: false
}),
secretsController.createSecret
);
@@ -129,6 +267,7 @@ router.patch(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
+ requireE2EEOff: false
}),
secretsController.updateSecretByName
);
@@ -155,6 +294,7 @@ router.delete(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
+ requireE2EEOff: false
}),
secretsController.deleteSecretByName
);
diff --git a/backend/src/routes/v3/workspaces.ts b/backend/src/routes/v3/workspaces.ts
index fdd734cf99..5c4df39d86 100644
--- a/backend/src/routes/v3/workspaces.ts
+++ b/backend/src/routes/v3/workspaces.ts
@@ -8,10 +8,9 @@ import {
import { workspacesController } from '../../controllers/v3';
import {
AUTH_MODE_JWT,
- ADMIN,
- PERMISSION_READ_SECRETS
+ ADMIN
} from '../../variables';
-import { param, body, validationResult } from 'express-validator';
+import { param, body } from 'express-validator';
// -- migration to blind indices endpoints
diff --git a/backend/src/services/BotService.ts b/backend/src/services/BotService.ts
index 2c0db03550..7081a64cbf 100644
--- a/backend/src/services/BotService.ts
+++ b/backend/src/services/BotService.ts
@@ -2,7 +2,9 @@ import { Types } from 'mongoose';
import {
getSecretsBotHelper,
encryptSymmetricHelper,
- decryptSymmetricHelper
+ decryptSymmetricHelper,
+ getKey,
+ getIsWorkspaceE2EEHelper
} from '../helpers/bot';
/**
@@ -10,6 +12,31 @@ import {
*/
class BotService {
+ /**
+ * Return whether or not workspace with id [workspaceId] is end-to-end encrypted
+ * @param workspaceId - id of workspace
+ * @returns {Boolean}
+ */
+ static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) {
+ return await getIsWorkspaceE2EEHelper(workspaceId);
+ }
+
+ /**
+ * Get workspace key for workspace with id [workspaceId] shared to bot.
+ * @param {Object} obj
+ * @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for
+ * @returns
+ */
+ static async getWorkspaceKeyWithBot({
+ workspaceId
+ }: {
+ workspaceId: Types.ObjectId;
+ }) {
+ return await getKey({
+ workspaceId
+ });
+ }
+
/**
* Return decrypted secrets for workspace with id [workspaceId] and
* environment [environmen] shared to bot.
diff --git a/backend/src/services/SecretService.ts b/backend/src/services/SecretService.ts
index d7c1c96443..afd706dbd7 100644
--- a/backend/src/services/SecretService.ts
+++ b/backend/src/services/SecretService.ts
@@ -1,23 +1,22 @@
import { Types } from "mongoose";
-import { ISecret } from "../models";
import {
- CreateSecretParams,
- GetSecretsParams,
- GetSecretParams,
- UpdateSecretParams,
- DeleteSecretParams,
-} from "../interfaces/services/SecretService";
-import {
- createSecretBlindIndexDataHelper,
- getSecretBlindIndexSaltHelper,
- generateSecretBlindIndexWithSaltHelper,
- generateSecretBlindIndexHelper,
- createSecretHelper,
- getSecretsHelper,
- getSecretHelper,
- updateSecretHelper,
- deleteSecretHelper,
-} from "../helpers/secrets";
+ CreateSecretParams,
+ GetSecretsParams,
+ GetSecretParams,
+ UpdateSecretParams,
+ DeleteSecretParams
+} from '../interfaces/services/SecretService';
+import {
+ createSecretBlindIndexDataHelper,
+ getSecretBlindIndexSaltHelper,
+ generateSecretBlindIndexWithSaltHelper,
+ generateSecretBlindIndexHelper,
+ createSecretHelper,
+ getSecretsHelper,
+ getSecretHelper,
+ updateSecretHelper,
+ deleteSecretHelper
+} from '../helpers/secrets';
class SecretService {
/**
diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts
index b7a04634ff..28c4a20ea7 100644
--- a/backend/src/validation/workspace.ts
+++ b/backend/src/validation/workspace.ts
@@ -13,6 +13,7 @@ import { validateServiceAccountClientForWorkspace } from './serviceAccount';
import { validateUserClientForWorkspace } from './user';
import { validateServiceTokenDataClientForWorkspace } from './serviceTokenData';
import {
+ BadRequestError,
UnauthorizedRequestError,
WorkspaceNotFoundError
} from '../utils/errors';
@@ -22,6 +23,7 @@ import {
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
+import { BotService } from '../services';
/**
* Validate authenticated clients for workspace with id [workspaceId] based
@@ -39,7 +41,8 @@ export const validateClientForWorkspace = async ({
environment,
acceptedRoles,
requiredPermissions,
- requireBlindIndicesEnabled
+ requireBlindIndicesEnabled,
+ requireE2EEOff
}: {
authData: {
authMode: string;
@@ -50,6 +53,7 @@ export const validateClientForWorkspace = async ({
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean;
+ requireE2EEOff: boolean;
}) => {
const workspace = await Workspace.findById(workspaceId);
@@ -70,6 +74,14 @@ export const validateClientForWorkspace = async ({
message: 'Failed workspace authorization due to blind indices not being enabled'
});
}
+
+ if (requireE2EEOff) {
+ const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId);
+
+ if (isWorkspaceE2EE) throw BadRequestError({
+ message: 'Failed workspace authorization due to end-to-end encryption not being disabled'
+ });
+ }
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx
index baee98940c..8bad0114f3 100644
--- a/docs/api-reference/overview/authentication.mdx
+++ b/docs/api-reference/overview/authentication.mdx
@@ -5,10 +5,9 @@ description: "How to authenticate with the Infisical Public API"
## Essentials
-The Public API accepts multiple modes of authentication being via API Key, Service Account credentials, or [Infisical Token](/documentation/platform/token).
+The Public API accepts multiple modes of authentication being via API Key or [Infisical Token](/documentation/platform/token).
-- API Key: Provides full access to all endpoints representing the user.
-- Service Account: Provides scoped access to an organization and select projects representing a machine such as a VM or application client.
+- API Key: Provides full access to all endpoints representing the user without ability to encrypt/decrypt secrets in **E2EE** mode.
- [Infisical Token](/documentation/platform/token): Provides short-lived, scoped CRUD access to the secrets of a specific project and environment.
@@ -21,14 +20,6 @@ You can obtain an API key in User Settings > API Keys


-
-
-The Service Account mode uses an Access Key to authenticate with the API and a Public Key and Private Key to perform any cryptographic operations.
-
-To authenticate requests with Infisical using the Access Key, you must include it in the `Authorization` header of HTTP requests made to the platform with the value `Bearer `.
-
-You can create a Service Account in Organization Settings > Service Accounts
-
@@ -40,12 +31,4 @@ You can obtain an Infisical Token in Project Settings > Service Tokens.

-
-
-## Use Cases
-
-Depending on your use case, it may make sense to use one or another authentication mode:
-
-- API Key (not recommended): Use if you need full access to the Public API without needing to access any secrets endpoints (because API keys can't encrypt/decrypt secrets).
-- Service Account (recommeded): Use if you need access to multiple projects and environments in an organization; service accounts can generate short-lived access tokens, making them useful for some complex setups.
-- Service Token (recommeded): Use if you need short-lived, scoped CRUD access to the secrets of a specific project and environment.
+
\ No newline at end of file
diff --git a/docs/api-reference/overview/encryption-modes/e2ee-mode.mdx b/docs/api-reference/overview/encryption-modes/e2ee-mode.mdx
new file mode 100644
index 0000000000..b24ec9fb7c
--- /dev/null
+++ b/docs/api-reference/overview/encryption-modes/e2ee-mode.mdx
@@ -0,0 +1,861 @@
+---
+title: "E2EE Mode"
+---
+
+End-to-End Encrypted (E2EE) mode is the default way to use Infisical's API. With it, you must perform client-side encryption/decryption
+when reading/writing secrets via HTTP call to Infisical.
+
+Prerequisites:
+
+- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
+- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled.
+- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
+- [Ensure that your project is blind-indexed](../blind-indices).
+
+Below, we showcase how to execute common CRUD operations to manage secrets in **E2EE** mode:
+
+
+
+
+
+```js
+const crypto = require('crypto');
+const axios = require('axios');
+
+const BASE_URL = 'https://app.infisical.com';
+const ALGORITHM = 'aes-256-gcm';
+
+const decrypt = ({ ciphertext, iv, tag, secret}) => {
+ const decipher = crypto.createDecipheriv(
+ ALGORITHM,
+ secret,
+ Buffer.from(iv, 'base64')
+ );
+ decipher.setAuthTag(Buffer.from(tag, 'base64'));
+
+ let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
+ cleartext += decipher.final('utf8');
+
+ return cleartext;
+}
+
+const getSecrets = async () => {
+ const serviceToken = 'your_service_token';
+ const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
+
+ // 1. Get your Infisical Token data
+ const { data: serviceTokenData } = await axios.get(
+ `${BASE_URL}/api/v2/service-token`,
+ {
+ headers: {
+ Authorization: `Bearer ${serviceToken}`
+ }
+ }
+ );
+
+ // 2. Get secrets for your project and environment
+ const { data } = await axios.get(
+ `${BASE_URL}/api/v3/secrets?${new URLSearchParams({
+ environment: serviceTokenData.environment,
+ workspaceId: serviceTokenData.workspace
+ })}`,
+ {
+ headers: {
+ Authorization: `Bearer ${serviceToken}`
+ }
+ }
+ );
+
+ const encryptedSecrets = data.secrets;
+
+ // 3. Decrypt the (encrypted) project key with the key from your Infisical Token
+ const projectKey = decrypt({
+ ciphertext: serviceTokenData.encryptedKey,
+ iv: serviceTokenData.iv,
+ tag: serviceTokenData.tag,
+ secret: serviceTokenSecret
+ });
+
+ // 4. Decrypt the (encrypted) secrets
+ const secrets = encryptedSecrets.map((secret) => {
+ const secretKey = decrypt({
+ ciphertext: secret.secretKeyCiphertext,
+ iv: secret.secretKeyIV,
+ tag: secret.secretKeyTag,
+ secret: projectKey
+ });
+
+ const secretValue = decrypt({
+ ciphertext: secret.secretValueCiphertext,
+ iv: secret.secretValueIV,
+ tag: secret.secretValueTag,
+ secret: projectKey
+ });
+
+ return ({
+ secretKey,
+ secretValue
+ });
+ });
+
+ console.log('secrets: ', secrets);
+}
+
+getSecrets();
+
+```
+
+
+
+```Python
+import requests
+import base64
+from Cryptodome.Cipher import AES
+
+
+BASE_URL = "http://app.infisical.com"
+
+
+def decrypt(ciphertext, iv, tag, secret):
+ secret = bytes(secret, "utf-8")
+ iv = base64.standard_b64decode(iv)
+ tag = base64.standard_b64decode(tag)
+ ciphertext = base64.standard_b64decode(ciphertext)
+
+ cipher = AES.new(secret, AES.MODE_GCM, iv)
+ cipher.update(tag)
+ cleartext = cipher.decrypt(ciphertext).decode("utf-8")
+ return cleartext
+
+
+def get_secrets():
+ service_token = "your_service_token"
+ service_token_secret = service_token[service_token.rindex(".") + 1 :]
+
+ # 1. Get your Infisical Token data
+ service_token_data = requests.get(
+ f"{BASE_URL}/api/v2/service-token",
+ headers={"Authorization": f"Bearer {service_token}"},
+ ).json()
+
+ # 2. Get secrets for your project and environment
+ data = requests.get(
+ f"{BASE_URL}/api/v3/secrets",
+ params={
+ "environment": service_token_data["environment"],
+ "workspaceId": service_token_data["workspace"],
+ },
+ headers={"Authorization": f"Bearer {service_token}"},
+ ).json()
+
+ encrypted_secrets = data["secrets"]
+
+ # 3. Decrypt the (encrypted) project key with the key from your Infisical Token
+ project_key = decrypt(
+ ciphertext=service_token_data["encryptedKey"],
+ iv=service_token_data["iv"],
+ tag=service_token_data["tag"],
+ secret=service_token_secret,
+ )
+
+ # 4. Decrypt the (encrypted) secrets
+ secrets = []
+ for secret in encrypted_secrets:
+ secret_key = decrypt(
+ ciphertext=secret["secretKeyCiphertext"],
+ iv=secret["secretKeyIV"],
+ tag=secret["secretKeyTag"],
+ secret=project_key,
+ )
+
+ secret_value = decrypt(
+ ciphertext=secret["secretValueCiphertext"],
+ iv=secret["secretValueIV"],
+ tag=secret["secretValueTag"],
+ secret=project_key,
+ )
+
+ secrets.append(
+ {
+ "secret_key": secret_key,
+ "secret_value": secret_value,
+ }
+ )
+
+ print("secrets:", secrets)
+
+
+get_secrets()
+
+```
+
+
+
+
+
+
+```js
+const crypto = require('crypto');
+const axios = require('axios');
+const nacl = require('tweetnacl');
+
+const BASE_URL = 'https://app.infisical.com';
+const ALGORITHM = 'aes-256-gcm';
+const BLOCK_SIZE_BYTES = 16;
+
+const encrypt = ({ text, secret }) => {
+ const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
+ const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
+
+ let ciphertext = cipher.update(text, 'utf8', 'base64');
+ ciphertext += cipher.final('base64');
+ return {
+ ciphertext,
+ iv: iv.toString('base64'),
+ tag: cipher.getAuthTag().toString('base64')
+ };
+}
+
+const decrypt = ({ ciphertext, iv, tag, secret}) => {
+ const decipher = crypto.createDecipheriv(
+ ALGORITHM,
+ secret,
+ Buffer.from(iv, 'base64')
+ );
+ decipher.setAuthTag(Buffer.from(tag, 'base64'));
+
+ let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
+ cleartext += decipher.final('utf8');
+
+ return cleartext;
+}
+
+const createSecrets = async () => {
+ const serviceToken = '';
+ const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
+
+ const secretType = 'shared'; // 'shared' or 'personal'
+ const secretKey = 'some_key';
+ const secretValue = 'some_value';
+ const secretComment = 'some_comment';
+
+ // 1. Get your Infisical Token data
+ const { data: serviceTokenData } = await axios.get(
+ `${BASE_URL}/api/v2/service-token`,
+ {
+ headers: {
+ Authorization: `Bearer ${serviceToken}`
+ }
+ }
+ );
+
+ // 2. Decrypt the (encrypted) project key with the key from your Infisical Token
+ const projectKey = decrypt({
+ ciphertext: serviceTokenData.encryptedKey,
+ iv: serviceTokenData.iv,
+ tag: serviceTokenData.tag,
+ secret: serviceTokenSecret
+ });
+
+ // 3. Encrypt your secret with the project key
+ const {
+ ciphertext: secretKeyCiphertext,
+ iv: secretKeyIV,
+ tag: secretKeyTag
+ } = encrypt({
+ text: secretKey,
+ secret: projectKey
+ });
+
+ const {
+ ciphertext: secretValueCiphertext,
+ iv: secretValueIV,
+ tag: secretValueTag
+ } = encrypt({
+ text: secretValue,
+ secret: projectKey
+ });
+
+ const {
+ ciphertext: secretCommentCiphertext,
+ iv: secretCommentIV,
+ tag: secretCommentTag
+ } = encrypt({
+ text: secretComment,
+ secret: projectKey
+ });
+
+ // 4. Send (encrypted) secret to Infisical
+ await axios.post(
+ `${BASE_URL}/api/v3/secrets/${secretKey}`,
+ {
+ workspaceId: serviceTokenData.workspace,
+ environment: serviceTokenData.environment,
+ type: secretType,
+ secretKeyCiphertext,
+ secretKeyIV,
+ secretKeyTag,
+ secretValueCiphertext,
+ secretValueIV,
+ secretValueTag,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${serviceToken}`
+ }
+ }
+ );
+}
+
+createSecrets();
+```
+
+
+
+```Python
+import base64
+import requests
+from Cryptodome.Cipher import AES
+from Cryptodome.Random import get_random_bytes
+
+
+BASE_URL = "https://app.infisical.com"
+BLOCK_SIZE_BYTES = 16
+
+
+def encrypt(text, secret):
+ iv = get_random_bytes(BLOCK_SIZE_BYTES)
+ secret = bytes(secret, "utf-8")
+ cipher = AES.new(secret, AES.MODE_GCM, iv)
+ ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8"))
+ return {
+ "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"),
+ "tag": base64.standard_b64encode(tag).decode("utf-8"),
+ "iv": base64.standard_b64encode(iv).decode("utf-8"),
+ }
+
+
+def decrypt(ciphertext, iv, tag, secret):
+ secret = bytes(secret, "utf-8")
+ iv = base64.standard_b64decode(iv)
+ tag = base64.standard_b64decode(tag)
+ ciphertext = base64.standard_b64decode(ciphertext)
+
+ cipher = AES.new(secret, AES.MODE_GCM, iv)
+ cipher.update(tag)
+ cleartext = cipher.decrypt(ciphertext).decode("utf-8")
+ return cleartext
+
+
+def create_secrets():
+ service_token = "your_service_token"
+ service_token_secret = service_token[service_token.rindex(".") + 1 :]
+
+ secret_type = "shared" # "shared or "personal"
+ secret_key = "some_key"
+ secret_value = "some_value"
+ secret_comment = "some_comment"
+
+ # 1. Get your Infisical Token data
+ service_token_data = requests.get(
+ f"{BASE_URL}/api/v2/service-token",
+ headers={"Authorization": f"Bearer {service_token}"},
+ ).json()
+
+ # 2. Decrypt the (encrypted) project key with the key from your Infisical Token
+ project_key = decrypt(
+ ciphertext=service_token_data["encryptedKey"],
+ iv=service_token_data["iv"],
+ tag=service_token_data["tag"],
+ secret=service_token_secret,
+ )
+
+ # 3. Encrypt your secret with the project key
+ encrypted_key_data = encrypt(text=secret_key, secret=project_key)
+ encrypted_value_data = encrypt(text=secret_value, secret=project_key)
+ encrypted_comment_data = encrypt(text=secret_comment, secret=project_key)
+
+ # 4. Send (encrypted) secret to Infisical
+ requests.post(
+ f"{BASE_URL}/api/v3/secrets/{secret_key}",
+ json={
+ "workspaceId": service_token_data["workspace"],
+ "environment": service_token_data["environment"],
+ "type": secret_type,
+ "secretKeyCiphertext": encrypted_key_data["ciphertext"],
+ "secretKeyIV": encrypted_key_data["iv"],
+ "secretKeyTag": encrypted_key_data["tag"],
+ "secretValueCiphertext": encrypted_value_data["ciphertext"],
+ "secretValueIV": encrypted_value_data["iv"],
+ "secretValueTag": encrypted_value_data["tag"],
+ "secretCommentCiphertext": encrypted_comment_data["ciphertext"],
+ "secretCommentIV": encrypted_comment_data["iv"],
+ "secretCommentTag": encrypted_comment_data["tag"]
+ },
+ headers={"Authorization": f"Bearer {service_token}"},
+ )
+
+
+create_secrets()
+
+```
+
+
+
+
+
+
+```js
+const crypto = require('crypto');
+const axios = require('axios');
+
+const BASE_URL = 'https://app.infisical.com';
+const ALGORITHM = 'aes-256-gcm';
+
+const decrypt = ({ ciphertext, iv, tag, secret}) => {
+ const decipher = crypto.createDecipheriv(
+ ALGORITHM,
+ secret,
+ Buffer.from(iv, 'base64')
+ );
+ decipher.setAuthTag(Buffer.from(tag, 'base64'));
+
+ let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
+ cleartext += decipher.final('utf8');
+
+ return cleartext;
+}
+
+const getSecret = async () => {
+ const serviceToken = 'your_service_token';
+ const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
+
+ const secretType = 'shared' // 'shared' or 'personal'
+ const secretKey = 'some_key';
+
+ // 1. Get your Infisical Token data
+ const { data: serviceTokenData } = await axios.get(
+ `${BASE_URL}/api/v2/service-token`,
+ {
+ headers: {
+ Authorization: `Bearer ${serviceToken}`
+ }
+ }
+ );
+
+ // 2. Get the secret from your project and environment
+ const { data } = await axios.get(
+ `${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({
+ environment: serviceTokenData.environment,
+ workspaceId: serviceTokenData.workspace,
+ type: secretType // optional, defaults to 'shared'
+ })}`,
+ {
+ headers: {
+ Authorization: `Bearer ${serviceToken}`
+ }
+ }
+ );
+
+ const encryptedSecret = data.secret;
+
+ // 3. Decrypt the (encrypted) project key with the key from your Infisical Token
+ const projectKey = decrypt({
+ ciphertext: serviceTokenData.encryptedKey,
+ iv: serviceTokenData.iv,
+ tag: serviceTokenData.tag,
+ secret: serviceTokenSecret
+ });
+
+ // 4. Decrypt the (encrypted) secret value
+
+ const secretValue = decrypt({
+ ciphertext: encryptedSecret.secretValueCiphertext,
+ iv: encryptedSecret.secretValueIV,
+ tag: encryptedSecret.secretValueTag,
+ secret: projectKey
+ });
+
+ console.log('secret: ', ({
+ secretKey,
+ secretValue
+ }));
+}
+
+getSecret();
+
+```
+
+
+
+```Python
+import requests
+import base64
+from Cryptodome.Cipher import AES
+
+
+BASE_URL = "http://app.infisical.com"
+
+
+def decrypt(ciphertext, iv, tag, secret):
+ secret = bytes(secret, "utf-8")
+ iv = base64.standard_b64decode(iv)
+ tag = base64.standard_b64decode(tag)
+ ciphertext = base64.standard_b64decode(ciphertext)
+
+ cipher = AES.new(secret, AES.MODE_GCM, iv)
+ cipher.update(tag)
+ cleartext = cipher.decrypt(ciphertext).decode("utf-8")
+ return cleartext
+
+
+def get_secret():
+ service_token = "your_service_token"
+ service_token_secret = service_token[service_token.rindex(".") + 1 :]
+
+ secret_type = "shared" # "shared" or "personal"
+ secret_key = "some_key"
+
+ # 1. Get your Infisical Token data
+ service_token_data = requests.get(
+ f"{BASE_URL}/api/v2/service-token",
+ headers={"Authorization": f"Bearer {service_token}"},
+ ).json()
+
+ # 2. Get secret from your project and environment
+ data = requests.get(
+ f"{BASE_URL}/api/v3/secrets/{secret_key}",
+ params={
+ "environment": service_token_data["environment"],
+ "workspaceId": service_token_data["workspace"],
+ "type": secret_type # optional, defaults to "shared"
+ },
+ headers={"Authorization": f"Bearer {service_token}"},
+ ).json()
+
+ encrypted_secret = data["secret"]
+
+ # 3. Decrypt the (encrypted) project key with the key from your Infisical Token
+ project_key = decrypt(
+ ciphertext=service_token_data["encryptedKey"],
+ iv=service_token_data["iv"],
+ tag=service_token_data["tag"],
+ secret=service_token_secret,
+ )
+
+ # 4. Decrypt the (encrypted) secret value
+ secret_value = decrypt(
+ ciphertext=encrypted_secret["secretValueCiphertext"],
+ iv=encrypted_secret["secretValueIV"],
+ tag=encrypted_secret["secretValueTag"],
+ secret=project_key,
+ )
+
+ print("secret: ", {
+ "secret_key": secret_key,
+ "secret_value": secret_value
+ })
+
+
+get_secret()
+
+```
+
+
+
+
+
+
+```js
+const crypto = require('crypto');
+const axios = require('axios');
+
+const BASE_URL = 'https://app.infisical.com';
+const ALGORITHM = 'aes-256-gcm';
+const BLOCK_SIZE_BYTES = 16;
+
+const encrypt = ({ text, secret }) => {
+ const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
+ const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
+
+ let ciphertext = cipher.update(text, 'utf8', 'base64');
+ ciphertext += cipher.final('base64');
+ return {
+ ciphertext,
+ iv: iv.toString('base64'),
+ tag: cipher.getAuthTag().toString('base64')
+ };
+}
+
+const decrypt = ({ ciphertext, iv, tag, secret}) => {
+ const decipher = crypto.createDecipheriv(
+ ALGORITHM,
+ secret,
+ Buffer.from(iv, 'base64')
+ );
+ decipher.setAuthTag(Buffer.from(tag, 'base64'));
+
+ let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
+ cleartext += decipher.final('utf8');
+
+ return cleartext;
+}
+
+const updateSecrets = async () => {
+ const serviceToken = 'your_service_token';
+ const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
+
+ const secretType = 'shared' // 'shared' or 'personal'
+ const secretKey = 'some_key';
+ const secretValue = 'updated_value';
+ const secretComment = 'updated_comment';
+
+ // 1. Get your Infisical Token data
+ const { data: serviceTokenData } = await axios.get(
+ `${BASE_URL}/api/v2/service-token`,
+ {
+ headers: {
+ Authorization: `Bearer ${serviceToken}`
+ }
+ }
+ );
+
+ // 2. Decrypt the (encrypted) project key with the key from your Infisical Token
+ const projectKey = decrypt({
+ ciphertext: serviceTokenData.encryptedKey,
+ iv: serviceTokenData.iv,
+ tag: serviceTokenData.tag,
+ secret: serviceTokenSecret
+ });
+
+ // 3. Encrypt your updated secret with the project key
+ const {
+ ciphertext: secretKeyCiphertext,
+ iv: secretKeyIV,
+ tag: secretKeyTag
+ } = encrypt({
+ text: secretKey,
+ secret: projectKey
+ });
+
+ const {
+ ciphertext: secretValueCiphertext,
+ iv: secretValueIV,
+ tag: secretValueTag
+ } = encrypt({
+ text: secretValue,
+ secret: projectKey
+ });
+
+ const {
+ ciphertext: secretCommentCiphertext,
+ iv: secretCommentIV,
+ tag: secretCommentTag
+ } = encrypt({
+ text: secretComment,
+ secret: projectKey
+ });
+
+ // 4. Send (encrypted) updated secret to Infisical
+ await axios.patch(
+ `${BASE_URL}/api/v3/secrets/${secretKey}`,
+ {
+ workspaceId: serviceTokenData.workspace,
+ environment: serviceTokenData.environment,
+ type: secretType,
+ secretValueCiphertext,
+ secretValueIV,
+ secretValueTag,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${serviceToken}`
+ }
+ }
+ );
+}
+
+updateSecrets();
+```
+
+
+
+```Python
+import base64
+import requests
+from Cryptodome.Cipher import AES
+from Cryptodome.Random import get_random_bytes
+
+
+BASE_URL = "https://app.infisical.com"
+BLOCK_SIZE_BYTES = 16
+
+
+def encrypt(text, secret):
+ iv = get_random_bytes(BLOCK_SIZE_BYTES)
+ secret = bytes(secret, "utf-8")
+ cipher = AES.new(secret, AES.MODE_GCM, iv)
+ ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8"))
+ return {
+ "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"),
+ "tag": base64.standard_b64encode(tag).decode("utf-8"),
+ "iv": base64.standard_b64encode(iv).decode("utf-8"),
+ }
+
+
+def decrypt(ciphertext, iv, tag, secret):
+ secret = bytes(secret, "utf-8")
+ iv = base64.standard_b64decode(iv)
+ tag = base64.standard_b64decode(tag)
+ ciphertext = base64.standard_b64decode(ciphertext)
+
+ cipher = AES.new(secret, AES.MODE_GCM, iv)
+ cipher.update(tag)
+ cleartext = cipher.decrypt(ciphertext).decode("utf-8")
+ return cleartext
+
+
+def update_secret():
+ service_token = "your_service_token"
+ service_token_secret = service_token[service_token.rindex(".") + 1 :]
+
+ secret_type = "shared" # "shared" or "personal"
+ secret_key = "some_key"
+ secret_value = "updated_value"
+ secret_comment = "updated_comment"
+
+ # 1. Get your Infisical Token data
+ service_token_data = requests.get(
+ f"{BASE_URL}/api/v2/service-token",
+ headers={"Authorization": f"Bearer {service_token}"},
+ ).json()
+
+ # 2. Decrypt the (encrypted) project key with the key from your Infisical Token
+ project_key = decrypt(
+ ciphertext=service_token_data["encryptedKey"],
+ iv=service_token_data["iv"],
+ tag=service_token_data["tag"],
+ secret=service_token_secret,
+ )
+
+ # 3. Encrypt your updated secret with the project key
+ encrypted_key_data = encrypt(text=secret_key, secret=project_key)
+ encrypted_value_data = encrypt(text=secret_value, secret=project_key)
+ encrypted_comment_data = encrypt(text=secret_comment, secret=project_key)
+
+ # 4. Send (encrypted) updated secret to Infisical
+ requests.patch(
+ f"{BASE_URL}/api/v3/secrets/{secret_key}",
+ json={
+ "workspaceId": service_token_data["workspace"],
+ "environment": service_token_data["environment"],
+ "type": secret_type,
+ "secretKeyCiphertext": encrypted_key_data["ciphertext"],
+ "secretKeyIV": encrypted_key_data["iv"],
+ "secretKeyTag": encrypted_key_data["tag"],
+ "secretValueCiphertext": encrypted_value_data["ciphertext"],
+ "secretValueIV": encrypted_value_data["iv"],
+ "secretValueTag": encrypted_value_data["tag"],
+ "secretCommentCiphertext": encrypted_comment_data["ciphertext"],
+ "secretCommentIV": encrypted_comment_data["iv"],
+ "secretCommentTag": encrypted_comment_data["tag"]
+ },
+ headers={"Authorization": f"Bearer {service_token}"},
+ )
+
+
+update_secret()
+
+```
+
+
+
+
+
+
+```js
+const axios = require('axios');
+const BASE_URL = 'https://app.infisical.com';
+
+const deleteSecrets = async () => {
+ const serviceToken = 'your_service_token';
+ const secretType = 'shared' // 'shared' or 'personal'
+ const secretKey = 'some_key'
+
+ // 1. Get your Infisical Token data
+ const { data: serviceTokenData } = await axios.get(
+ `${BASE_URL}/api/v2/service-token`,
+ {
+ headers: {
+ Authorization: `Bearer ${serviceToken}`
+ }
+ }
+ );
+
+ // 2. Delete secret from Infisical
+ await axios.delete(
+ `${BASE_URL}/api/v3/secrets/${secretKey}`,
+ {
+ workspaceId: serviceTokenData.workspace,
+ environment: serviceTokenData.environment,
+ type: secretType
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${serviceToken}`
+ },
+ }
+ );
+};
+
+deleteSecrets();
+```
+
+
+
+```Python
+import requests
+
+BASE_URL = "https://app.infisical.com"
+
+
+def delete_secrets():
+ service_token = ""
+ secret_type = "shared" # "shared" or "personal"
+ secret_key = "some_key"
+
+ # 1. Get your Infisical Token data
+ service_token_data = requests.get(
+ f"{BASE_URL}/api/v2/service-token",
+ headers={"Authorization": f"Bearer {service_token}"},
+ ).json()
+
+ # 2. Delete secret from Infisical
+ requests.delete(
+ f"{BASE_URL}/api/v2/secrets/{secret_key}",
+ json={
+ "workspaceId": service_token_data["workspace"],
+ "environment": service_token_data["environment"],
+ "type": secret_type
+ },
+ headers={"Authorization": f"Bearer {service_token}"},
+ )
+
+
+delete_secrets()
+
+```
+
+
+
+ If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header.
+
+
+
+
\ No newline at end of file
diff --git a/docs/api-reference/overview/encryption-modes/es-mode.mdx b/docs/api-reference/overview/encryption-modes/es-mode.mdx
new file mode 100644
index 0000000000..2756980ac9
--- /dev/null
+++ b/docs/api-reference/overview/encryption-modes/es-mode.mdx
@@ -0,0 +1,92 @@
+---
+title: "ES Mode"
+---
+
+Encrypted Standard (ES) mode is the easiest way to use Infisical's API. With it, you can make HTTP calls to Infisical
+to read/write secrets in plaintext.
+
+Prerequisites:
+
+- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
+- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled.
+- [Ensure that your project is blind-indexed](../blind-indices).
+
+Below, we showcase how to execute common CRUD operations to manage secrets in **ES** mode:
+
+
+
+
+
+ ```bash
+ curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw?environment=dev&workspaceId=xxx' \
+ --header 'Authorization: Bearer st.xxx'
+
+ ```
+
+
+
+
+
+
+ ```bash
+ curl --location --request POST 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \
+ --header 'Authorization: Bearer st.xxx' \
+ --header 'Content-Type: application/json' \
+ --data-raw '{
+ "workspaceId": "xxx",
+ "environment": "dev",
+ "type": "shared",
+ "secretValue": "SECRET_VALUE",
+ "secretPath": "/"
+ }'
+ ```
+
+
+
+
+
+
+ ```bash
+ curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME?workspaceId=xxx&environment=dev&secretPath=/' \
+ --header 'Authorization: Bearer st.xxx'
+ ```
+
+
+
+
+
+
+ ```bash
+ curl --location --request PATCH 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \
+ --header 'Authorization: Bearer st.xxx' \
+ --header 'Content-Type: application/json' \
+ --data-raw '{
+ "workspaceId": "xxx",
+ "environment": "dev",
+ "type": "shared",
+ "secretValue": "SECRET_VALUE",
+ "secretPath": "/"
+ }'
+ ```
+
+
+
+
+
+
+ ```bash
+ curl --location --request DELETE 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \
+ --header 'Authorization: Bearer st.xxx' \
+ --header 'Content-Type: application/json' \
+ --data-raw '{
+ "workspaceId": "xxx",
+ "environment": "dev",
+ "type": "shared",
+ "secretValue": "SECRET_VALUE",
+ "secretPath": "/"
+ }'
+ ```
+
+
+
+
diff --git a/docs/api-reference/overview/encryption-modes/overview.mdx b/docs/api-reference/overview/encryption-modes/overview.mdx
new file mode 100644
index 0000000000..84606d1149
--- /dev/null
+++ b/docs/api-reference/overview/encryption-modes/overview.mdx
@@ -0,0 +1,57 @@
+---
+title: "Preface"
+---
+
+Each project in Infisical can be used either in **End-to-End Encrypted (E2EE)** mode or **Encrypted Standard (ES)** mode which dictates how it can be interacted with via the Infisical API.
+
+
+
+ Secret operations without client-side encryption/decryption
+
+
+ Secret operations with client-side encryption/decryption
+
+
+
+By default, all projects are initialized in **E2EE** mode which means the server is not able to decrypt any values because all secret encryption/decryption operations occur on the client-side. However, this has limitations around functionality and ease-of-use:
+
+- You cannot make HTTP calls to Infisical to read/write secrets in plaintext.
+- You cannot leverage non-E2EE features like native integrations and in-platform automations like dynamic secrets and secret rotation.
+
+For this reason, Infisical also provides the **ES** mode of operation to unlock the above limitations by enabling the server to decrypt your values. You can optionally switch a project to using **ES** mode
+in your Project Settings.
+
+
+ Make no mistake, the limitations of **E2EE** mode do not prevent you from syncing secrets from Infisical to platforms like GitLab. They just imply
+ that you have to do things the "E2EE-way" such as by embedding the Infisical CLI into your GitLab CI/CD pipelines to fetch and decrypt
+ secrets on the client-side.
+
+
+## FAQ
+
+
+
+ We recommend starting with **E2EE** mode and switching to **ES** mode when:
+
+ - Your team needs more power out of non-E2EE features available in **ES** mode such as secret rotation, dynamic secrets, etc.
+ - Your team wants an easier way to read/write secrets with Infisical.
+
+
+
+ By default, all projects in Infisical are initialized to **E2EE** mode and can be switched to **ES** mode in the Project Settings by disabling end-to-end encryption.
+
+
+ **ES** mode is secure and in fact what most vendors in the secret management industry are doing at the moment. In this mode, secrets are encrypted at rest by
+ a series of keys, secured ultimately by a top-level `ROOT_ENCRYPTION_KEY` located on the server.
+
+ If you're concerned about Infisical Cloud's ability to read your secrets if using **ES** mode in Infisical Cloud, then you may wish to
+ use Infisical Cloud in **E2EE** mode or self-host Infisical on your own infrastructure and then use **ES** mode; this of course which means setting up firewalls and securing the instance yourself.
+
+ As an organization, we prohibit reading any customer secrets without explicit permission; access to the `ROOT_ENCRYPTION_KEY` is restricted to one individual in the organization.
+
+
\ No newline at end of file
diff --git a/docs/api-reference/overview/introduction.mdx b/docs/api-reference/overview/introduction.mdx
index 5199256e52..546e0b9a26 100644
--- a/docs/api-reference/overview/introduction.mdx
+++ b/docs/api-reference/overview/introduction.mdx
@@ -8,31 +8,6 @@ rotating credentials, or for integrating secret management into a larger system.
With the Public API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
-
- We highly recommend using one of the available SDKs when working with the Infisical API.
-
- If you decide to make your own requests using the API reference instead, be prepared for a steeper learning curve and more manual work.
-
-
In April 2023, we added the capability for users to query for secrets by name to improve the user experience of Infisical. If your project was created prior to April 2023, please read and follow the section on [blind indices](./blind-indices) and how to enable them for better usage of Infisical.
-
-
-## Concepts
-
-Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview). A few key points:
-
-- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by protected keys that are encrypted by keys derived from Argon2id applied to the user's password before being sent off to the server during the account signup process.
-- Each (encrypted) secret belongs to a project and environment.
-- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key.
-- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing.
-- Infisical Tokens contain a symmetric key that can be used to decrypt a copy of a project key from the [call to get the Infisical Token data](/api-reference/endpoints/service-tokens/get).
-- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.
-
-
- Infisical's system requires that secrets be encrypted/decrypted on the
- client-side to maintain E2EE. We strongly recommend you read up on the system
- prior to using the Infisical API. The (opt-in) ability to retrieve secrets
- back in decrypted format if you choose to share secrets with Infisical is on
- our roadmap.
-
+
\ No newline at end of file
diff --git a/docs/mint.json b/docs/mint.json
index 553db5172a..7be9707706 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -247,17 +247,15 @@
"pages": [
"api-reference/overview/introduction",
"api-reference/overview/authentication",
- "api-reference/overview/blind-indices",
{
"group": "Examples",
"pages": [
- "api-reference/overview/examples/retrieve-secrets",
- "api-reference/overview/examples/create-secret",
- "api-reference/overview/examples/retrieve-secret",
- "api-reference/overview/examples/update-secret",
- "api-reference/overview/examples/delete-secret"
+ "api-reference/overview/encryption-modes/overview",
+ "api-reference/overview/encryption-modes/es-mode",
+ "api-reference/overview/encryption-modes/e2ee-mode"
]
- }
+ },
+ "api-reference/overview/blind-indices"
]
},
{
diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx
index 3a9d8fd2f9..e38a552421 100644
--- a/frontend/src/hooks/api/workspace/index.tsx
+++ b/frontend/src/hooks/api/workspace/index.tsx
@@ -12,4 +12,5 @@ export {
useNameWorkspaceSecrets,
useRenameWorkspace,
useToggleAutoCapitalization,
- useUpdateWsEnvironment} from './queries';
+ useUpdateWsEnvironment
+} from './queries';
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx
index 5e8f5afe79..ee9b85cc58 100644
--- a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx
+++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx
@@ -44,7 +44,8 @@ import {
EnvironmentSection,
ProjectIndexSecretsSection,
ProjectNameChangeSection,
- ServiceTokenSection
+ ServiceTokenSection,
+ E2EESection
} from './components';
export const ProjectSettingsPage = () => {
@@ -400,8 +401,13 @@ export const ProjectSettingsPage = () => {
onAutoCapitalizationChange={onAutoCapitalizationToggle}
/>
{!isBlindIndexedLoading && !isBlindIndexed && (
-
+
)}
+
{t('settings.project.danger-zone')}
{t('settings.project.danger-zone-note')}
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx
new file mode 100644
index 0000000000..4faf3e7637
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx
@@ -0,0 +1,115 @@
+import { useEffect, useState } from "react";
+
+import {
+ decryptAssymmetric,
+ encryptAssymmetric
+} from "@app/components/utilities/cryptography/crypto";
+import {
+ Checkbox
+} from "@app/components/v2";
+
+import getBot from '../../../../../pages/api/bot/getBot';
+import setBotActiveStatus from '../../../../../pages/api/bot/setBotActiveStatus';
+import getLatestFileKey from '../../../../../pages/api/workspace/getLatestFileKey';
+
+type Props = {
+ workspaceId: string;
+}
+
+export const E2EESection = ({
+ workspaceId
+}: Props) => {
+ const [bot, setBot] = useState
(null);
+
+ useEffect(() => {
+ (async () => {
+ // get project bot
+ setBot(await getBot({ workspaceId }));
+ })();
+ }, []);
+
+ /**
+ * Activate bot for project by performing the following steps:
+ * 1. Get the (encrypted) project key
+ * 2. Decrypt project key with user's private key
+ * 3. Encrypt project key with bot's public key
+ * 4. Send encrypted project key to backend and set bot status to active
+ */
+ const toggleBotActivate = async () => {
+ let botKey;
+ try {
+ if (bot) {
+ // case: there is a bot
+
+ if (!bot.isActive) {
+ // bot is not active -> activate bot
+ const key = await getLatestFileKey({ workspaceId });
+ const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
+
+ if (!PRIVATE_KEY) {
+ throw new Error('Private Key missing');
+ }
+
+ const WORKSPACE_KEY = decryptAssymmetric({
+ ciphertext: key.latestKey.encryptedKey,
+ nonce: key.latestKey.nonce,
+ publicKey: key.latestKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ const { ciphertext, nonce } = encryptAssymmetric({
+ plaintext: WORKSPACE_KEY,
+ publicKey: bot.publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ botKey = {
+ encryptedKey: ciphertext,
+ nonce
+ };
+
+ const botx = await setBotActiveStatus({
+ botId: bot._id,
+ isActive: true,
+ botKey
+ });
+
+ setBot(botx.bot);
+ } else {
+ // bot is active -> deactivate bot
+ const botx = await setBotActiveStatus({
+ botId: bot._id,
+ isActive: false
+ });
+
+ setBot(botx.bot);
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ return bot ? (
+
+
End-to-End Encryption
+
+ Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to cloud providers as well as HTTP calls to get secrets back raw but enables the server to read/decrypt your secret values.
+
+
+ Note that, even with E2EE disabled, your secrets are always encrypted at rest.
+
+
{
+ await toggleBotActivate();
+ }}
+ >
+ End-to-end encryption enabled
+
+
+ ) : ;
+ };
+
\ No newline at end of file
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/index.tsx
new file mode 100644
index 0000000000..f2de56bf42
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/index.tsx
@@ -0,0 +1,3 @@
+export {
+ E2EESection
+} from './E2EESection';
\ No newline at end of file
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx
index a591114470..34a0c22f51 100644
--- a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx
@@ -6,3 +6,4 @@ export { ProjectNameChangeSection } from './ProjectNameChangeSection';
export type { CreateWsTag } from './SecretTagsSection/SecretTagsSection';
export { ServiceTokenSection } from './ServiceTokenSection';
export type { CreateServiceToken } from './ServiceTokenSection/ServiceTokenSection';
+export { E2EESection } from './E2EESection';