mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 14:27:23 +00:00
Add back hashed hex for secret sharing
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -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>({
|
||||
|
||||
Reference in New Issue
Block a user