diff --git a/backend/src/db/migrations/20240728010334_secret-sharing-name.ts b/backend/src/db/migrations/20240728010334_secret-sharing-name.ts index d051d5e08..5bf43065f 100644 --- a/backend/src/db/migrations/20240728010334_secret-sharing-name.ts +++ b/backend/src/db/migrations/20240728010334_secret-sharing-name.ts @@ -17,13 +17,6 @@ export async function up(knex: Knex): Promise { t.timestamp("lastViewedAt").nullable(); }); } - - const doesHashedHexExist = await knex.schema.hasColumn(TableName.SecretSharing, "hashedHex"); - if (doesHashedHexExist) { - await knex.schema.alterTable(TableName.SecretSharing, (t) => { - t.dropColumn("hashedHex"); - }); - } } } diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index 3701e9f7a..de75fbafc 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -12,6 +12,7 @@ export const SecretSharingSchema = z.object({ encryptedValue: z.string(), iv: z.string(), tag: z.string(), + hashedHex: z.string(), expiresAt: z.date(), userId: z.string().uuid().nullable().optional(), orgId: z.string().uuid().nullable().optional(), diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 1cb6eb307..a23c1f596 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -57,6 +57,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => params: z.object({ id: z.string().uuid() }), + querystring: z.object({ + hashedHex: z.string().min(1) + }), response: { 200: SecretSharingSchema.pick({ encryptedValue: true, @@ -71,10 +74,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById( - req.params.id, - req.permission?.orgId - ); + const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({ + sharedSecretId: req.params.id, + hashedHex: req.query.hashedHex, + orgId: req.permission?.orgId + }); if (!sharedSecret) return undefined; return { encryptedValue: sharedSecret.encryptedValue, @@ -97,6 +101,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => schema: { body: z.object({ encryptedValue: z.string(), + hashedHex: z.string(), iv: z.string(), tag: z.string(), expiresAt: z.string(), @@ -109,13 +114,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const { encryptedValue, iv, tag, expiresAt, expiresAfterViews } = req.body; const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({ - encryptedValue, - iv, - tag, - expiresAt, - expiresAfterViews, + ...req.body, accessType: SecretSharingAccessType.Anyone }); return { id: sharedSecret.id }; @@ -132,6 +132,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => body: z.object({ name: z.string().max(50).optional(), encryptedValue: z.string(), + hashedHex: z.string(), iv: z.string(), tag: z.string(), expiresAt: z.string(), diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 17f1d8493..1f38bf1f1 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -8,6 +8,7 @@ import { TCreatePublicSharedSecretDTO, TCreateSharedSecretDTO, TDeleteSharedSecretDTO, + TGetActiveSharedSecretByIdDTO, TGetSharedSecretsDTO } from "./secret-sharing-types"; @@ -24,21 +25,21 @@ export const secretSharingServiceFactory = ({ secretSharingDAL, orgDAL }: TSecretSharingServiceFactoryDep) => { - const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => { - const { - actor, - actorId, - orgId, - actorAuthMethod, - actorOrgId, - encryptedValue, - iv, - tag, - name, - accessType, - expiresAt, - expiresAfterViews - } = createSharedSecretInput; + const createSharedSecret = async ({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + encryptedValue, + hashedHex, + iv, + tag, + name, + accessType, + expiresAt, + expiresAfterViews + }: TCreateSharedSecretDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); if (!permission) throw new UnauthorizedError({ name: "User not in org" }); @@ -62,6 +63,7 @@ export const secretSharingServiceFactory = ({ const newSharedSecret = await secretSharingDAL.create({ name, encryptedValue, + hashedHex, iv, tag, expiresAt: new Date(expiresAt), @@ -74,8 +76,15 @@ export const secretSharingServiceFactory = ({ return { id: newSharedSecret.id }; }; - const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => { - const { encryptedValue, iv, tag, expiresAt, expiresAfterViews, accessType } = createSharedSecretInput; + const createPublicSharedSecret = async ({ + encryptedValue, + hashedHex, + iv, + tag, + expiresAt, + expiresAfterViews, + accessType + }: TCreatePublicSharedSecretDTO) => { if (new Date(expiresAt) < new Date()) { throw new BadRequestError({ message: "Expiration date cannot be in the past" }); } @@ -95,6 +104,7 @@ export const secretSharingServiceFactory = ({ const newSharedSecret = await secretSharingDAL.create({ encryptedValue, + hashedHex, iv, tag, expiresAt: new Date(expiresAt), @@ -142,8 +152,11 @@ export const secretSharingServiceFactory = ({ }; }; - const getActiveSharedSecretById = async (sharedSecretId: string, orgId?: string) => { - const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId }); + const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => { + const sharedSecret = await secretSharingDAL.findOne({ + id: sharedSecretId, + hashedHex + }); if (!sharedSecret) throw new NotFoundError({ message: "Shared secret not found" diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 5ee656e77..76d723c62 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -19,6 +19,7 @@ export type TSharedSecretPermission = { export type TCreatePublicSharedSecretDTO = { encryptedValue: string; + hashedHex: string; iv: string; tag: string; expiresAt: string; @@ -26,6 +27,12 @@ export type TCreatePublicSharedSecretDTO = { accessType: SecretSharingAccessType; }; +export type TGetActiveSharedSecretByIdDTO = { + sharedSecretId: string; + hashedHex: string; + orgId?: string; +}; + export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO; export type TDeleteSharedSecretDTO = { diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index c36ab18c1..89a804f6e 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -36,12 +36,25 @@ export const useGetSharedSecrets = ({ }); }; -export const useGetActiveSharedSecretById = (secretId: string) => { +export const useGetActiveSharedSecretById = ({ + sharedSecretId, + hashedHex +}: { + sharedSecretId: string; + hashedHex: string; +}) => { return useQuery({ - enabled: Boolean(secretId), + enabled: Boolean(sharedSecretId) && Boolean(hashedHex), queryFn: async () => { + const params = new URLSearchParams({ + hashedHex + }); + const { data } = await apiRequest.get( - `/api/v1/secret-sharing/public/${secretId}` + `/api/v1/secret-sharing/public/${sharedSecretId}`, + { + params + } ); return { encryptedValue: data.encryptedValue, diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index d9d10c9f6..d35fea8d6 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -16,6 +16,7 @@ export type TSharedSecret = { export type TCreateSharedSecretRequest = { name?: string; encryptedValue: string; + hashedHex: string; iv: string; tag: string; expiresAt: Date; diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx index 83971a505..147e9d2b8 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx @@ -13,7 +13,7 @@ import { secretKeys } from "@app/hooks/api/secrets/queries"; import { DecryptedSecret, SecretType } from "@app/hooks/api/secrets/types"; import { secretSnapshotKeys } from "@app/hooks/api/secretSnapshots/queries"; import { UserWsKeyPair, WsTag } from "@app/hooks/api/types"; -import { AddShareSecretModal2 } from "@app/views/ShareSecretPage/components/AddShareSecretModal2"; +import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal"; import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store"; import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types"; @@ -404,7 +404,7 @@ export const SecretListView = ({ isOpen={popUp.createTag.isOpen} onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)} /> - + ); }; diff --git a/frontend/src/views/ShareSecretPage/components/AddShareSecretForm.tsx b/frontend/src/views/ShareSecretPage/components/AddShareSecretForm.tsx deleted file mode 100644 index e7b24d682..000000000 --- a/frontend/src/views/ShareSecretPage/components/AddShareSecretForm.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import crypto from "crypto"; - -import { useEffect, useRef } from "react"; -import { Controller } from "react-hook-form"; -import { AxiosError } from "axios"; -import * as yup from "yup"; - -import { createNotification } from "@app/components/notifications"; -import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto"; -import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2"; -import { - SecretSharingAccessType, - useCreatePublicSharedSecret, - useCreateSharedSecret -} from "@app/hooks/api/secretSharing"; - -const schema = yup.object({ - value: yup.string().max(10000).required().label("Shared Secret Value"), - expiresAfterViews: yup.string().required().label("Expires After Views"), - expiresInValue: yup.string().min(1).required().label("Expiration Value"), - accessType: yup.string().required().label("General Access") -}); - -export type FormData = yup.InferType; - -// values in ms -const expiresInOptions = [ - { label: "5 min", value: 5 * 60 * 1000 }, - { label: "30 min", value: 30 * 60 * 1000 }, - { label: "1 hour", value: 60 * 60 * 1000 }, - { label: "1 day", value: 24 * 60 * 60 * 1000 }, - { label: "7 days", value: 7 * 24 * 60 * 60 * 1000 }, - { label: "14 days", value: 14 * 24 * 60 * 60 * 1000 }, - { label: "30 days", value: 30 * 24 * 60 * 60 * 1000 } -]; - -const viewLimitOptions = [ - { label: "1", value: 1 }, - { label: "Unlimited", value: -1 } -]; - -export const AddShareSecretForm = ({ - isPublic, - inModal, - handleSubmit, - control, - isSubmitting, - setNewSharedSecret, - isInputDisabled -}: { - isPublic: boolean; - inModal: boolean; - handleSubmit: any; - control: any; - isSubmitting: boolean; - setNewSharedSecret: (value: string) => void; - isInputDisabled?: boolean; -}) => { - const isMounted = useRef(true); - - useEffect(() => { - return () => { - isMounted.current = false; - }; - }, []); - - const publicSharedSecretCreator = useCreatePublicSharedSecret(); - const privateSharedSecretCreator = useCreateSharedSecret(); - const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator; - - const onFormSubmit = async ({ - value, - expiresInValue, - expiresAfterViews, - accessType - }: FormData) => { - try { - const expiresAt = new Date(new Date().getTime() + Number(expiresInValue)); - - const key = crypto.randomBytes(16).toString("hex"); - const hashedHex = crypto.createHash("sha256").update(key).digest("hex"); - const { ciphertext, iv, tag } = encryptSymmetric({ - plaintext: value, - key - }); - - const { id } = await createSharedSecret.mutateAsync({ - encryptedValue: ciphertext, - iv, - tag, - expiresAt, - expiresAfterViews: expiresAfterViews === "-1" ? undefined : Number(expiresAfterViews), - accessType: accessType as SecretSharingAccessType - }); - - if (isMounted.current) { - setNewSharedSecret( - `${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent( - hashedHex - )}-${encodeURIComponent(key)}` - ); - createNotification({ - text: "Successfully created a shared secret", - type: "success" - }); - } - } catch (err) { - console.error(err); - const axiosError = err as AxiosError; - if (axiosError?.response?.status === 401) { - createNotification({ - text: "You do not have access to create shared secrets", - type: "error" - }); - } else { - createNotification({ - text: "Failed to create a shared secret", - type: "error" - }); - } - } - }; - return ( -
-
-
- ( - -