Changed frontend to use the new secrets routes

This commit is contained in:
Vladyslav Matsiiako
2023-01-09 13:14:07 -08:00
parent b6189a90f4
commit 486aa139c2
23 changed files with 648 additions and 390 deletions

View File

@ -273,7 +273,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
});
await Secret.bulkWrite(ops);
let newSecretsObj: { [key: string]: PatchSecret } = {};
const newSecretsObj: { [key: string]: PatchSecret } = {};
req.body.secrets.forEach((secret: PatchSecret) => {
newSecretsObj[secret.id] = secret;
});
@ -323,7 +323,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
// group secrets into workspaces so updated secrets can
// be logged and snapshotted separately for each workspace
let workspaceSecretObj: any = {};
const workspaceSecretObj: any = {};
req.secrets.forEach((s: any) => {
if (s.workspace.toString() in workspaceSecretObj) {
workspaceSecretObj[s.workspace.toString()].push(s);
@ -399,7 +399,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
// group secrets into workspaces so deleted secrets can
// be logged and snapshotted separately for each workspace
let workspaceSecretObj: any = {};
const workspaceSecretObj: any = {};
req.secrets.forEach((s: any) => {
if (s.workspace.toString() in workspaceSecretObj) {
workspaceSecretObj[s.workspace.toString()].push(s);
@ -445,7 +445,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}
});
return res.status(400).send({
return res.status(200).send({
secrets: req.secrets
});
}

View File

@ -138,7 +138,7 @@ router.patch(
router.delete(
'/',
[
check('secretIds')
body('secretIds')
.exists()
.custom((value) => {
// case: delete 1 secret

View File

@ -7,12 +7,12 @@ import {
} from '../config';
import { getLogger } from '../utils/logger';
if(TELEMETRY_ENABLED){
if(!TELEMETRY_ENABLED){
getLogger("backend-main").info([
"",
"Infisical collects telemetry data about general usage.",
"The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.",
"To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables",
"To improve, Infisical collects telemetry data about general usage.",
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
].join('\n'))
}

View File

@ -34,7 +34,7 @@ interface ToggleProps {
* @param {string} obj.value - value of a certain secret
* @param {number} obj.pos - position of a certain secret
#TODO: make the secret id persistent?
* @param {string} obj.id - id of a certain secret
* @param {string} obj.id - id of a certain secret (NOTE: THIS IS THE ID OF THE MAIN SECRET - NOT OF AN OVERRIDE)
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually

View File

@ -6,10 +6,10 @@ import { useTranslation } from "next-i18next";
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
const { t } = useTranslation();
return <div className={`relative mt-4 px-4 pt-4`}>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.comments")}</p>
return <div className={`relative mt-4 px-4 pt-6`}>
<p className='text-sm text-bunker-300 pl-0.5'>{t("dashboard:sidebar.comments")}</p>
<textarea
className="bg-bunker-800 h-32 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
className="bg-bunker-800 placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
value={comment}
onChange={(e) => modifyComment(e.target.value, position)}
placeholder="Leave any comments here..."

View File

@ -0,0 +1,75 @@
import { Fragment } from 'react';
import { useTranslation } from "next-i18next";
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { Menu, Transition } from '@headlessui/react';
import Button from '../basic/buttons/Button';
import downloadDotEnv from '../utilities/secrets/downloadDotEnv';
import downloadYaml from '../utilities/secrets/downloadYaml';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
* @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to downlaod
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
*/
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
const { t } = useTranslation();
return <Menu
as="div"
className="relative inline-block text-left"
>
<Menu.Button
as="div"
className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
<Button
color="mineshaft"
size="icon-md"
icon={faDownload}
onButtonPressed={() => {}}
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
<Menu.Item>
<Button
color="mineshaft"
onButtonPressed={() => downloadDotEnv({ data, env })}
size="md"
text="Download as .env"
/>
</Menu.Item>
<Menu.Item>
<Button
color="mineshaft"
onButtonPressed={() => downloadYaml({ data, env })}
size="md"
text="Download as .yml"
/>
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
}
export default DownloadSecretMenu;

View File

@ -158,7 +158,7 @@ const SideBar = ({
</div>
</div>
<SecretVersionList secretId={data[0]?.id} />
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
</div>
)}
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>

View File

@ -1,35 +1,46 @@
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
import login1 from '~/pages/api/auth/Login1';
import login2 from '~/pages/api/auth/Login2';
import addSecrets from '~/pages/api/files/AddSecrets';
import getOrganizations from '~/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects';
import pushKeys from './secrets/pushKeys';
import encryptSecrets from './secrets/encryptSecrets';
import Telemetry from './telemetry/Telemetry';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
const jsrp = require('jsrp');
const client = new jsrp.client();
/**
* This function loggs in the user (whether it's right after signup, or a normal login)
* @param {*} email
* @param {*} password
* @param {*} setErrorLogin
* This function logs in the user (whether it's right after signup, or a normal login)
* @param {string} email - email of the user logging in
* @param {string} password - password of the user logging in
* @param {function} setErrorLogin - function that visually dispay an error is something is wrong
* @param {*} router
* @param {*} isSignUp
* @param {boolean} isSignUp - whether this log in is a part of signup
* @param {boolean} isLogin - ?
* @returns
*/
const attemptLogin = async (
email,
password,
setErrorLogin,
router,
isSignUp,
isLogin
email: string,
password: string,
setErrorLogin: (value: boolean) => void,
router: any,
isSignUp: boolean,
isLogin: boolean
) => {
try {
const telemetry = new Telemetry().getInstance();
@ -76,7 +87,7 @@ const attemptLogin = async (
});
const userOrgs = await getOrganizations();
const userOrgsData = userOrgs.map((org) => org._id);
const userOrgsData = userOrgs.map((org: { _id: string; }) => org._id);
let orgToLogin;
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
@ -90,7 +101,7 @@ const attemptLogin = async (
orgId: orgToLogin
});
orgUserProjects = orgUserProjects?.map((project) => project._id);
orgUserProjects = orgUserProjects?.map((project: { _id: string; }) => project._id);
let projectToLogin;
if (
orgUserProjects.includes(localStorage.getItem('projectData.id'))
@ -104,26 +115,7 @@ const attemptLogin = async (
console.log('ERROR: User likely has no projects. ', error);
}
}
// If user is logging in for the first time, add the example keys
if (isSignUp) {
await pushKeys({
obj: {
sDATABASE_URL: [
'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
'This is an example of secret referencing.'
],
sDB_USERNAME: ['OVERRIDE_THIS', ''],
sDB_PASSWORD: ['OVERRIDE_THIS', ''],
pDB_USERNAME: ['user1234', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need.'],
pDB_PASSWORD: ['example_password', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need.'],
sTWILIO_AUTH_TOKEN: ['example_twillio_token', ''],
sWEBSITE_URL: ['http://localhost:3000', ''],
},
workspaceId: projectToLogin,
env: 'Development'
});
}
if (email) {
telemetry.identify(email);
telemetry.capture('User Logged In');
@ -133,6 +125,7 @@ const attemptLogin = async (
router.push('/dashboard/');
}
} catch (error) {
console.log(error)
setErrorLogin(true);
console.log('Login response not available');
}

View File

@ -0,0 +1,33 @@
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This function downloads the secrets as a .env file
* @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
* @returns
*/
const checkOverrides = async ({ data }: { data: SecretDataProps[]; }) => {
let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret));
const overridenSecrets = data!.filter(
(secret) => secret.type === 'personal'
);
if (overridenSecrets.length) {
overridenSecrets.forEach((secret) => {
const index = secrets!.findIndex(
(_secret) => _secret.key === secret.key && _secret.type === 'shared'
);
secrets![index].value = secret.value;
});
secrets = secrets!.filter((secret) => secret.type === 'shared');
}
return secrets;
}
export default checkOverrides;

View File

@ -0,0 +1,46 @@
import { envMapping } from "../../../public/data/frequentConstants";
import checkOverrides from './checkOverrides';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This function downloads the secrets as a .env file
* @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to download
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
*/
const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
if (!data) return;
const secrets = await checkOverrides({ data });
const file = secrets!
.map(
(item: SecretDataProps) =>
`${
item.comment
? item.comment
.split('\n')
.map((comment) => '# '.concat(comment))
.join('\n') + '\n'
: ''
}` + [item.key, item.value].join('=')
)
.join('\n');
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a');
alink.href = fileDownloadUrl;
alink.download = envMapping[env] + '.env';
alink.click();
}
export default downloadDotEnv;

View File

@ -0,0 +1,52 @@
import YAML from 'yaml';
import { YAMLSeq } from 'yaml/types'
import { envMapping } from "../../../public/data/frequentConstants";
import checkOverrides from './checkOverrides';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This function downloads the secrets as a .yml file
* @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to download
* @param {string} obj.env - used for naming the file
* @returns
*/
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
if (!data) return;
const doc = new YAML.Document();
doc.contents = new YAMLSeq()
const secrets = await checkOverrides({ data });
secrets.forEach((secret) => {
const pair = YAML.createNode({ [secret.key]: secret.value });
pair.commentBefore = secret.comment
.split('\n')
.map((line) => (line ? ' '.concat(line) : ''))
.join('\n');
doc.add(pair);
});
const file = doc
.toString()
.split('\n')
.map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
.join('\n');
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a');
alink.href = fileDownloadUrl;
alink.download = envMapping[env] + '.yml';
alink.click();
}
export default downloadYaml;

View File

@ -0,0 +1,116 @@
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
const crypto = require("crypto");
const {
decryptAssymmetric,
encryptSymmetric,
} = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
interface EncryptedSecretProps {
id: string;
createdAt: string;
environment: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: "personal" | "shared";
}
/**
* Encypt secrets before pushing the to the DB
* @param {object} obj
* @param {object} obj.secretsToEncrypt - secrets that we want to encrypt
* @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets
* @returns
*/
const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsToEncrypt: SecretDataProps[]; workspaceId: string; env: string; }) => {
const sharedKey = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
let randomBytes: string;
if (Object.keys(sharedKey).length > 0) {
// case: a (shared) key exists for the workspace
randomBytes = decryptAssymmetric({
ciphertext: sharedKey.latestKey.encryptedKey,
nonce: sharedKey.latestKey.nonce,
publicKey: sharedKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY,
});
} else {
// case: a (shared) key does not exist for the workspace
randomBytes = crypto.randomBytes(16).toString("hex");
}
const secrets = secretsToEncrypt.map((secret) => {
// encrypt key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag,
} = encryptSymmetric({
plaintext: secret.key,
key: randomBytes,
});
// encrypt value
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag,
} = encryptSymmetric({
plaintext: secret.value,
key: randomBytes,
});
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag,
} = encryptSymmetric({
plaintext: secret.comment ?? '',
key: randomBytes,
});
const result: EncryptedSecretProps = {
id: secret.id,
createdAt: '',
environment: env,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
type: secret.type,
};
return result;
});
return secrets;
}
export default encryptSecrets;

View File

@ -1,7 +1,7 @@
import getSecrets from '~/pages/api/files/GetSecrets';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
import { envMapping } from '../../../public/data/frequentConstants';
import guidGenerator from '../randomId';
const {
decryptAssymmetric,
@ -10,6 +10,22 @@ const {
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
interface EncryptedSecretProps {
_id: string;
createdAt: string;
environment: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: "personal" | "shared";
}
interface SecretProps {
key: string;
value: string;
@ -18,107 +34,102 @@ interface SecretProps {
id: string;
}
interface Props {
interface FunctionProps {
env: keyof typeof envMapping;
setFileState: any;
setIsKeyAvailable: any;
setData: any;
workspaceId: string;
}
/**
* Gets the secrets for a certain project
* @param {object} obj
* @param {string} obj.env - environment for which we are getting secrets
* @param {boolean} obj.isKeyAvailable - if a person is able to create new key pairs
* @param {function} obj.setData - state function that manages the state of secrets in the dashboard
* @param {string} obj.workspaceId - id of a workspace for which we are getting secrets
*/
const getSecretsForProject = async ({
env,
setFileState,
setIsKeyAvailable,
setData,
workspaceId
}: Props) => {
}: FunctionProps) => {
try {
let file;
let encryptedSecrets;
try {
file = await getSecrets(workspaceId, envMapping[env]);
setFileState(file);
encryptedSecrets = await getSecrets(workspaceId, envMapping[env]);
} catch (error) {
console.log('ERROR: Not able to access the latest file');
console.log('ERROR: Not able to access the latest version of secrets');
}
const latestKey = await getLatestFileKey({ workspaceId })
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
setIsKeyAvailable(!file.key ? file.secrets.length == 0 : true);
setIsKeyAvailable(!latestKey ? encryptedSecrets.length == 0 : true);
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
const tempFileState: SecretProps[] = [];
if (file.key) {
const tempDecryptedSecrets: SecretProps[] = [];
if (latestKey) {
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: file.key.encryptedKey,
nonce: file.key.nonce,
publicKey: file.key.sender.publicKey,
ciphertext: latestKey.latestKey.encryptedKey,
nonce: latestKey.latestKey.nonce,
publicKey: latestKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
file.secrets.map((secretPair: any) => {
// decrypt .env file with symmetric key
// decrypt secret keys, values, and comments
encryptedSecrets.map((secret: EncryptedSecretProps) => {
const plainTextKey = decryptSymmetric({
ciphertext: secretPair.secretKey.ciphertext,
iv: secretPair.secretKey.iv,
tag: secretPair.secretKey.tag,
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const plainTextValue = decryptSymmetric({
ciphertext: secretPair.secretValue.ciphertext,
iv: secretPair.secretValue.iv,
tag: secretPair.secretValue.tag,
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
let plainTextComment;
if (secretPair.secretComment.ciphertext) {
if (secret.secretCommentCiphertext) {
plainTextComment = decryptSymmetric({
ciphertext: secretPair.secretComment.ciphertext,
iv: secretPair.secretComment.iv,
tag: secretPair.secretComment.tag,
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key
});
} else {
plainTextComment = "";
}
tempFileState.push({
id: secretPair._id,
tempDecryptedSecrets.push({
id: secret._id,
key: plainTextKey,
value: plainTextValue,
type: secretPair.type,
type: secret.type,
comment: plainTextComment
});
});
}
setFileState(tempFileState);
setData(
tempFileState.map((line, index) => {
return {
id: line['id'],
pos: index,
key: line['key'],
value: line['value'],
type: line['type'],
comment: line['comment']
};
})
);
return tempFileState.map((line, index) => {
const result = tempDecryptedSecrets.map((secret, index) => {
return {
id: line['id'],
id: secret['id'],
pos: index,
key: line['key'],
value: line['value'],
type: line['type'],
comment: line['comment']
key: secret['key'],
value: secret['value'],
type: secret['type'],
comment: secret['comment']
};
});
setData(result);
return result;
} catch (error) {
console.log('Something went wrong during accessing or decripting secrets.');
}

View File

@ -1,126 +0,0 @@
import uploadSecrets from "~/pages/api/files/UploadSecrets";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import getWorkspaceKeys from "~/pages/api/workspace/getWorkspaceKeys";
import { envMapping } from "../../../public/data/frequentConstants";
const crypto = require("crypto");
const {
decryptAssymmetric,
encryptSymmetric,
encryptAssymmetric,
} = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
export interface IK {
publicKey: string;
userId: string;
}
/**
* This function pushes the keys to the database after decrypting them end-to-end
* @param {object} obj
* @param {object} obj.obj - object with all the key pairs
* @param {object} obj.workspaceId - the id of a project to which a user is pushing
* @param {object} obj.env - which environment a user is pushing to
*/
const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: string; env: string; }) => {
const sharedKey = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
let randomBytes: string;
if (Object.keys(sharedKey).length > 0) {
// case: a (shared) key exists for the workspace
randomBytes = decryptAssymmetric({
ciphertext: sharedKey.latestKey.encryptedKey,
nonce: sharedKey.latestKey.nonce,
publicKey: sharedKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY,
});
} else {
// case: a (shared) key does not exist for the workspace
randomBytes = crypto.randomBytes(16).toString("hex");
}
const secrets = Object.keys(obj).map((key) => {
// encrypt key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag,
} = encryptSymmetric({
plaintext: key.slice(1),
key: randomBytes,
});
// encrypt value
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag,
} = encryptSymmetric({
plaintext: obj[key as keyof typeof obj][0],
key: randomBytes,
});
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag,
} = encryptSymmetric({
plaintext: obj[key as keyof typeof obj][1],
key: randomBytes,
});
const visibility = key.charAt(0) == "p" ? "personal" : "shared";
return {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"),
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][1]).digest("hex"),
type: visibility,
};
});
// obtain public keys of all receivers (i.e. members in workspace)
const publicKeys = await getWorkspaceKeys({
workspaceId,
});
// assymmetrically encrypt key with each receiver public keys
const keys = publicKeys.map((k: IK) => {
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: k.publicKey,
privateKey: PRIVATE_KEY,
});
return {
encryptedKey: ciphertext,
nonce,
userId: k.userId,
};
});
// send payload
await uploadSecrets({
workspaceId,
secrets,
keys,
environment: envMapping[env as keyof typeof envMapping],
});
};
export default pushKeys;

View File

@ -1,79 +0,0 @@
import publicKeyInfical from '~/pages/api/auth/publicKeyInfisical';
import changeHerokuConfigVars from '~/pages/api/integrations/ChangeHerokuConfigVars';
const crypto = require('crypto');
const {
encryptSymmetric,
encryptAssymmetric
} = require('../cryptography/crypto');
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
interface Props {
obj: Record<string, string>;
integrationId: string;
}
const pushKeysIntegration = async ({ obj, integrationId }: Props) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
const randomBytes = crypto.randomBytes(16).toString('hex');
const secrets = Object.keys(obj).map((key) => {
// encrypt key
const {
ciphertext: ciphertextKey,
iv: ivKey,
tag: tagKey
} = encryptSymmetric({
plaintext: key,
key: randomBytes
});
// encrypt value
const {
ciphertext: ciphertextValue,
iv: ivValue,
tag: tagValue
} = encryptSymmetric({
plaintext: obj[key],
key: randomBytes
});
const visibility = 'shared';
return {
ciphertextKey,
ivKey,
tagKey,
hashKey: crypto.createHash('sha256').update(key).digest('hex'),
ciphertextValue,
ivValue,
tagValue,
hashValue: crypto.createHash('sha256').update(obj[key]).digest('hex'),
type: visibility
};
});
// obtain public keys of all receivers (i.e. members in workspace)
const publicKeyInfisical = await publicKeyInfical();
const publicKey = (await publicKeyInfisical.json()).publicKey;
// assymmetrically encrypt key with each receiver public keys
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey,
privateKey: PRIVATE_KEY
});
const key = {
encryptedKey: ciphertext,
nonce
};
changeHerokuConfigVars({ integrationId, key, secrets });
};
export default pushKeysIntegration;

View File

@ -20,7 +20,6 @@ const getActionData = async ({ actionId }: workspaceProps) => {
}
}
).then(async (res) => {
console.log(188, res)
if (res && res.status == 200) {
return (await res.json()).action;
} else {

View File

@ -0,0 +1,30 @@
import SecurityClient from '~/utilities/SecurityClient';
/**
* This function performs a rollback of secrets in a certain project
* @param {object} obj
* @param {string} obj.workspaceId - id of the project for which we are rolling back data
* @param {number} obj.version - version to which we are rolling back
* @returns
*/
const performSecretRollback = async ({ workspaceId, version }: { workspaceId: string; version: number; }) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + "/secret-snapshots/rollback", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
version
})
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json());
} else {
console.log('Failed to perform the secret rollback');
}
});
};
export default performSecretRollback;

View File

@ -111,7 +111,7 @@ const PITRecoverySidebar = ({
}
})
setSnapshotData({ id: secretSnapshotData._id, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions })
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions })
}
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>

