Enable all auth clients for secrets v3, remove serviceTokenData .populate in middleware, make secret versions and rollbacks compatible with blind indexing

This commit is contained in:
Tuan Dang
2023-04-19 15:38:13 +03:00
parent acb90ee0f7
commit ad5852fe3a
21 changed files with 313 additions and 133 deletions

View File

@@ -8,6 +8,8 @@ import { BadRequestError, InternalServerError, UnauthorizedRequestError, Validat
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { TelemetryService } from '../../services';
import { User } from "../../models";
import { AccountNotFoundError } from '../../utils/errors';
/**
* Create secret for workspace with id [workspaceId] and environment [environment]
@@ -340,15 +342,18 @@ export const getSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
let userEmail: string | undefined = undefined // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
userId = req.serviceTokenData.user;
const user = await User.findById(req.serviceTokenData.user, 'email');
if (!user) throw AccountNotFoundError();
userEmail = user.email;
}
const [err, secrets] = await to(Secret.find(

View File

@@ -374,8 +374,14 @@ export const createSecrets = async (req: Request, res: Response) => {
listOfSecretsToCreate = [req.body.secrets];
}
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
type secretsToCreateType = {
type: string;
secretName?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
@@ -388,25 +394,10 @@ export const createSecrets = async (req: Request, res: Response) => {
tags: string[]
}
const secretsToInsert: ISecret[] = listOfSecretsToCreate.map(({
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}: secretsToCreateType) => {
return ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
const secretsToInsert: ISecret[] = await Promise.all(
listOfSecretsToCreate.map(async ({
type,
user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined,
environment,
secretName,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -417,11 +408,38 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentIV,
secretCommentTag,
tags
});
});
}: secretsToCreateType) => {
let secretBlindIndex;
if (secretName) {
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName,
salt
});
}
return ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
type,
...(secretBlindIndex ? { secretBlindIndex } : {}),
user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
});
})
);
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject());
setTimeout(async () => {
// trigger event - push secrets
await EventService.handleEvent({
@@ -440,6 +458,7 @@ export const createSecrets = async (req: Request, res: Response) => {
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -453,6 +472,7 @@ export const createSecrets = async (req: Request, res: Response) => {
type,
user,
environment,
secretBlindIndex,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
@@ -492,7 +512,7 @@ export const createSecrets = async (req: Request, res: Response) => {
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: TelemetryService.getDistinctId({
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
properties: {
@@ -607,7 +627,7 @@ export const getSecrets = async (req: Request, res: Response) => {
// case: client authorization is via service token
if (req.serviceTokenData) {
const userId = req.serviceTokenData.user._id
const userId = req.serviceTokenData.user;
const secretQuery: any = {
workspace: workspaceId,
@@ -667,7 +687,7 @@ export const getSecrets = async (req: Request, res: Response) => {
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
distinctId: TelemetryService.getDistinctId({
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
properties: {
@@ -889,7 +909,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: TelemetryService.getDistinctId({
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
properties: {
@@ -1023,7 +1043,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: TelemetryService.getDistinctId({
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
properties: {

View File

@@ -11,9 +11,11 @@ import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissi
import {
PERMISSION_READ_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN
} from '../../variables';
import { getSaltRounds } from '../../config';
import { BadRequestError } from '../../utils/errors';
/**
* Return service token data associated with service token on request
@@ -48,7 +50,15 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
}
*/
return res.status(200).json(req.serviceTokenData);
if (!(req.authData.authPayload instanceof ServiceTokenData)) throw BadRequestError({
message: 'Failed accepted client validation for service token data'
});
const serviceTokenData = await ServiceTokenData
.findById(req.authData.authPayload._id)
.populate('user');
return res.status(200).json(serviceTokenData);
}
/**

View File

@@ -132,7 +132,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
if (req.user) {
userId = req.user._id.toString();
} else if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userId = req.serviceTokenData.user.toString();
}
// validate environment
const workspaceEnvs = req.membership.workspace.environments;

View File

@@ -70,7 +70,10 @@ export const createSecret = async (req: Request, res: Response) => {
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} = req.body;
const secret = await SecretService.createSecret({
@@ -84,7 +87,12 @@ export const createSecret = async (req: Request, res: Response) => {
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
...((secretCommentCiphertext && secretCommentIV && secretCommentTag) ? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} : {})
});
await EventService.handleEvent({

View File

@@ -146,7 +146,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
const oldSecretVersion = await SecretVersion.findOne({
secret: secretId,
version
});
}).select('+secretBlindIndex')
if (!oldSecretVersion) throw new Error('Failed to find secret version');
@@ -155,6 +155,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -174,6 +175,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
type,
user,
environment,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -197,6 +199,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
user,
environment,
isDeleted: false,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,

View File

@@ -173,6 +173,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
}
}
*/
let secrets;
try {
const { workspaceId } = req.params;
@@ -182,7 +183,10 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
version
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
}).populate<{ secretVersions: ISecretVersion[]}>({
path: 'secretVersions',
select: '+secretBlindIndex'
});
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
@@ -259,7 +263,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
);
// add secret versions
await SecretVersion.insertMany(
const secretV = await SecretVersion.insertMany(
secrets.map(({
_id,
version,

View File

@@ -157,7 +157,7 @@ const getAuthSTDPayload = async ({
}, {
new: true
})
.select('+encryptedKey +iv +tag').populate('user serviceAccount');
.select('+encryptedKey +iv +tag');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });

View File

@@ -1,5 +1,6 @@
import mongoose from 'mongoose';
import { EESecretService } from '../ee/services';
import { SecretService } from '../services';
import { getLogger } from '../utils/logger';
/**
@@ -22,6 +23,7 @@ const initDatabaseHelper = async ({
getLogger("database").info("Database connection established");
await EESecretService.initSecretVersioning();
await SecretService.initSecretBlindIndexDataHelper();
} catch (err) {
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
}

View File

@@ -11,6 +11,7 @@ import {
} from '../interfaces/middleware';
import {
User,
Workspace,
ServiceAccount,
ServiceTokenData,
Secret,
@@ -26,7 +27,8 @@ import {
validateUserClientForSecrets
} from '../helpers/user';
import {
validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace
validateServiceTokenDataClientForSecrets,
validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
import {
validateServiceAccountClientForSecrets,
@@ -63,7 +65,8 @@ import {
EELogService
} from '../ee/services';
import {
getAuthDataPayloadIdObj
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj
} from '../utils/auth';
/**
@@ -217,6 +220,46 @@ const validateClientForSecrets = async ({
});
}
/**
* Initialize secret blind index data by setting previously
* un-initialized projects to have secret blind index data
* (Ensures that all projects have associated blind index data)
*/
const initSecretBlindIndexDataHelper = async () => {
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace');
const workspaceIdsToBlindIndex = await Workspace.distinct('_id', {
_id: {
$nin: workspaceIdsBlindIndexed
}
});
const secretBlindIndexDataToInsert = workspaceIdsToBlindIndex.map((workspaceToBlindIndex) => {
const salt = crypto.randomBytes(16).toString('base64');
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric({
plaintext: salt,
key: getEncryptionKey()
});
const secretBlindIndexData = new SecretBlindIndexData({
workspace: workspaceToBlindIndex,
encryptedSaltCiphertext,
saltIV,
saltTag
})
return secretBlindIndexData;
});
if (secretBlindIndexDataToInsert.length > 0) {
await SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert);
}
}
/**
* Create secret blind index data containing encrypted blind index [salt]
* for workspace with id [workspaceId]
@@ -283,7 +326,7 @@ const getSecretBlindIndexSaltHelper = async ({
* 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.secretName - name of secret to generate blind index for
* @param {String} obj.salt - base64-salt
*/
const generateSecretBlindIndexWithSaltHelper = async ({
@@ -312,8 +355,8 @@ const getSecretBlindIndexSaltHelper = async ({
* Generate blind index for secret with name [secretName]
* for workspace with id [workspaceId]
* @param {Object} obj
* @param {Object} obj.secretName - name of secret to generate blind index for
* @param {Object} obj.workspaceId - id of workspace that secret belongs to
* @param {Stringj} obj.secretName - name of secret to generate blind index for
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/
const generateSecretBlindIndexHelper = async ({
secretName,
@@ -367,11 +410,11 @@ const createSecretHelper = async ({
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}: CreateSecretParams) => {
// preliminary OK
// pending check for other types of clients
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
@@ -380,7 +423,8 @@ const createSecretHelper = async ({
const exists = await Secret.exists({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
type
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
});
if (exists) throw BadRequestError({
@@ -400,12 +444,6 @@ const createSecretHelper = async ({
if (!exists) throw BadRequestError({
message: 'Failed to create personal secret override for no corresponding shared secret'
});
// TODO: adapt to other client types
if (!(authData.authPayload instanceof User)) throw BadRequestError({
message: 'Failed to create personal secret override for no specified user'
});
}
// create secret
@@ -414,16 +452,17 @@ const createSecretHelper = async ({
workspace: new Types.ObjectId(workspaceId),
environment,
type,
...(type === SECRET_PERSONAL && (authData.authPayload instanceof User) ? {
user: authData.authPayload._id
} : {}),
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}).save();
const secretVersion = new SecretVersion({
@@ -431,11 +470,10 @@ const createSecretHelper = async ({
version: secret.version,
workspace: secret.workspace,
type,
...(type === SECRET_PERSONAL && authData.authPayload instanceof User ? {
user: authData.authPayload._id
} : {}),
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -475,7 +513,7 @@ const createSecretHelper = async ({
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: TelemetryService.getDistinctId({
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
@@ -504,21 +542,17 @@ const getSecretsHelper = async ({
environment,
authData
}: GetSecretsParams) => {
// preliminary OK
// pending check for other types of clients
let secrets: ISecret[] = [];
if (authData.authPayload instanceof User) {
// case: get personal secrets first
secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_PERSONAL,
user: authData.authPayload._id
});
}
// get personal secrets first
secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData)
});
// concat with shared secrets
secrets = secrets.concat(await Secret.find({
workspace: new Types.ObjectId(workspaceId),
environment,
@@ -549,7 +583,7 @@ const getSecretsHelper = async ({
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
distinctId: TelemetryService.getDistinctId({
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
@@ -582,38 +616,31 @@ const getSecretHelper = async ({
type,
authData
}: GetSecretParams) => {
// preliminary OK
// pending check for other types of clients
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
let secret;
let secret: ISecret | null = null;
if (authData.authPayload instanceof User) {
// case: find any personal secret matching criteria
// try getting personal secret first (if exists)
secret = await Secret.findOne({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type: type ?? SECRET_PERSONAL,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
});
if (!secret) {
// case: failed to find personal secret matching criteria
// -> find shared secret matching criteria
secret = await Secret.findOne({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type: type ?? SECRET_PERSONAL,
...(type === SECRET_PERSONAL ? {
user: authData.authPayload._id
} : {})
type: SECRET_SHARED
});
if (!secret) {
// case: failed to find personal secret matching criteria
// -> find shared secret matching criteria
secret = await Secret.findOne({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_SHARED
});
}
}
if (!secret) throw SecretNotFoundError();
@@ -639,7 +666,7 @@ const getSecretHelper = async ({
if (postHogClient) {
postHogClient.capture({
event: 'secrets pull',
distinctId: TelemetryService.getDistinctId({
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
@@ -678,8 +705,6 @@ const updateSecretHelper = async ({
secretValueIV,
secretValueTag
}: UpdateSecretParams) => {
// preliminary OK
// pending check for other types of clients
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
@@ -688,6 +713,7 @@ const updateSecretHelper = async ({
let secret: ISecret | null = null;
if (type === SECRET_SHARED) {
// case: update shared secret
secret = await Secret.findOneAndUpdate(
{
secretBlindIndex,
@@ -705,14 +731,15 @@ const updateSecretHelper = async ({
new: true
}
);
} else if (type === SECRET_PERSONAL && authData.authPayload instanceof User) {
} else {
// case: update personal secret
secret = await Secret.findOneAndUpdate(
{
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type,
user: authData.authPayload._id
...getAuthDataPayloadUserObj(authData)
},
{
secretValueCiphertext,
@@ -733,11 +760,10 @@ const updateSecretHelper = async ({
version: secret.version,
workspace: secret.workspace,
type,
...(type === SECRET_PERSONAL && authData.authPayload instanceof User ? {
user: authData.authPayload._id
} : {}),
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
@@ -777,7 +803,7 @@ const updateSecretHelper = async ({
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: TelemetryService.getDistinctId({
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
@@ -810,9 +836,6 @@ const deleteSecretHelper = async ({
type,
authData
}: DeleteSecretParams) => {
// preliminary OK
// pending check for other types of clients
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
@@ -840,13 +863,13 @@ const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId),
environment
});
} else if (type === SECRET_PERSONAL && authData.authPayload instanceof User) {
} else {
secret = await Secret.findOneAndDelete({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
user: authData.authPayload._id
...getAuthDataPayloadUserObj(authData)
});
if (secret) {
@@ -887,7 +910,7 @@ const deleteSecretHelper = async ({
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: TelemetryService.getDistinctId({
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
@@ -909,6 +932,7 @@ const deleteSecretHelper = async ({
export {
validateClientForSecret,
validateClientForSecrets,
initSecretBlindIndexDataHelper,
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,

View File

@@ -111,7 +111,6 @@ const validateClientForServiceTokenData = async ({
environment?: string;
requiredPermissions?: string[];
}) => {
if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({

View File

@@ -13,6 +13,9 @@ export interface CreateSecretParams {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
}
export interface GetSecretsParams {

View File

@@ -33,7 +33,8 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
},
user: {
type: Schema.Types.ObjectId,
ref: 'User'
ref: 'User',
required: true
},
serviceAccount: {
type: Schema.Types.ObjectId,

View File

@@ -10,6 +10,8 @@ import { secretsController } from '../../controllers/v3';
import {
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
ADMIN,
MEMBER,
PERMISSION_WRITE_SECRETS,
@@ -25,7 +27,12 @@ router.get(
query('tagSlugs'),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -48,9 +55,17 @@ router.post(
body('secretValueCiphertext').exists().isString().trim(),
body('secretValueIV').exists().isString().trim(),
body('secretValueTag').exists().isString().trim(),
body('secretCommentCiphertext').optional().isString().trim(),
body('secretCommentIV').optional().isString().trim(),
body('secretCommentTag').optional().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -70,7 +85,12 @@ router.get(
query('type').optional().isIn([SECRET_SHARED, SECRET_PERSONAL]),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -93,7 +113,12 @@ router.patch(
body('secretValueTag').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -113,7 +138,12 @@ router.delete(
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],

View File

@@ -11,6 +11,7 @@ import {
DeleteSecretParams
} from '../interfaces/services/SecretService';
import {
initSecretBlindIndexDataHelper,
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
@@ -23,6 +24,17 @@ import {
} from '../helpers/secrets';
class SecretService {
/**
*
* @param param0 h
* @returns
*/
static async initSecretBlindIndexDataHelper() {
return await initSecretBlindIndexDataHelper();
}
/**
* Create secret blind index data containing encrypted blind index salt
* for workspace with id [workspaceId]

View File

@@ -16,6 +16,7 @@ import {
ServiceTokenData
} from '../models';
import {
AccountNotFoundError,
BadRequestError
} from '../utils/errors';
@@ -50,20 +51,23 @@ class Telemetry {
return postHogClient;
}
static getDistinctId ({
static getDistinctId = async ({
authData
}: {
authData: AuthData;
}) {
let distinctId: any = '';
}) => {
let distinctId = '';
if (authData.authPayload instanceof User) {
distinctId = authData.authPayload.email;
} else if (authData.authPayload instanceof ServiceAccount) {
distinctId = `sa.${authData.authPayload._id.toString()}`;
} else if (authData.authPayload instanceof ServiceTokenData) {
if (authData.authPayload.user instanceof User) {
distinctId = authData.authPayload?.user.email;
} else if (authData.authPayload?.serviceAccount) {
if (authData.authPayload.user) {
const user = await User.findById(authData.authPayload.user, 'email');
if (!user) throw AccountNotFoundError();
distinctId = user.email;
} else if (authData.authPayload.serviceAccount) {
distinctId = distinctId = `sa.${authData.authPayload.serviceAccount.toString()}`;
}
}

View File

@@ -2,9 +2,17 @@ import { AuthData } from '../interfaces/middleware';
import {
User,
ServiceAccount,
ServiceTokenData
ServiceTokenData,
ServiceToken
} from '../models';
// TODO: find a more optimal folder structure to store these types of functions
/**
* Returns an object containing the id of the authentication data payload
* @param {AuthData} authData - authentication data object
* @returns
*/
const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { userId: authData.authPayload._id };
@@ -17,10 +25,30 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
return {};
};
/**
* Returns an object containing the user associated with the authentication data payload
* @param {AuthData} authData - authentication data object
* @returns
*/
const getAuthDataPayloadUserObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
}
}
export {
getAuthDataPayloadIdObj
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj
}

View File

@@ -21,21 +21,30 @@ Infisical makes a usability-security tradeoff that is to give users convenient a
## Secrets
The `Secret` model includes the fields `workspace`, `type`, `user`, `environment`, `secretKeyCiphertext`, `secretKeyIV`, `secretKeyTag`, `secretValueCiphertext`, `secretValueIV`, and `secretValueTag`.
The `Secret` model includes the fields `workspace`, `type`, `user`, `environment`, `secretBlindIndex`, `secretKeyCiphertext`, `secretKeyIV`, `secretKeyTag`, `secretValueCiphertext`, `secretValueIV`, and `secretValueTag`.
Each secret is symmetrically encrypted by the key of the project that it belongs to; that key's encrypted copies are stored in a separate `Key` collection.
Each secret consists of a key name and value pair and is symmetrically encrypted by the key of the project that it belongs to; that key's encrypted copies are stored in a separate `Key` collection.
The `secretBlindIndex` enables users to query secrets by their names; it is a blind index computed by applying `argon2id` with a 128-bit random salt (unique to each project) and the name of the secret. The salt itself is symmetrically encrypted under the server key and stored in the `SecretBlindIndexData` collection.
## Blind Index Data
The `SecretBlindIndexData` model includes the fields `workspace`, `encryptedSaltCiphertext`, `saltIV`, and `saltTag`.
Infisical stores salts (unique to each project) symmetrically encrypted under the server key. The salts are used to compute blind indices for secrets that enable
users to query secrets by name.
## Project Keys
The `Key` model includes the fields `encryptedKey`, `nonce`, `sender`, `receiver`, and `workspace`.
Infisical stores copies of project keys, one for each member of a project, encrypted under each member's public key.
Infisical stores copies of project keys, one for each member of a project, asymmetrically encrypted under each member's public key.
## Bots
The `Bot` model contains the fields `name`, `workspace`, `isActive`, `publicKey`, `encryptedPrivateKey`, `iv`, and `tag`.
Each project comes with a bot that has its own public-private key pair; its private key is encrypted by the server's symmetric key. If needed, a user can opt-in to share their project key with the bot (i.e. Infisical) to give the platform access to the project's secrets.
Each project comes with a bot that has its own public-private key pair; its private key is symmetrically encrypted by the server's key. If needed, a user can opt-in to share their project key with the bot (i.e. Infisical) to give the platform access to the project's secrets.
<Note>
Sharing secrets with Infisical so they can be synced to integrations like

View File

@@ -27,3 +27,20 @@ After signing up, a user can invite other users to their organization to partake
To push secrets, a sender randomly-generates a symmetric encryption key, uses that key to encrypt their secret keys and values separately, asymmetrically encrypts the key with the receivers’ public keys, and uploads the encrypted secrets and keys to the server.
To pull secrets, a receiver obtains encrypted secret keys and values and their encrypted copy of the project key to decrypt the secrets from the server — they asymmetrically decrypt the key using their private key and use the decrypted key to decrypt the secrets. This public-key mechanism prevents the server-side from reading any secrets.
When dealing with individual secrets (e.g. pulling one secret by name) or creating new secrets, a user passes the name of the secret to the server which is then converted to a blind index by applying `argon2id` with the name of the secret and a 128-bit random salt unique to each project; the salt itself is encrypted by the server key and stored in the database.
<Info>
Infisical ensures that the name of any secret is never stored in plaintext and instead only a blind index generated from the name. It is infeasible to reverse back a blind index to the name of a secret without knowledge of the server key.
</Info>
## Bot
To use some features like integrations, users must opt out of E2EE (this means sharing access to secrets with Infisical).
Infisical employs the concept of a bot which is a cryptographic abstraction for how Infisical interacts with secrets when users opt out of E2EE. In this model, each project is assigned a bot with its own public-private key pair where each bot's private key is stored symmetrically encrypted under the server key. When a user opts out of E2EE, they share the project key with the bot by encrypting a copy of it under the public key of the bot.
When users wish to sync secrets from a project and environment Infisical to other platform integrations like Vercel or GitHub, Infisical decrypts the intended secrets and uses the integration platform's APIs to send secrets over. It should be noted that opting out of E2EE is optional and it is entirely possible to use Infisical to manage secrets across your team and infrastructure without opting out of E2EE.

View File

@@ -5,7 +5,7 @@ description: "Infisical's security statement."
## Summary
Infisical uses end-to-end encryption (E2EE) whenever possible to securely store and share secrets. It uses secure remote password (SRP) to handle authentication and public-key cryptography for secret sharing and syncing; secrets are symmetrically encrypted at rest by keys decryptable only by members of the project.
Infisical uses end-to-end encryption (E2EE) whenever possible to securely store and share secret values. It uses secure remote password (SRP) to handle authentication and public-key cryptography for secret sharing and syncing; secrets are symmetrically encrypted by keys decryptable only by members of the project.
Infisical uses AES256-GCM for symmetric encryption and x25519-xsalsa20-poly1305 for asymmetric encryption operations mentioned in this brief; key generation and asymmetric algorithms are implemented with the [TweetNaCl.js](https://tweetnacl.js.org/#/) library which has been well-audited and recommended for use by cybersecurity firm Cure53. Lastly, the secure remote password (SRP) implementation uses [jsrp](https://github.com/alax/jsrp) package for user authentication. As part of our commitment to user privacy and security, we aim to conduct formal security and compliance audits in the following year.

View File

@@ -93,6 +93,7 @@ const initProjectHelper = async ({
}) => {
let project;
try {
// create new project
project = await createWorkspace({
workspaceName: projectName,