From 0187d3012b203400297fb452e8e9d6a923a4a703 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 9 Jun 2023 21:20:12 +0100 Subject: [PATCH 1/4] Add non-e2ee option for getSecret, getSecrets, start createSecret --- .../src/controllers/v3/secretsController.ts | 14 +- backend/src/helpers/bot.ts | 20 ++- backend/src/helpers/secrets.ts | 152 +++++++++++++++++- .../interfaces/services/BotService/index.ts | 0 .../services/SecretService/index.ts | 14 +- backend/src/routes/v3/secrets.ts | 14 +- backend/src/services/BotService.ts | 20 ++- backend/src/services/SecretService.ts | 3 - 8 files changed, 202 insertions(+), 35 deletions(-) create mode 100644 backend/src/interfaces/services/BotService/index.ts diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index dac41b53d2..321ab41d22 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -6,8 +6,6 @@ import { EventService } from '../../services'; import { eventPushSecrets } from '../../events'; -import { getAuthDataPayloadIdObj } from '../../utils/auth'; -import { BadRequestError } from '../../utils/errors'; /** * Get secrets for workspace with id [workspaceId] and environment @@ -68,9 +66,11 @@ export const createSecret = async (req: Request, res: Response) => { secretKeyCiphertext, secretKeyIV, secretKeyTag, + secretValue, secretValueCiphertext, secretValueIV, secretValueTag, + secretComment, secretCommentCiphertext, secretCommentIV, secretCommentTag @@ -85,14 +85,14 @@ export const createSecret = async (req: Request, res: Response) => { secretKeyCiphertext, secretKeyIV, secretKeyTag, + secretValue, secretValueCiphertext, secretValueIV, secretValueTag, - ...((secretCommentCiphertext && secretCommentIV && secretCommentTag) ? { - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - } : {}) + secretComment, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag }); await EventService.handleEvent({ 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 1c58119b11..baa8d292e5 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -6,7 +6,12 @@ import { UpdateSecretParams, DeleteSecretParams, } from '../interfaces/services/SecretService'; -import { Secret, ISecret, SecretBlindIndexData } from '../models'; +import { + ISecret, + Secret, + SecretBlindIndexData, + BotKey +} from '../models'; import { SecretVersion } from '../ee/models'; import { BadRequestError, @@ -32,7 +37,7 @@ import { decryptSymmetric128BitHexKeyUTF8, } from '../utils/crypto'; import { getEncryptionKey, client, getRootEncryptionKey } from '../config'; -import { TelemetryService } from '../services'; +import { BotService, TelemetryService } from '../services'; import { EESecretService, EELogService } from '../ee/services'; import { getAuthDataPayloadIdObj, @@ -237,6 +242,23 @@ export const generateSecretBlindIndexHelper = async ({ }); }; +// secretName, +// workspaceId, +// environment, +// type, +// authData, +// secretKeyCiphertext, +// secretKeyIV, +// secretKeyTag, +// secretValue, +// secretValueCiphertext, +// secretValueIV, +// secretValueTag, +// secretCommentCiphertext, +// secretCommentIV, +// secretCommentTag, +// folderId, + /** * Create secret with name [secretName] * @param {Object} obj @@ -256,14 +278,17 @@ export const createSecretHelper = async ({ secretKeyCiphertext, secretKeyIV, secretKeyTag, + secretValue, secretValueCiphertext, secretValueIV, secretValueTag, + secretComment, secretCommentCiphertext, secretCommentIV, secretCommentTag, folderId, }: CreateSecretParams) => { + const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, workspaceId: new Types.ObjectId(workspaceId), @@ -298,6 +323,52 @@ export const createSecretHelper = async ({ }); } + // can generate secretKeyCiphertext etc. if not E2EE. + const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); + + if (!isWorkspaceE2EE) { + // if workspace is not end-to-end encrypted, then decrypt + // secret and return it in plaintext + + const key = await BotService.getWorkspaceKeyWithBot({ + workspaceId + }); + + if (secretName) { + const encryptedSecretKey = encryptSymmetric128BitHexKeyUTF8({ + plaintext: secretName, + key + }); + + secretKeyCiphertext = encryptedSecretKey.ciphertext; + secretKeyIV = encryptedSecretKey.iv; + secretKeyTag = encryptedSecretKey.tag; + } + + if (secretValue) { + const encryptedSecretValue = encryptSymmetric128BitHexKeyUTF8({ + plaintext: secretValue, + key + }); + + secretValueCiphertext = encryptedSecretValue.ciphertext; + secretValueIV = encryptedSecretValue.iv; + secretValueTag = encryptedSecretValue.tag; + } + + if (secretComment) { + const encryptedSecretComment = encryptSymmetric128BitHexKeyUTF8({ + plaintext: secretComment, + key + }); + + secretCommentCiphertext = encryptedSecretComment.ciphertext; + secretCommentIV = encryptedSecretComment.iv; + secretCommentTag = encryptedSecretComment.tag; + } + } + + // create secret const secret = await new Secret({ version: 1, @@ -410,7 +481,7 @@ export const getSecretsHelper = async ({ environment, type: SECRET_PERSONAL, ...getAuthDataPayloadUserObj(authData), - }); + }).lean(); // concat with shared secrets secrets = secrets.concat( @@ -421,7 +492,7 @@ export const getSecretsHelper = async ({ secretBlindIndex: { $nin: secrets.map((secret) => secret.secretBlindIndex), }, - }) + }).lean() ); // (EE) create (audit) log @@ -458,8 +529,45 @@ export const getSecretsHelper = async ({ }, }); } + + const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); - return secrets; + if (!isWorkspaceE2EE) { + // if workspace is not end-to-end encrypted, then decrypt + // secret and return it in plaintext + + const key = await BotService.getWorkspaceKeyWithBot({ + workspaceId + }); + + return secrets.map((secret) => { + const secretName = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key + }); + + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }); + + return ({ + ...secret, + secretName, + secretValue + }); + }); + } + + return secrets.map((secret) => ({ + ...secret, + secretName: null, + secretValue: null + })); }; /** @@ -492,7 +600,7 @@ export const getSecretHelper = async ({ environment, type: type ?? SECRET_PERSONAL, ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), - }); + }).lean(); if (!secret) { // case: failed to find personal secret matching criteria @@ -502,7 +610,7 @@ export const getSecretHelper = async ({ workspace: new Types.ObjectId(workspaceId), environment, type: SECRET_SHARED, - }); + }).lean(); } if (!secret) throw SecretNotFoundError(); @@ -541,8 +649,36 @@ export const getSecretHelper = async ({ }, }); } + + const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); + + if (!isWorkspaceE2EE) { + // if workspace is not end-to-end encrypted, then decrypt + // secret and return it in plaintext - return secret; + const key = await BotService.getWorkspaceKeyWithBot({ + workspaceId + }); + + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }); + + return ({ + ...secret, + secretName, + secretValue + }); + } + + return ({ + ...secret, + secretName: null, + secretValue: null + }); }; /** 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/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index 2efd53f4eb..920db69cdc 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -8,12 +8,14 @@ export interface CreateSecretParams { folderId?: string; type: "shared" | "personal"; authData: AuthData; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; + secretKeyCiphertext?: string; + secretKeyIV?: string; + secretKeyTag?: string; + secretValue?: string; + secretValueCiphertext?: string; + secretValueIV?: string; + secretValueTag?: string; + secretComment?: string; secretCommentCiphertext?: string; secretCommentIV?: string; secretCommentTag?: string; diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 6a18fdf736..6d8b11c296 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -48,12 +48,14 @@ router.post( body('workspaceId').exists().isString().trim(), body('environment').exists().isString().trim(), body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body('secretKeyCiphertext').exists().isString().trim(), - body('secretKeyIV').exists().isString().trim(), - body('secretKeyTag').exists().isString().trim(), - body('secretValueCiphertext').exists().isString().trim(), - body('secretValueIV').exists().isString().trim(), - body('secretValueTag').exists().isString().trim(), + body('secretKeyCiphertext').optional().isString().trim(), + body('secretKeyIV').optional().isString().trim(), + body('secretKeyTag').optional().isString().trim(), + body('secretValue').optional().isString().trim(), + body('secretValueCiphertext').optional().isString().trim(), + body('secretValueIV').optional().isString().trim(), + body('secretValueTag').optional().isString().trim(), + body('secretComment').optional().isString().trim(), body('secretCommentCiphertext').optional().isString().trim(), body('secretCommentIV').optional().isString().trim(), body('secretCommentTag').optional().isString().trim(), diff --git a/backend/src/services/BotService.ts b/backend/src/services/BotService.ts index 2c0db03550..ab6d8fbd4d 100644 --- a/backend/src/services/BotService.ts +++ b/backend/src/services/BotService.ts @@ -2,13 +2,31 @@ import { Types } from 'mongoose'; import { getSecretsBotHelper, encryptSymmetricHelper, - decryptSymmetricHelper + decryptSymmetricHelper, + getKey, + getIsWorkspaceE2EEHelper } from '../helpers/bot'; +// rename the functions here +// refactor the interface situation here + /** * Class to handle bot actions */ class BotService { + static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) { + return await getIsWorkspaceE2EEHelper(workspaceId); + } + + static async getWorkspaceKeyWithBot({ + workspaceId + }: { + workspaceId: Types.ObjectId; + }) { + return await getKey({ + workspaceId + }); + } /** * Return decrypted secrets for workspace with id [workspaceId] and diff --git a/backend/src/services/SecretService.ts b/backend/src/services/SecretService.ts index 5b5b73e5f4..9e430ab3dd 100644 --- a/backend/src/services/SecretService.ts +++ b/backend/src/services/SecretService.ts @@ -1,7 +1,4 @@ import { Types } from 'mongoose'; -import { - ISecret -} from '../models'; import { CreateSecretParams, GetSecretsParams, From 631eac803e40cb18ef652830f2a74ea9f1d46285 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 11 Jun 2023 12:11:25 +0100 Subject: [PATCH 2/4] Finish preliminary v3/secrets/raw endpoints --- .../src/controllers/v3/secretsController.ts | 308 ++++++++++++++---- backend/src/helpers/secrets.ts | 209 ++++-------- .../services/SecretService/index.ts | 14 +- .../src/middleware/requireWorkspaceAuth.ts | 7 +- backend/src/routes/v3/secrets.ts | 140 ++++++++ backend/src/validation/workspace.ts | 14 +- 6 files changed, 475 insertions(+), 217 deletions(-) diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 63aa2d6605..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 */ @@ -58,59 +292,6 @@ export const getSecretByName = async (req: Request, res: Response) => { * @param res */ export const createSecret = async (req: Request, res: Response) => { -<<<<<<< HEAD - const { secretName } = req.params; - const { - workspaceId, - environment, - type, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValue, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretComment, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - } = req.body; - - const secret = await SecretService.createSecret({ - secretName, - workspaceId: new Types.ObjectId(workspaceId), - environment, - type, - authData: req.authData, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValue, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretComment, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }); - - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId), - environment - }) - }); - - const secretWithoutBlindIndex = secret.toObject(); - delete secretWithoutBlindIndex.secretBlindIndex; - - return res.status(200).send({ - secret: secretWithoutBlindIndex - }); -} -======= const { secretName } = req.params; const { workspaceId, @@ -141,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({ @@ -164,7 +341,7 @@ export const createSecret = async (req: Request, res: Response) => { secret: secretWithoutBlindIndex, }); }; ->>>>>>> origin + /** * Update secret with name [secretName] @@ -214,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/secrets.ts b/backend/src/helpers/secrets.ts index 5f72b8f03b..d7cc31cdae 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -37,7 +37,7 @@ import { encryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8, } from '../utils/crypto'; -import { BotService, TelemetryService } from '../services'; +import { TelemetryService } from '../services'; import { getEncryptionKey, client, getRootEncryptionKey } from "../config"; import { EESecretService, EELogService } from "../ee/services"; import { @@ -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] @@ -244,23 +298,6 @@ export const generateSecretBlindIndexHelper = async ({ }); }; -// secretName, -// workspaceId, -// environment, -// type, -// authData, -// secretKeyCiphertext, -// secretKeyIV, -// secretKeyTag, -// secretValue, -// secretValueCiphertext, -// secretValueIV, -// secretValueTag, -// secretCommentCiphertext, -// secretCommentIV, -// secretCommentTag, -// folderId, - /** * Create secret with name [secretName] * @param {Object} obj @@ -280,11 +317,9 @@ export const createSecretHelper = async ({ secretKeyCiphertext, secretKeyIV, secretKeyTag, - secretValue, secretValueCiphertext, secretValueIV, secretValueTag, - secretComment, secretCommentCiphertext, secretCommentIV, secretCommentTag, @@ -340,52 +375,6 @@ export const createSecretHelper = async ({ }); } - // can generate secretKeyCiphertext etc. if not E2EE. - const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); - - if (!isWorkspaceE2EE) { - // if workspace is not end-to-end encrypted, then decrypt - // secret and return it in plaintext - - const key = await BotService.getWorkspaceKeyWithBot({ - workspaceId - }); - - if (secretName) { - const encryptedSecretKey = encryptSymmetric128BitHexKeyUTF8({ - plaintext: secretName, - key - }); - - secretKeyCiphertext = encryptedSecretKey.ciphertext; - secretKeyIV = encryptedSecretKey.iv; - secretKeyTag = encryptedSecretKey.tag; - } - - if (secretValue) { - const encryptedSecretValue = encryptSymmetric128BitHexKeyUTF8({ - plaintext: secretValue, - key - }); - - secretValueCiphertext = encryptedSecretValue.ciphertext; - secretValueIV = encryptedSecretValue.iv; - secretValueTag = encryptedSecretValue.tag; - } - - if (secretComment) { - const encryptedSecretComment = encryptSymmetric128BitHexKeyUTF8({ - plaintext: secretComment, - key - }); - - secretCommentCiphertext = encryptedSecretComment.ciphertext; - secretCommentIV = encryptedSecretComment.iv; - secretCommentTag = encryptedSecretComment.tag; - } - } - - // create secret const secret = await new Secret({ version: 1, @@ -565,44 +554,7 @@ export const getSecretsHelper = async ({ }); } - const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); - - if (!isWorkspaceE2EE) { - // if workspace is not end-to-end encrypted, then decrypt - // secret and return it in plaintext - - const key = await BotService.getWorkspaceKeyWithBot({ - workspaceId - }); - - return secrets.map((secret) => { - const secretName = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - key - }); - - const secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); - - return ({ - ...secret, - secretName, - secretValue - }); - }); - } - - return secrets.map((secret) => ({ - ...secret, - secretName: null, - secretValue: null - })); + return secrets; }; /** @@ -701,35 +653,7 @@ export const getSecretHelper = async ({ }); } - const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); - - if (!isWorkspaceE2EE) { - // if workspace is not end-to-end encrypted, then decrypt - // secret and return it in plaintext - - const key = await BotService.getWorkspaceKeyWithBot({ - workspaceId - }); - - const secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); - - return ({ - ...secret, - secretName, - secretValue - }); - } - - return ({ - ...secret, - secretName: null, - secretValue: null - }); + return secret; }; /** @@ -919,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" }); } @@ -938,7 +863,7 @@ export const deleteSecretHelper = async ({ workspaceId: new Types.ObjectId(workspaceId), environment, folder: folderId, - }); + }).lean(); secret = await Secret.findOneAndDelete({ secretBlindIndex, @@ -946,7 +871,7 @@ export const deleteSecretHelper = async ({ environment, type, folder: folderId, - }); + }).lean(); await Secret.deleteMany({ secretBlindIndex, @@ -962,7 +887,7 @@ export const deleteSecretHelper = async ({ environment, type, ...getAuthDataPayloadUserObj(authData), - }); + }).lean(); if (secret) { secrets = [secret]; @@ -983,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], @@ -1018,9 +941,9 @@ export const deleteSecretHelper = async ({ }, }); } - - return { + + return ({ secrets, - secret, - }; + secret + }); }; diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index 9fc26738c6..678c677117 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -7,14 +7,12 @@ export interface CreateSecretParams { environment: string; type: "shared" | "personal"; authData: AuthData; - secretKeyCiphertext?: string; - secretKeyIV?: string; - secretKeyTag?: string; - secretValue?: string; - secretValueCiphertext?: string; - secretValueIV?: string; - secretValueTag?: string; - secretComment?: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; secretCommentCiphertext?: string; secretCommentIV?: string; secretCommentTag?: string; diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index da517b998b..a4d65ffd29 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 = true }: { 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/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({ From 4616cffecd8d4e08c15d421fa469e738a6eb8cf4 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 12 Jun 2023 12:04:28 +0100 Subject: [PATCH 3/4] Add support for read/write non-e2ee secrets --- .../src/middleware/requireWorkspaceAuth.ts | 2 +- backend/src/routes/v3/workspaces.ts | 5 +- backend/src/services/BotService.ts | 15 +- .../api-reference/overview/authentication.mdx | 23 +- .../overview/encryption-modes/e2ee-mode.mdx | 861 ++++++++++++++++++ .../overview/encryption-modes/es-mode.mdx | 92 ++ .../overview/encryption-modes/overview.mdx | 57 ++ docs/api-reference/overview/introduction.mdx | 27 +- docs/mint.json | 12 +- frontend/src/hooks/api/workspace/index.tsx | 3 +- .../ProjectSettingsPage.tsx | 10 +- .../components/E2EESection/E2EESection.tsx | 113 +++ .../components/E2EESection/index.tsx | 3 + .../ProjectSettingsPage/components/index.tsx | 1 + 14 files changed, 1161 insertions(+), 63 deletions(-) create mode 100644 docs/api-reference/overview/encryption-modes/e2ee-mode.mdx create mode 100644 docs/api-reference/overview/encryption-modes/es-mode.mdx create mode 100644 docs/api-reference/overview/encryption-modes/overview.mdx create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/index.tsx diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index a4d65ffd29..9833e6e566 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -17,7 +17,7 @@ const requireWorkspaceAuth = ({ locationEnvironment = undefined, requiredPermissions = [], requireBlindIndicesEnabled = false, - requireE2EEOff = true + requireE2EEOff = false }: { acceptedRoles: Array<'admin' | 'member'>; locationWorkspaceId: req; 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 ab6d8fbd4d..7081a64cbf 100644 --- a/backend/src/services/BotService.ts +++ b/backend/src/services/BotService.ts @@ -7,17 +7,26 @@ import { getIsWorkspaceE2EEHelper } from '../helpers/bot'; -// rename the functions here -// refactor the interface situation here - /** * Class to handle bot actions */ 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 }: { 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 ![API key dashboard](../../images/api-key-dashboard.png) ![API key in personal settings](../../images/api-key-settings.png) - - -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. ![token add](../../images/project-token-add.png) - - -## 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 f4fab2ab36..a36c6f0b51 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..66037afe53 --- /dev/null +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from "react"; +import { + Checkbox +} from "@app/components/v2"; +import { + decryptAssymmetric, + encryptAssymmetric +} from "@app/components/utilities/cryptography/crypto"; +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'; From 553cf11ad25bb070d086672ceb941099aef035f3 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 12 Jun 2023 12:16:23 +0100 Subject: [PATCH 4/4] Fix lint issue --- .../components/E2EESection/E2EESection.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx index 66037afe53..4faf3e7637 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx @@ -1,11 +1,13 @@ import { useEffect, useState } from "react"; -import { - Checkbox -} from "@app/components/v2"; + 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'; @@ -101,7 +103,7 @@ export const E2EESection = ({ className="data-[state=checked]:bg-primary" id="autoCapitalization" isChecked={!bot.isActive} - onCheckedChange={async (state) => { + onCheckedChange={async () => { await toggleBotActivate(); }} >