From 0f98fc94f0444bbfbdedd69b308feb30e7bbb076 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 9 Feb 2024 22:28:37 +0400 Subject: [PATCH] Ghost user and migration finished! --- backend/src/lib/secret/index.ts | 126 ++++++++++++++++++ backend/src/server/routes/index.ts | 5 + backend/src/server/routes/v1/bot-router.ts | 4 +- .../src/server/routes/v2/project-router.ts | 6 +- backend/src/server/routes/v3/index.ts | 2 - .../src/server/routes/v3/project-router.ts | 88 ------------ .../src/services/project/project-service.ts | 20 ++- .../SecretOverviewPage/SecretOverviewPage.tsx | 26 +++- 8 files changed, 179 insertions(+), 98 deletions(-) create mode 100644 backend/src/lib/secret/index.ts delete mode 100644 backend/src/server/routes/v3/project-router.ts diff --git a/backend/src/lib/secret/index.ts b/backend/src/lib/secret/index.ts new file mode 100644 index 000000000..08cb0163d --- /dev/null +++ b/backend/src/lib/secret/index.ts @@ -0,0 +1,126 @@ +import { z } from "zod"; + +import { SecretKeyEncoding, TProjectKeys } from "@app/db/schemas"; + +import { decryptAsymmetric, decryptSymmetric } from "../crypto"; +import { decryptSymmetric128BitHexKeyUTF8, TDecryptSymmetricInput } from "../crypto/encryption"; + +export enum SecretDocType { + Secret = "secret", + SecretVersion = "secretVersion", + ApprovalSecret = "approvalSecret" +} + +const PartialSecretSchema = z.object({ + id: z.string(), + secretKeyCiphertext: z.string(), + secretKeyIV: z.string(), + secretKeyTag: z.string(), + + secretValueCiphertext: z.string(), + secretValueIV: z.string(), + secretValueTag: z.string(), + + secretCommentCiphertext: z.string().nullish(), + secretCommentIV: z.string().nullish(), + secretCommentTag: z.string().nullish(), + + docType: z.nativeEnum(SecretDocType), + + keyEncoding: z.string() +}); + +const PartialDecryptedSecretSchema = z.object({ + id: z.string(), + secretKey: z.string(), + secretValue: z.string(), + secretComment: z.string().optional(), + + docType: z.nativeEnum(SecretDocType) +}); + +export type TPartialSecret = z.infer; +export type TPartialDecryptedSecret = z.infer; + +const symmetricDecrypt = ({ + keyEncoding, + ciphertext, + tag, + iv, + key, + isApprovalSecret +}: TDecryptSymmetricInput & { keyEncoding: SecretKeyEncoding; isApprovalSecret: boolean }) => { + if (keyEncoding === SecretKeyEncoding.UTF8 || isApprovalSecret) { + const data = decryptSymmetric128BitHexKeyUTF8({ key, iv, tag, ciphertext }); + return data; + } + if (keyEncoding === SecretKeyEncoding.BASE64) { + const data = decryptSymmetric({ key, iv, tag, ciphertext }); + return data; + } + + throw new Error("Missing both encryption keys"); +}; + +export const decryptSecrets = ( + encryptedSecrets: TPartialSecret[], + privateKey: string, + latestKey: TProjectKeys & { + sender: { + publicKey: string; + }; + } +) => { + const key = decryptAsymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey + }); + + const secrets: TPartialDecryptedSecret[] = []; + + encryptedSecrets.forEach((encSecret) => { + const secretKey = symmetricDecrypt({ + ciphertext: encSecret.secretKeyCiphertext, + iv: encSecret.secretKeyIV, + tag: encSecret.secretKeyTag, + key, + keyEncoding: encSecret.keyEncoding as SecretKeyEncoding, + isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret + }); + + const secretValue = symmetricDecrypt({ + ciphertext: encSecret.secretValueCiphertext, + iv: encSecret.secretValueIV, + tag: encSecret.secretValueTag, + key, + keyEncoding: encSecret.keyEncoding as SecretKeyEncoding, + isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret + }); + + const secretComment = + encSecret.secretCommentCiphertext && encSecret.secretCommentIV && encSecret.secretCommentTag + ? symmetricDecrypt({ + ciphertext: encSecret.secretCommentCiphertext, + iv: encSecret.secretCommentIV, + tag: encSecret.secretCommentTag, + key, + keyEncoding: encSecret.keyEncoding as SecretKeyEncoding, + isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret + }) + : ""; + + const decryptedSecret: TPartialDecryptedSecret = { + id: encSecret.id, + secretKey, + secretValue, + secretComment, + docType: encSecret.docType + }; + + secrets.push(decryptedSecret); + }); + + return secrets; +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9444aff16..22d1e2123 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -327,7 +327,12 @@ export const registerRoutes = async ( identityProjectDAL, identityOrgMembershipDAL, projectBotDAL, + secretDAL, + orgDAL, + secretApprovalRequestDAL, + secretApprovalSecretDAL: sarSecretDAL, projectKeyDAL, + secretVersionDAL, userDAL, projectEnvDAL, orgService, diff --git a/backend/src/server/routes/v1/bot-router.ts b/backend/src/server/routes/v1/bot-router.ts index ff451a17e..20acf95ec 100644 --- a/backend/src/server/routes/v1/bot-router.ts +++ b/backend/src/server/routes/v1/bot-router.ts @@ -33,7 +33,7 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => { projectId: req.params.projectId }); - if (!project.e2ee) { + if (project.version === "v2") { throw new BadRequestError({ message: "Failed to find bot, project has E2EE disabled" }); } @@ -79,7 +79,7 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const project = await server.services.projectBot.findProjectByBotId(req.params.botId); - if (project?.e2ee === false) { + if (project?.version === "v2") { throw new BadRequestError({ message: "Failed to set bot active, project has E2EE disabled" }); } diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 7cbcff37c..faeb89f4c 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -17,7 +17,7 @@ const projectWithEnv = ProjectsSchema.merge( export const registerProjectRouter = async (server: FastifyZodProvider) => { /* Get project key */ server.route({ - url: "/:workspaceId/encrypted-key", + url: "/:projectId/encrypted-key", method: "GET", schema: { description: "Return encrypted project key", @@ -27,7 +27,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { 200: ProjectKeysSchema.merge( @@ -50,7 +50,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.params.workspaceId, + projectId: req.params.projectId, event: { type: EventType.GET_WORKSPACE_KEY, metadata: { diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts index 21d57d359..6a061a635 100644 --- a/backend/src/server/routes/v3/index.ts +++ b/backend/src/server/routes/v3/index.ts @@ -1,5 +1,4 @@ import { registerLoginRouter } from "./login-router"; -import { registerProjectRouter } from "./project-router"; import { registerSecretBlindIndexRouter } from "./secret-blind-index-router"; import { registerSecretRouter } from "./secret-router"; import { registerSignupRouter } from "./signup-router"; @@ -11,5 +10,4 @@ export const registerV3Routes = async (server: FastifyZodProvider) => { await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerSecretRouter, { prefix: "/secrets" }); await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" }); - await server.register(registerProjectRouter, { prefix: "/projects" }); }; diff --git a/backend/src/server/routes/v3/project-router.ts b/backend/src/server/routes/v3/project-router.ts deleted file mode 100644 index c1759f718..000000000 --- a/backend/src/server/routes/v3/project-router.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { z } from "zod"; - -import { ProjectMembershipsSchema, ProjectsSchema } from "@app/db/schemas"; -import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { authRateLimit } from "@app/server/config/rateLimiter"; - -const projectWithEnv = ProjectsSchema.merge( - z.object({ - _id: z.string(), - environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array() - }) -); - -export const registerProjectRouter = async (server: FastifyZodProvider) => { - /* Create new project */ - server.route({ - method: "POST", - url: "/", - config: { - rateLimit: authRateLimit - }, - schema: { - body: z.object({ - projectName: z.string().trim(), - organizationId: z.string().trim() - }), - response: { - 200: z.object({ - project: projectWithEnv - }) - } - }, - handler: async (req) => { - const project = await server.services.project.createProject({ - actorId: req.permission.id, - actor: req.permission.type, - orgId: req.body.organizationId, - workspaceName: req.body.projectName - }); - - return { project }; - } - }); - - server.route({ - method: "POST", - url: "/:projectId/memberships", - config: { - rateLimit: authRateLimit - }, - schema: { - params: z.object({ - projectId: z.string() - }), - body: z.object({ - emails: z.string().email().array() - }), - response: { - 200: z.object({ - memberships: ProjectMembershipsSchema.array() - }) - } - }, - handler: async (req) => { - const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({ - projectId: req.params.projectId, - actorId: req.permission.id, - actor: req.permission.type, - emails: req.body.emails - }); - - await server.services.auditLog.createAuditLog({ - projectId: req.params.projectId, - ...req.auditLogInfo, - event: { - type: EventType.ADD_BATCH_WORKSPACE_MEMBER, - metadata: memberships.map(({ userId, id }) => ({ - userId: userId || "", - membershipId: id, - email: "" - })) - } - }); - - return { memberships }; - } - }); -}; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 5826e77f0..643c8aa72 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ +/* eslint-disable no-await-in-loop */ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; @@ -6,10 +8,18 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; +import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; +import { RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { createSecretBlindIndex } from "@app/lib/crypto"; -import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { + decryptAsymmetric, + encryptSymmetric128BitHexKeyUTF8, + infisicalSymmetricDecrypt, + infisicalSymmetricEncypt +} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectPermission } from "@app/lib/types"; @@ -17,11 +27,14 @@ import { TProjectPermission } from "@app/lib/types"; import { ActorType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { TSecretDALFactory } from "../secret/secret-dal"; +import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TUserDALFactory } from "../user/user-dal"; @@ -65,13 +78,18 @@ export const projectServiceFactory = ({ projectDAL, projectQueue, projectKeyDAL, + secretApprovalRequestDAL, + secretApprovalSecretDAL, permissionService, userDAL, folderDAL, orgService, + orgDAL, identityProjectDAL, + secretVersionDAL, projectBotDAL, identityOrgMembershipDAL, + secretDAL, secretBlindIndexDAL, projectMembershipDAL, projectEnvDAL, diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index d31eb3b6e..58c34d627 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -14,6 +14,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No import NavHeader from "@app/components/navigation/NavHeader"; import { PermissionDeniedBanner } from "@app/components/permissions"; import { + Alert, + AlertDescription, Button, EmptyState, IconButton, @@ -38,7 +40,8 @@ import { useGetFoldersByEnv, useGetProjectSecretsAllEnv, useGetUserWsKey, - useUpdateSecretV3 + useUpdateSecretV3, + useUpgradeProject } from "@app/hooks/api"; import { ProjectVersion } from "@app/hooks/api/workspace/types"; @@ -106,6 +109,7 @@ export const SecretOverviewPage = () => { environments: userAvailableEnvs.map(({ slug }) => slug) }); + const upgradeProject = useUpgradeProject(); const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3(); @@ -199,6 +203,24 @@ export const SecretOverviewPage = () => { } }; + const onUpgradeProject = useCallback(async () => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); + + if (!PRIVATE_KEY) { + createNotification({ + type: "error", + text: "Private key not found" + }); + + return; + } + + await upgradeProject.mutateAsync({ + projectId: workspaceId, + privateKey: PRIVATE_KEY + }); + }, []); + const handleResetSearch = () => setSearchFilter(""); const handleFolderClick = (path: string) => {