View File

@ -0,0 +1,48 @@
import SecurityClient from '~/utilities/SecurityClient';
interface EncryptedSecretProps {
id: string;
createdAt: string;
environment: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: "personal" | "shared";
}
/**
* This function adds secrets to a certain project
* @param {object} obj
* @param {EncryptedSecretProps} obj.secrets - the ids of secrets that we want to add
* @param {string} obj.env - the environment to which we are adding secrets
* @param {string} obj.workspaceId - the project to which we are adding secrets
* @returns
*/
const addSecrets = async ({ secrets, env, workspaceId }: { secrets: EncryptedSecretProps[]; env: string; workspaceId: string; }) => {
return SecurityClient.fetchCall('/api/v2/secrets', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
environment: env,
workspaceId,
secrets
})
}
).then(async (res) => {
if (res && res.status == 200) {
return await res.json();
} else {
console.log('Failed to add certain project secrets');
}
});
};
export default addSecrets;

View File

@ -0,0 +1,27 @@
import SecurityClient from '~/utilities/SecurityClient';
/**
* This function deletes certain secrets from a certain project
* @param {string[]} secretIds - the ids of secrets that we want to be deleted
* @returns
*/
const deleteSecrets = async ({ secretIds }: { secretIds: string[] }) => {
return SecurityClient.fetchCall('/api/v2/secrets', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
secretIds
})
}
).then(async (res) => {
if (res && res.status == 200) {
return await res.json();
} else {
console.log('Failed to delete certain project secrets');
}
});
};
export default deleteSecrets;

