Add back hashed hex for secret sharing

This commit is contained in:
Tuan Dang
2024-07-30 07:16:03 -07:00
parent 0d4164ea81
commit 7cbd254f06
16 changed files with 100 additions and 322 deletions
@@ -36,12 +36,25 @@ export const useGetSharedSecrets = ({
});
};
export const useGetActiveSharedSecretById = (secretId: string) => {
export const useGetActiveSharedSecretById = ({
sharedSecretId,
hashedHex
}: {
sharedSecretId: string;
hashedHex: string;
}) => {
return useQuery<TViewSharedSecretResponse, [string]>({
enabled: Boolean(secretId),
enabled: Boolean(sharedSecretId) && Boolean(hashedHex),
queryFn: async () => {
const params = new URLSearchParams({
hashedHex
});
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
`/api/v1/secret-sharing/public/${secretId}`
`/api/v1/secret-sharing/public/${sharedSecretId}`,
{
params
}
);
return {
encryptedValue: data.encryptedValue,
@@ -16,6 +16,7 @@ export type TSharedSecret = {
export type TCreateSharedSecretRequest = {
name?: string;
encryptedValue: string;
hashedHex: string;
iv: string;
tag: string;
expiresAt: Date;
@@ -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)}
/>
<AddShareSecretModal2 popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</>
);
};
@@ -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<typeof schema>;
// 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 (
<form
className="flex w-full max-w-7xl flex-col items-center"
onSubmit={handleSubmit(onFormSubmit)}
>
<div
className={`w-full ${
!inModal && "rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6"
}`}
>
<div>
<Controller
control={control}
name="value"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Your Secret"
isError={Boolean(error)}
errorText={error?.message}
className="mb-2"
>
<textarea
disabled={isInputDisabled}
placeholder="Enter sensitive data to share via an encrypted link..."
{...field}
className="h-40 min-h-[70px] w-full rounded-md border border-mineshaft-600 bg-mineshaft-900 py-1.5 px-2 text-bunker-300 outline-none transition-all placeholder:text-mineshaft-400 hover:border-primary-400/30 focus:border-primary-400/50 group-hover:mr-2"
/>
</FormControl>
)}
/>
</div>
<Controller
control={control}
name="expiresInValue"
defaultValue="3600000"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Expires In" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{expiresInOptions.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="expiresAfterViews"
defaultValue="-1"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Max Views" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{viewLimitOptions.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{!isPublic && (
<Controller
control={control}
name="accessType"
defaultValue="organization"
render={({ field: { onChange, ...field } }) => (
<FormControl label="General Access">
<Select {...field} onValueChange={(e) => onChange(e)} className="w-full">
<SelectItem value="organization">People within your organization</SelectItem>
<SelectItem value="anyone">Anyone</SelectItem>
</Select>
</FormControl>
)}
/>
)}
<div className={`flex items-center space-x-4 pt-2 ${!inModal && ""}`}>
<Button className="mr-0" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
{inModal ? "Create" : "Create secret link"}
</Button>
{inModal && (
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
)}
</div>
</div>
</form>
);
};
@@ -10,7 +10,7 @@ type Props = {
) => void;
};
export const AddShareSecretModal2 = ({ popUp, handlePopUpToggle }: Props) => {
export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
return (
<Modal
isOpen={popUp?.createSharedSecret?.isOpen}
@@ -7,7 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
import { AddShareSecretModal2 } from "./AddShareSecretModal2";
import { AddShareSecretModal } from "./AddShareSecretModal";
import { ShareSecretsTable } from "./ShareSecretsTable";
type DeleteModalData = { name: string; id: string };
@@ -59,7 +59,7 @@ export const ShareSecretSection = () => {
</Button>
</div>
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
<AddShareSecretModal2 popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
title={`Delete ${
@@ -59,15 +59,18 @@ export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
))}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isLoading &&
data?.secrets &&
data.secrets.length >= perPage &&
data?.totalCount !== undefined && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isLoading && !data?.secrets?.length && (
<EmptyState title="No secrets shared yet" icon={faKey} />
)}
@@ -1,37 +0,0 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton } from "@app/components/v2";
export const ViewAndCopySharedSecret = ({
inModal,
newSharedSecret,
isUrlCopied,
copyUrlToClipboard
}: {
inModal: boolean;
newSharedSecret: string;
isUrlCopied: boolean;
copyUrlToClipboard: () => void;
}) => {
return (
<div className={`flex w-full justify-center px-6 ${!inModal ? "mx-auto max-w-2xl" : ""}`}>
<div className={`${!inModal ? "border border-mineshaft-600 bg-mineshaft-800 rounded-md p-4" : ""}`}>
<div className="my-2 flex items-center justify-end rounded-md border border-mineshaft-500 bg-mineshaft-700 p-2 text-base text-gray-400">
<p className="mr-4 break-all">{newSharedSecret}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyUrlToClipboard}
>
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Click to Copy
</span>
</IconButton>
</div>
</div>
</div>
);
};
@@ -72,6 +72,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
const key = crypto.randomBytes(16).toString("hex");
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: secret,
key
@@ -80,6 +81,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
const { id } = await createSharedSecret.mutateAsync({
name,
encryptedValue: ciphertext,
hashedHex,
iv,
tag,
expiresAt,
@@ -87,8 +89,11 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
accessType
});
setSecretLink(`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(key)}`);
setSecretLink(
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
hashedHex
)}-${encodeURIComponent(key)}`
);
reset();
setCopyTextSecret("secret");
@@ -11,9 +11,15 @@ import { SecretContainer, SecretErrorContainer } from "./components";
export const ViewSecretPublicPage = () => {
const router = useRouter();
const { id, key: urlEncodedPublicKey } = router.query;
const key = decodeURIComponent(urlEncodedPublicKey as string);
const { data: secret, error } = useGetActiveSharedSecretById(id as string);
const [hashedHex, key] = urlEncodedPublicKey
? urlEncodedPublicKey.toString().split("-")
: ["", ""];
const { data: secret, error } = useGetActiveSharedSecretById({
sharedSecretId: id as string,
hashedHex
});
return (
<div className="flex h-screen flex-col justify-between bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
@@ -18,8 +18,6 @@ type Props = {
secretKey: string;
};
// note: implementation currently doesn't account
// for in-org (authenticated) secret sharing
export const SecretContainer = ({ secret, secretKey: key }: Props) => {
const [isVisible, setIsVisible] = useToggle(false);
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({