diff --git a/backend/src/controllers/v3/index.ts b/backend/src/controllers/v3/index.ts index dba8067f7..1cbb36595 100644 --- a/backend/src/controllers/v3/index.ts +++ b/backend/src/controllers/v3/index.ts @@ -1,5 +1,7 @@ import * as secretsController from './secretsController'; +import * as workspacesController from './workspacesController'; export { - secretsController + secretsController, + workspacesController } \ No newline at end of file diff --git a/backend/src/controllers/v3/workspacesController.ts b/backend/src/controllers/v3/workspacesController.ts new file mode 100644 index 000000000..61079e6b4 --- /dev/null +++ b/backend/src/controllers/v3/workspacesController.ts @@ -0,0 +1,94 @@ +import { Request, Response } from 'express'; +import { Types } from 'mongoose'; +import { Secret, Workspace, SecretBlindIndexData } from '../../models'; +import { SecretService } from'../../services'; +import { BadRequestError } from '../../utils/errors'; +import { decryptSymmetric } from '../../utils/crypto'; +import { getEncryptionKey } from '../../config'; +import * as argon2 from 'argon2'; + +/** + * Return whether or not all secrets in workspace with id [workspaceId] + * are blind-indexed + * @param req + * @param res + * @returns + */ +export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => { + const { workspaceId } = req.params; + + const secretsWithoutBlindIndex = await Secret.countDocuments({ + workspace: new Types.ObjectId(workspaceId) + }); + + const isBlindIndexed = secretsWithoutBlindIndex === 0; + + return res.status(200).send(isBlindIndexed); +} + +/** + * Get all secrets for workspace with id [workspaceId] + */ +export const getWorkspaceSecrets = async (req: Request, res: Response) => { + const { workspaceId } = req.params; + + const secrets = await Secret.find({ + workspace: new Types.ObjectId (workspaceId) + }); + + return res.status(200).send({ + secrets + }); +} + +/** + * Update blind indices for secrets in workspace with id [workspaceId] + * @param req + * @param res + */ +export const nameWorkspaceSecrets = async (req: Request, res: Response) => { + + interface SecretToUpdate { + secretName: string; + _id: string; + } + + const { workspaceId } = req.params; + const { + secretsToUpdate + }: { + secretsToUpdate: SecretToUpdate[]; + } = req.body; + + // get secret blind index salt + const salt = await SecretService.getSecretBlindIndexSalt({ + workspaceId: new Types.ObjectId(workspaceId) + }); + + // update secret blind indices + const operations = await Promise.all( + secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => { + const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ + secretName: secretToUpdate.secretName, + salt + }); + + return ({ + updateOne: { + filter: { + _id: new Types.ObjectId(secretToUpdate._id) + }, + update: { + secretBlindIndex + } + } + }); + }) + ); + + await Secret.bulkWrite(operations); + + return res.status(200).send({ + operations + }); +} \ No newline at end of file diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index f900fb70b..0eb39da2d 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -218,7 +218,7 @@ const validateClientForSecrets = async ({ } /** - * Create secret blind index data containing encrypted blind index salt + * Create secret blind index data containing encrypted blind index [salt] * for workspace with id [workspaceId] * @param {Object} obj * @param {Types.ObjectId} obj.workspaceId @@ -250,6 +250,64 @@ const createSecretBlindIndexDataHelper = async ({ return secretBlindIndexData; } +/** + * Get secret blind index salt for workspace with id [workspaceId] + * @param {Object} obj + * @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for + * @returns + */ +const getSecretBlindIndexSaltHelper = async ({ + workspaceId +}: { + workspaceId: Types.ObjectId; +}) => { + // check if workspace blind index data exists + const secretBlindIndexData = await SecretBlindIndexData.findOne({ + workspace: workspaceId + }); + + if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); + + // decrypt workspace salt + const salt = decryptSymmetric({ + ciphertext: secretBlindIndexData.encryptedSaltCiphertext, + iv: secretBlindIndexData.saltIV, + tag: secretBlindIndexData.saltTag, + key: getEncryptionKey() + }); + + return salt; +} + +/** + * Generate blind index for secret with name [secretName] + * and salt [salt] + * @param {Object} obj + * @param {Object} obj.secretName - name of secret to generate blind index for + * @param {String} obj.salt - base64-salt + */ + const generateSecretBlindIndexWithSaltHelper = async ({ + secretName, + salt +}: { + secretName: string; + salt: string; +}) => { + + // generate secret blind index + const secretBlindIndex = (await argon2.hash(secretName, { + type: argon2.argon2id, + salt: Buffer.from(salt, 'base64'), + saltLength: 16, // default 16 bytes + memoryCost: 65536, // default pool of 64 MiB per thread. + hashLength: 32, + parallelism: 1, + raw: true + })).toString('base64'); + + return secretBlindIndex; +} + /** * Generate blind index for secret with name [secretName] * for workspace with id [workspaceId] @@ -280,16 +338,10 @@ const generateSecretBlindIndexHelper = async ({ key: getEncryptionKey() }); - // generate secret blind index - const secretBlindIndex = (await argon2.hash(secretName, { - type: argon2.argon2id, - salt: Buffer.from(salt, 'base64'), - saltLength: 16, // default 16 bytes - memoryCost: 65536, // default pool of 64 MiB per thread. - hashLength: 32, - parallelism: 1, - raw: true - })).toString('base64'); + const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ + secretName, + salt + }); return secretBlindIndex; } @@ -858,6 +910,8 @@ export { validateClientForSecret, validateClientForSecrets, createSecretBlindIndexDataHelper, + getSecretBlindIndexSaltHelper, + generateSecretBlindIndexWithSaltHelper, generateSecretBlindIndexHelper, createSecretHelper, getSecretsHelper, diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index e6be64f8c..bb002176b 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -67,12 +67,16 @@ const validateClientForWorkspace = async ({ message: 'Failed to find workspace' }); - if (requireBlindIndicesEnabled && !workspace.isBlindedIndicesEnabled) { + if (requireBlindIndicesEnabled) { // case: blind indices are not enabled for secrets in this workspace // (i.e. workspace was created before blind indices were introduced // and no admin has enabled it) - throw UnauthorizedRequestError({ + const secretBlindIndexData = await SecretBlindIndexData.exists({ + workspace: new Types.ObjectId(workspaceId) + }); + + if (!secretBlindIndexData) throw UnauthorizedRequestError({ message: 'Failed workspace authorization due to blind indices not being enabled' }); } @@ -148,8 +152,7 @@ const createWorkspace = async ({ workspace = await new Workspace({ name, organization: organizationId, - autoCapitalization: true, - isBlindedIndicesEnabled: true + autoCapitalization: true }).save(); // initialize bot for workspace diff --git a/backend/src/index.ts b/backend/src/index.ts index dcf66ffd6..022031d7c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -62,7 +62,8 @@ import { tags as v2TagsRouter, } from './routes/v2'; import { - secrets as v3SecretsRouter + secrets as v3SecretsRouter, + workspaces as v3WorkspacesRouter } from './routes/v3'; import { healthCheck } from './routes/status'; import { getLogger } from './utils/logger'; @@ -159,6 +160,7 @@ const main = async () => { // v3 routes (experimental) app.use('/api/v3/secrets', v3SecretsRouter); + app.use('/api/v3/workspaces', v3WorkspacesRouter); // api docs app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) diff --git a/backend/src/models/secretBlindIndexData.ts b/backend/src/models/secretBlindIndexData.ts index aeaa3f027..47649dde4 100644 --- a/backend/src/models/secretBlindIndexData.ts +++ b/backend/src/models/secretBlindIndexData.ts @@ -1,6 +1,6 @@ import { Schema, model, Types, Document } from 'mongoose'; -export interface ISecretBlindIndexData { +export interface ISecretBlindIndexData extends Document { _id: Types.ObjectId; workspace: Types.ObjectId; encryptedSaltCiphertext: string; diff --git a/backend/src/models/workspace.ts b/backend/src/models/workspace.ts index c31542b3a..94a086e82 100644 --- a/backend/src/models/workspace.ts +++ b/backend/src/models/workspace.ts @@ -9,7 +9,6 @@ export interface IWorkspace { slug: string; }>; autoCapitalization: boolean; - isBlindedIndicesEnabled: boolean; } const workspaceSchema = new Schema({ @@ -21,10 +20,6 @@ const workspaceSchema = new Schema({ type: Boolean, default: true, }, - isBlindedIndicesEnabled: { - type: Boolean, - required: true - }, organization: { type: Schema.Types.ObjectId, ref: 'Organization', diff --git a/backend/src/routes/v3/index.ts b/backend/src/routes/v3/index.ts index fffd60b49..e66723133 100644 --- a/backend/src/routes/v3/index.ts +++ b/backend/src/routes/v3/index.ts @@ -1,5 +1,7 @@ import secrets from './secrets'; +import workspaces from './workspaces'; export { - secrets + secrets, + workspaces } \ No newline at end of file diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index f40c0f332..fe346b0de 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -23,6 +23,7 @@ router.get( query('workspaceId').exists().isString().trim(), query('environment').exists().isString().trim(), query('tagSlugs'), + validateRequest, requireAuth({ acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), diff --git a/backend/src/routes/v3/workspaces.ts b/backend/src/routes/v3/workspaces.ts new file mode 100644 index 000000000..ad02e9a4c --- /dev/null +++ b/backend/src/routes/v3/workspaces.ts @@ -0,0 +1,80 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + validateRequest +} from '../../middleware'; +import { workspacesController } from '../../controllers/v3'; +import { + AUTH_MODE_JWT, + ADMIN, + PERMISSION_READ_SECRETS +} from '../../variables'; +import { param, body, validationResult } from 'express-validator'; + +// -- migration to blind indices endpoints + +router.get( + '/:workspaceId/secrets/blind-index-status', + param('workspaceId').exists().isString().trim(), + validateRequest, + // requireAuth({ + // acceptedAuthModes: [AUTH_MODE_JWT] + // }), + // requireWorkspaceAuth({ + // acceptedRoles: [ADMIN], + // locationWorkspaceId: 'params', + // }), + workspacesController.getWorkspaceBlindIndexStatus +); + +router.get( // allow admins to get all workspace secrets (part of blind indices migration) + '/:workspaceId/secrets', + param('workspaceId').exists().isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN], + locationWorkspaceId: 'params', + }), + workspacesController.getWorkspaceSecrets +); + +router.post( // allow admins to name all workspace secrets (part of blind indices migration) + '/:workspaceId/secrets/names', + param('workspaceId').exists().isString().trim(), + body('secretsToUpdate') + .exists() + .isArray() + .withMessage('secretsToUpdate must be an array') + .customSanitizer((value) => { + return value.map((secret: any) => ({ + secretName: secret.name, + _id: secret._id + })); + }), + body('secretsToUpdate.*.secretName') + .exists() + .isString() + .withMessage('secretName must be a string'), + body('secretsToUpdate.*._id') + .exists() + .isString() + .withMessage('secretId must be a string'), + validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN], + locationWorkspaceId: 'params' + }), + workspacesController.nameWorkspaceSecrets +); + +// -- + +export default router; \ No newline at end of file diff --git a/backend/src/services/SecretService.ts b/backend/src/services/SecretService.ts index 3682c0543..6d0ec0e07 100644 --- a/backend/src/services/SecretService.ts +++ b/backend/src/services/SecretService.ts @@ -12,6 +12,8 @@ import { } from '../interfaces/services/SecretService'; import { createSecretBlindIndexDataHelper, + getSecretBlindIndexSaltHelper, + generateSecretBlindIndexWithSaltHelper, generateSecretBlindIndexHelper, createSecretHelper, getSecretsHelper, @@ -25,10 +27,11 @@ class SecretService { * Create secret blind index data containing encrypted blind index salt * for workspace with id [workspaceId] * @param {Object} obj + * @param {Buffer} obj.salt - 16-byte random salt * @param {Types.ObjectId} obj.workspaceId */ static async createSecretBlindIndexData({ - workspaceId + workspaceId, }: { workspaceId: Types.ObjectId; }) { @@ -37,6 +40,42 @@ class SecretService { }); } + /** + * Get secret blind index salt for workspace with id [workspaceId] + * @param {Object} obj + * @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for + * @returns + */ + static async getSecretBlindIndexSalt({ + workspaceId + }: { + workspaceId: Types.ObjectId; + }) { + return await getSecretBlindIndexSaltHelper({ + workspaceId + }); + } + + /** + * Generate blind index for secret with name [secretName] + * and salt [salt] + * @param {Object} obj + * @param {Object} obj.secretName - name of secret to generate blind index for + * @param {String} obj.salt - base64-salt + */ + static async generateSecretBlindIndexWithSalt({ + secretName, + salt + }: { + secretName: string; + salt: string; + }) { + return await generateSecretBlindIndexWithSaltHelper({ + secretName, + salt + }); + } + /** * Create and return blind index for secret with * name [secretName] part of workspace with id [workspaceId]