From 5df53a25fc608e9475e76132a66c8e4ce0f5989b Mon Sep 17 00:00:00 2001 From: ShubhamPalriwala Date: Wed, 5 Jun 2024 18:02:55 +0530 Subject: [PATCH] feat: allow sharing of secrets publicly --- ...240605074539_make-secret-sharing-public.ts | 21 ++++ backend/src/db/schemas/secret-sharing.ts | 4 +- .../server/routes/v1/secret-sharing-router.ts | 37 +++++++ .../secret-sharing/secret-sharing-service.ts | 21 +++- .../secret-sharing/secret-sharing-types.ts | 6 +- .../src/hooks/api/secretSharing/mutations.ts | 20 +++- .../components/AddShareSecretModal.tsx | 15 +-- .../components/ShareSecretSection.tsx | 16 ++- .../ShareSecretPublicPage.tsx | 101 ++++++++++++++---- .../components/MainImage.tsx | 14 --- .../components/index.tsx | 1 - 11 files changed, 195 insertions(+), 61 deletions(-) create mode 100644 backend/src/db/migrations/20240605074539_make-secret-sharing-public.ts delete mode 100644 frontend/src/views/ShareSecretPublicPage/components/MainImage.tsx diff --git a/backend/src/db/migrations/20240605074539_make-secret-sharing-public.ts b/backend/src/db/migrations/20240605074539_make-secret-sharing-public.ts new file mode 100644 index 000000000..d4b034e52 --- /dev/null +++ b/backend/src/db/migrations/20240605074539_make-secret-sharing-public.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSharing)) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + t.uuid("orgId").nullable().alter(); + t.uuid("userId").nullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSharing)) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + t.uuid("orgId").notNullable().alter(); + t.uuid("userId").notNullable().alter(); + }); + } +} diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index 6fa104ebe..c8d938861 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -14,8 +14,8 @@ export const SecretSharingSchema = z.object({ tag: z.string(), hashedHex: z.string(), expiresAt: z.date(), - userId: z.string().uuid(), - orgId: z.string().uuid(), + userId: z.string().uuid().nullable().optional(), + orgId: z.string().uuid().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), expiresAfterViews: z.number().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 6cb551698..ef53d121a 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -70,6 +70,43 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => } }); + server.route({ + method: "POST", + url: "/public", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + encryptedValue: z.string(), + iv: z.string(), + tag: z.string(), + hashedHex: z.string(), + expiresAt: z + .string() + .refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"), + expiresAfterViews: z.number() + }), + response: { + 200: z.object({ + id: z.string().uuid() + }) + } + }, + handler: async (req) => { + const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body; + const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({ + encryptedValue, + iv, + tag, + hashedHex, + expiresAt: new Date(expiresAt), + expiresAfterViews + }); + return { id: sharedSecret.id }; + } + }); + server.route({ method: "POST", url: "/", diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index ccbce0a52..ceb3d3896 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -2,7 +2,12 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { UnauthorizedError } from "@app/lib/errors"; import { TSecretSharingDALFactory } from "./secret-sharing-dal"; -import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TSharedSecretPermission } from "./secret-sharing-types"; +import { + TCreatePublicSharedSecretDTO, + TCreateSharedSecretDTO, + TDeleteSharedSecretDTO, + TSharedSecretPermission +} from "./secret-sharing-types"; type TSecretSharingServiceFactoryDep = { permissionService: Pick; @@ -44,6 +49,19 @@ export const secretSharingServiceFactory = ({ return { id: newSharedSecret.id }; }; + const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => { + const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput; + const newSharedSecret = await secretSharingDAL.create({ + encryptedValue, + iv, + tag, + hashedHex, + expiresAt, + expiresAfterViews + }); + return { id: newSharedSecret.id }; + }; + const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => { const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput; const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); @@ -77,6 +95,7 @@ export const secretSharingServiceFactory = ({ return { createSharedSecret, + createPublicSharedSecret, getSharedSecrets, deleteSharedSecretById, getActiveSharedSecretByIdAndHashedHex diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 5f35b2848..769bb4479 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -8,14 +8,16 @@ export type TSharedSecretPermission = { orgId: string; }; -export type TCreateSharedSecretDTO = { +export type TCreatePublicSharedSecretDTO = { encryptedValue: string; iv: string; tag: string; hashedHex: string; expiresAt: Date; expiresAfterViews: number; -} & TSharedSecretPermission; +}; + +export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO; export type TDeleteSharedSecretDTO = { sharedSecretId: string; diff --git a/frontend/src/hooks/api/secretSharing/mutations.ts b/frontend/src/hooks/api/secretSharing/mutations.ts index e21cc08f6..e0c1dcc3c 100644 --- a/frontend/src/hooks/api/secretSharing/mutations.ts +++ b/frontend/src/hooks/api/secretSharing/mutations.ts @@ -15,13 +15,23 @@ export const useCreateSharedSecret = () => { }); }; +export const useCreatePublicSharedSecret = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (inputData: TCreateSharedSecretRequest) => { + const { data } = await apiRequest.post( + "/api/v1/secret-sharing/public", + inputData + ); + return data; + }, + onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"]) + }); +}; + export const useDeleteSharedSecret = () => { const queryClient = useQueryClient(); - return useMutation< - TSharedSecret, - { message: string }, - { sharedSecretId: string } - >({ + return useMutation({ mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => { const { data } = await apiRequest.delete( `/api/v1/secret-sharing/${sharedSecretId}` diff --git a/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx b/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx index 7ec9f95ad..6e2ebfab0 100644 --- a/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx +++ b/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx @@ -22,9 +22,8 @@ import { Select, SelectItem } from "@app/components/v2"; -import { useOrganization } from "@app/context"; import { useTimedReset } from "@app/hooks"; -import { useCreateSharedSecret } from "@app/hooks/api/secretSharing"; +import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing"; import { UsePopUpState } from "@app/hooks/usePopUp"; const expirationUnitsAndActions = [ @@ -65,9 +64,10 @@ type Props = { popUpName: keyof UsePopUpState<["createSharedSecret"]>, state?: boolean ) => void; + isPublic: boolean; }; -export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { +export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic }: Props) => { const { control, reset, @@ -76,8 +76,10 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { } = useForm({ resolver: yupResolver(schema) }); - const createSharedSecret = useCreateSharedSecret(); - const { currentOrg } = useOrganization(); + const publicSharedSecretCreator = useCreatePublicSharedSecret(); + const privateSharedSecretCreator = useCreateSharedSecret(); + const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator; + const [newSharedSecret, setnewSharedSecret] = useState(""); const hasSharedSecret = Boolean(newSharedSecret); const [isUrlCopied, , setIsUrlCopied] = useTimedReset({ @@ -101,7 +103,6 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { expiresAfterViews }: FormData) => { try { - if (!currentOrg?.id) return; const key = crypto.randomBytes(16).toString("hex"); const hashedHex = crypto.createHash("sha256").update(key).digest("hex"); const { ciphertext, iv, tag } = encryptSymmetric({ @@ -180,7 +181,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { )} diff --git a/frontend/src/views/ShareSecretPage/components/ShareSecretSection.tsx b/frontend/src/views/ShareSecretPage/components/ShareSecretSection.tsx index c71b9830f..41f9e55ff 100644 --- a/frontend/src/views/ShareSecretPage/components/ShareSecretSection.tsx +++ b/frontend/src/views/ShareSecretPage/components/ShareSecretSection.tsx @@ -22,7 +22,7 @@ export const ShareSecretSection = () => { const onDeleteApproved = async () => { try { deleteSharedSecret.mutateAsync({ - sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id, + sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id }); createNotification({ text: "Successfully deleted shared secret", @@ -40,7 +40,6 @@ export const ShareSecretSection = () => { }; return ( -
Secret Sharing @@ -60,14 +59,13 @@ export const ShareSecretSection = () => { Share Secret
- - + + handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)} deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name} onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")} @@ -75,4 +73,4 @@ export const ShareSecretSection = () => { /> ); -}; \ No newline at end of file +}; diff --git a/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx b/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx index 2da836eaa..d0a9bfaf7 100644 --- a/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx +++ b/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx @@ -1,13 +1,18 @@ import { useEffect, useMemo } from "react"; import Head from "next/head"; import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/router"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto"; -import { useTimedReset } from "@app/hooks"; +import { Button } from "@app/components/v2"; +import { usePopUp, useTimedReset } from "@app/hooks"; import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing"; -import { DragonMainImage, SecretTable } from "./components"; +import { AddShareSecretModal } from "../ShareSecretPage/components/AddShareSecretModal"; +import { SecretTable } from "./components"; export const ShareSecretPublicPage = () => { const router = useRouter(); @@ -53,36 +58,92 @@ export const ShareSecretPublicPage = () => { navigator.clipboard.writeText(decryptedSecret); setIsUrlCopied(true); }; + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["createSharedSecret"] as const); return ( -
+
Secret Shared | Infisical -
- Infisical logo +
+ + Infisical logo +
-

- A secret has been shared with you securely via Infisical -

-
- -
-

- Shared Secret + +

+
+

+ Secret Shared via Infisical

-
- +
+
+

+ Safe & Secure +

+

+ Infisical uses Zero Knowledge to ensure that + your secrets are truly private (even from us). +

+
+
+ +
+
+

+ Open Source +

+

+ Infisical is open source.
+ Check us out on{" "} + + GitHub + + . +

+
+
+ +
+

+ Developed by{" "} + + Infisical + +
+ Open Source Secret Management{" "} +

+
); }; diff --git a/frontend/src/views/ShareSecretPublicPage/components/MainImage.tsx b/frontend/src/views/ShareSecretPublicPage/components/MainImage.tsx deleted file mode 100644 index 49a7e17ed..000000000 --- a/frontend/src/views/ShareSecretPublicPage/components/MainImage.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import Image from "next/image"; - -export const DragonMainImage = () => { - return ( -
- Infisical Dragon - Came to send you a secret! -
- ); -}; diff --git a/frontend/src/views/ShareSecretPublicPage/components/index.tsx b/frontend/src/views/ShareSecretPublicPage/components/index.tsx index 5a7b53a0d..530af7c2f 100644 --- a/frontend/src/views/ShareSecretPublicPage/components/index.tsx +++ b/frontend/src/views/ShareSecretPublicPage/components/index.tsx @@ -1,2 +1 @@ -export { DragonMainImage } from "./MainImage"; export { SecretTable } from "./SecretTable";