Finish preliminary v3/secrets/raw endpoints

This commit is contained in:
Tuan Dang
2023-06-11 12:11:25 +01:00
parent facabc683b
commit 631eac803e
6 changed files with 475 additions and 217 deletions

View File

@@ -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,

View File

@@ -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
});
};

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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
);

View File

@@ -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({