feat: allow sharing of secrets publicly

This commit is contained in:
ShubhamPalriwala
2024-06-05 18:02:55 +05:30
parent 6c596092b0
commit 5df53a25fc
11 changed files with 195 additions and 61 deletions

View File

@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.uuid("orgId").notNullable().alter();
t.uuid("userId").notNullable().alter();
});
}
}

View File

@@ -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()

View File

@@ -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: "/",

View File

@@ -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<TPermissionServiceFactory, "getOrgPermission">;
@@ -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

View File

@@ -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;

View File

@@ -15,13 +15,23 @@ export const useCreateSharedSecret = () => {
});
};
export const useCreatePublicSharedSecret = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
const { data } = await apiRequest.post<TSharedSecret>(
"/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<TSharedSecret, { message: string }, { sharedSecretId: string }>({
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => {
const { data } = await apiRequest.delete<TSharedSecret>(
`/api/v1/secret-sharing/${sharedSecretId}`

View File

@@ -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<FormData>({
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<boolean>({
@@ -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) => {
<SecretInput
isVisible={false}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[100px]"
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[70px]"
/>
</FormControl>
)}

View File

@@ -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 (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<Head>
<title>Secret Sharing</title>
@@ -60,14 +59,13 @@ export const ShareSecretSection = () => {
Share Secret
</Button>
</div>
<ShareSecretsTable
handlePopUpOpen={handlePopUpOpen}
/>
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic={false} />
<DeleteActionModal
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
title={`Delete ${(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
} shared secret?`}
title={`Delete ${
(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
} shared secret?`}
onChange={(isOpen) => handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)}
deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")}
@@ -75,4 +73,4 @@ export const ShareSecretSection = () => {
/>
</div>
);
};
};

View File

@@ -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 (
<div className="flex flex-col justify-between bg-bunker-800 text-gray-200 md:h-screen">
<div className="flex h-screen flex-col bg-bunker-800 text-gray-200">
<Head>
<title>Secret Shared | Infisical</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="my-4 flex justify-center md:my-8">
<Image src="/images/biglogo.png" height={180} width={240} alt="Infisical logo" />
<div className="flex items-center justify-center p-4">
<Link href="https://infisical.com">
<Image
src="/images/biglogo.png"
height={60}
width={80}
alt="Infisical logo"
className="cursor-pointer"
/>
</Link>
</div>
<p className="mb-6 px-8 text-center text-xl md:px-0 md:text-3xl">
A secret has been shared with you securely via Infisical
</p>
<div className="flex min-h-screen w-full flex-col md:flex-row">
<DragonMainImage />
<div className="m-4 flex flex-1 flex-col items-center justify-start md:m-0">
<p className="mt-8 mb-2 text-xl font-semibold text-mineshaft-100 md:mt-20">
Shared Secret
<div className="flex flex-1 flex-col items-center justify-center px-4">
<div className="flex w-full max-w-4xl flex-col items-center gap-4 md:gap-20">
<p className="text-center text-xl font-semibold text-gray-200 md:text-3xl">
Secret Shared via Infisical
</p>
<div className="mb-4 rounded-lg md:p-2">
<SecretTable
isLoading={isLoading}
decryptedSecret={decryptedSecret}
isUrlCopied={isUrlCopied}
copyUrlToClipboard={copyUrlToClipboard}
/>
<div className="flex w-full flex-grow flex-col gap-6 md:flex-row md:gap-12">
<div className="flex-1 self-center pt-4 text-center md:pt-0 md:text-left">
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-4 md:text-xl">
Safe & Secure
</p>
<p className="md:text-md text-sm">
Infisical uses <span className="text-primary">Zero Knowledge</span> to ensure that
your secrets are truly private (even from us).
</p>
</div>
<div className="flex-1 rounded-lg p-2">
<SecretTable
isLoading={isLoading}
decryptedSecret={decryptedSecret}
isUrlCopied={isUrlCopied}
copyUrlToClipboard={copyUrlToClipboard}
/>
</div>
<div className="flex-1 self-center text-center md:text-right">
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-4 md:text-xl">
Open Source
</p>
<p className="md:text-md text-sm">
Infisical is open source. <br className="hidden md:inline" />
Check us out on{" "}
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-primary"
>
GitHub
</a>
.
</p>
</div>
</div>
<Button
className="mt-4 max-w-[600px] md:mt-0"
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("createSharedSecret");
}}
>
Share your own Secret
</Button>
</div>
</div>
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic />
<div className="flex items-center justify-center p-4">
<p className="text-center text-sm text-gray-200">
Developed by{" "}
<a className="text-primary" href="https://infisical.com">
Infisical
</a>
<br />
Open Source Secret Management{" "}
</p>
</div>
</div>
);
};

View File

@@ -1,14 +0,0 @@
import Image from "next/image";
export const DragonMainImage = () => {
return (
<div className="hidden flex-1 flex-col items-center justify-center md:block md:items-start md:p-4">
<Image
src="/images/dragon-book.svg"
height={1000}
width={1413}
alt="Infisical Dragon - Came to send you a secret!"
/>
</div>
);
};

View File

@@ -1,2 +1 @@
export { DragonMainImage } from "./MainImage";
export { SecretTable } from "./SecretTable";