View File

@ -1,19 +1,17 @@
import SecurityClient from '~/utilities/SecurityClient';
/**
* This function fetches the encrypted secrets from the .env file
* This function fetches the encrypted secrets for a certain project
* @param {string} workspaceId - project is for which a user is trying to get secrets
* @param {string} env - environment of a project for which a user is trying ot get secrets
* @returns
*/
const getSecrets = async (workspaceId: string, env: string) => {
return SecurityClient.fetchCall(
'/api/v1/secret/' +
workspaceId +
'?' +
'/api/v2/secrets?' +
new URLSearchParams({
environment: env,
channel: 'web'
workspaceId
}),
{
method: 'GET',
@ -23,7 +21,7 @@ const getSecrets = async (workspaceId: string, env: string) => {
}
).then(async (res) => {
if (res && res.status == 200) {
return await res.json();
return (await res.json()).secrets;
} else {
console.log('Failed to get project secrets');
}

View File

@ -0,0 +1,44 @@
import SecurityClient from '~/utilities/SecurityClient';
interface EncryptedSecretProps {
id: string;
createdAt: string;
environment: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: "personal" | "shared";
}
/**
* This function updates certain secrets in a certain project
* @param {object} obj
* @param {EncryptedSecretProps[]} obj.secrets - the ids of secrets that we want to update
* @returns
*/
const updateSecrets = async ({ secrets }: { secrets: EncryptedSecretProps[] }) => {
return SecurityClient.fetchCall('/api/v2/secrets', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
secrets
})
}
).then(async (res) => {
if (res && res.status == 200) {
return await res.json();
} else {
console.log('Failed to update certain project secrets');
}
});
};
export default updateSecrets;

View File

@ -9,7 +9,6 @@ import {
faArrowLeft,
faCheck,
faClockRotateLeft,
faDownload,
faEye,
faEyeSlash,
faFolderOpen,
@ -18,28 +17,32 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getProjectSercetSnapshotsCount from 'ee/api/secrets/GetProjectSercetSnapshotsCount';
import performSecretRollback from 'ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from 'ee/components/PITRecoverySidebar';
import Button from '~/components/basic/buttons/Button';
import ListBox from '~/components/basic/Listbox';
import BottonRightPopup from '~/components/basic/popups/BottomRightPopup';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
import DownloadSecretMenu from '~/components/dashboard/DownloadSecretsMenu';
import DropZone from '~/components/dashboard/DropZone';
import KeyPair from '~/components/dashboard/KeyPair';
import SideBar from '~/components/dashboard/SideBar';
import NavHeader from '~/components/navigation/NavHeader';
import encryptSecrets from '~/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
import pushKeys from '~/components/utilities/secrets/pushKeys';
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
import guidGenerator from '~/utilities/randomId';
import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants';
import addSecrets from '../api/files/AddSecrets';
import deleteSecrets from '../api/files/DeleteSecrets';
import updateSecrets from '../api/files/UpdateSecrets';
import getUser from '../api/user/getUser';
import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaces from '../api/workspace/getWorkspaces';
const queryString = require("query-string");
interface SecretDataProps {
type: 'personal' | 'shared';
@ -50,9 +53,18 @@ interface SecretDataProps {
comment: string;
}
interface overrideProps {
id: string;
keyName: string;
value: string;
pos: number;
comment: string;
}
interface SnapshotProps {
id: string;
createdAt: string;
version: number;
secretVersions: {
id: string;
pos: number;
@ -87,7 +99,7 @@ function findDuplicates(arr: any[]) {
*/
export default function Dashboard() {
const [data, setData] = useState<SecretDataProps[] | null>();
const [fileState, setFileState] = useState<SecretDataProps[]>([]);
const [initialData, setInitialData] = useState<SecretDataProps[]>([]);
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [workspaceId, setWorkspaceId] = useState('');
@ -194,11 +206,11 @@ export default function Dashboard() {
const dataToSort = await getSecretsForProject({
env,
setFileState,
setIsKeyAvailable,
setData,
workspaceId: String(router.query.id)
});
setInitialData(dataToSort);
reorderRows(dataToSort);
setSharedToHide(
@ -234,14 +246,6 @@ export default function Dashboard() {
]);
};
interface overrideProps {
id: string;
keyName: string;
value: string;
pos: number;
comment: string;
}
/**
* This function add an ovverrided version of a certain secret to the current user
* @param {object} obj
@ -273,12 +277,12 @@ export default function Dashboard() {
text: `${secretName} has been deleted. Remember to save changes.`,
type: 'error'
});
setData(data!.filter((row: SecretDataProps) => !ids.includes(row.id)));
sortValuesHandler(data!.filter((row: SecretDataProps) => !ids.includes(row.id)), sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
};
/**
* This function deleted the override of a certain secrer
* @param {string} id - id of a secret to be deleted
* @param {string} id - id of a shared secret; the override with the same key should be deleted
*/
const deleteOverride = (id: string) => {
setButtonReady(true);
@ -291,7 +295,7 @@ export default function Dashboard() {
setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride))
// resort secrets
const tempData = data!.filter((row: SecretDataProps) => !(row.id == id && row.type == 'personal'))
const tempData = data!.filter((row: SecretDataProps) => !(row.key == data!.filter(row => row.id == id)[0]?.key && row.type == 'personal'))
sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical")
};
@ -311,14 +315,6 @@ export default function Dashboard() {
setButtonReady(true);
};
const modifyVisibility = (value: "shared" | "personal", pos: number) => {
setData((oldData) => {
oldData![pos].type = value;
return [...oldData!];
});
setButtonReady(true);
};
const modifyComment = (value: string, pos: number) => {
setData((oldData) => {
oldData![pos].comment = value;
@ -336,10 +332,6 @@ export default function Dashboard() {
modifyKey(value, pos);
}, []);
const listenChangeVisibility = useCallback((value: "shared" | "personal", pos: number) => {
modifyVisibility(value, pos);
}, []);
const listenChangeComment = useCallback((value: string, pos: number) => {
modifyComment(value, pos);
}, []);
@ -347,22 +339,20 @@ export default function Dashboard() {
/**
* Save the changes of environment variables and push them to the database
*/
const savePush = async (dataToPush?: any[], envToPush?: string) => {
let obj;
const savePush = async (dataToPush?: SecretDataProps[]) => {
let newData: SecretDataProps[] | null | undefined;
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data
if ((dataToPush ?? [])?.length > 0) {
obj = Object.assign(
{},
...dataToPush!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
);
newData = dataToPush;
} else {
// Format the new object with environment variables
obj = Object.assign(
{},
...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
);
newData = data;
}
const obj = Object.assign(
{},
...newData!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
);
// Checking if any of the secret keys start with a number - if so, don't do anything
const nameErrors = !Object.keys(obj)
.map((key) => !isNaN(Number(key[0].charAt(0))))
@ -383,10 +373,37 @@ export default function Dashboard() {
});
}
// Once "Save changed is clicked", disable that button
// Once "Save changes" is clicked, disable that button
setButtonReady(false);
console.log(envToPush ? envToPush : env, env, envToPush)
pushKeys({ obj, workspaceId: String(router.query.id), env: envToPush ? envToPush : env });
const secretsToBeDeleted
= initialData
.filter(initDataPoint => !newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id))
.map(secret => secret.id);
const secretsToBeAdded
= newData!
.filter(newDataPoint => !initialData.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id));
const secretsToBeUpdated
= newData!.filter(newDataPoint => initialData
.filter(initDataPoint => newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id)
&& (newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].value != initDataPoint.value
|| newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key
|| newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment))
.map(secret => secret.id).includes(newDataPoint.id));
if (secretsToBeDeleted.length > 0) {
await deleteSecrets({ secretIds: secretsToBeDeleted });
}
if (secretsToBeAdded.length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId, env: envMapping[env] })
await addSecrets({ secrets, env: envMapping[env], workspaceId });
}
if (secretsToBeUpdated.length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated, workspaceId, env: envMapping[env] })
await updateSecrets({ secrets });
}
// If this user has never saved environment variables before, show them a prompt to read docs
if (!hasUserEverPushed) {
@ -425,19 +442,6 @@ export default function Dashboard() {
setData(sortedData);
};
// This function downloads the secrets as a .env file
const download = () => {
const file = data!
.map((item: SecretDataProps) => [item.key, item.value].join('='))
.join('\n');
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a');
alink.href = fileDownloadUrl;
alink.download = envMapping[env] + '.env';
alink.click();
};
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
deleteRow({ids, secretName});
};
@ -535,8 +539,6 @@ export default function Dashboard() {
<Button
text={String(t("Rollback to this snapshot"))}
onButtonPressed={async () => {
const envsToRollback = snapshotData.secretVersions.map(sv => sv.environment).filter((v, i, a) => a.indexOf(v) === i);
// Update secrets in the state only for the current environment
setData(
snapshotData.secretVersions
@ -548,18 +550,9 @@ export default function Dashboard() {
})
);
// Rollback each of the environments in the snapshot
// #TODO: clean up other environments
envsToRollback.map(async (envToRollback) => {
await savePush(
snapshotData.secretVersions
.filter(row => row.environment == envToRollback)
.map((sv, position) => {
return {id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''}
}),
reverseEnvMapping[envToRollback]
);
});
// Perform the rollback globally
performSecretRollback({ workspaceId, version: snapshotData.version })
setSnapshotData(undefined);
createNotification({
text: `Rollback has been performed successfully.`,
@ -614,12 +607,7 @@ export default function Dashboard() {
/>
</div>}
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
<Button
onButtonPressed={download}
color="mineshaft"
size="icon-md"
icon={faDownload}
/>
<DownloadSecretMenu data={data} env={env} />
</div>}
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
<Button
@ -672,7 +660,9 @@ export default function Dashboard() {
modifyValue={listenChangeValue}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
@ -694,7 +684,9 @@ export default function Dashboard() {
modifyValue={listenChangeValue}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={true}
@ -728,7 +720,6 @@ export default function Dashboard() {
/>
)}
{
// fileState.message == 'Access needed to pull the latest file' ||
(!isKeyAvailable && (
<>
<FontAwesomeIcon