mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add back hashed hex for secret sharing
This commit is contained in:
@@ -17,13 +17,6 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.timestamp("lastViewedAt").nullable();
|
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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const SecretSharingSchema = z.object({
|
|||||||
encryptedValue: z.string(),
|
encryptedValue: z.string(),
|
||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string().uuid()
|
||||||
}),
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
hashedHex: z.string().min(1)
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: SecretSharingSchema.pick({
|
200: SecretSharingSchema.pick({
|
||||||
encryptedValue: true,
|
encryptedValue: true,
|
||||||
@@ -71,10 +74,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById(
|
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({
|
||||||
req.params.id,
|
sharedSecretId: req.params.id,
|
||||||
req.permission?.orgId
|
hashedHex: req.query.hashedHex,
|
||||||
);
|
orgId: req.permission?.orgId
|
||||||
|
});
|
||||||
if (!sharedSecret) return undefined;
|
if (!sharedSecret) return undefined;
|
||||||
return {
|
return {
|
||||||
encryptedValue: sharedSecret.encryptedValue,
|
encryptedValue: sharedSecret.encryptedValue,
|
||||||
@@ -97,6 +101,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
encryptedValue: z.string(),
|
encryptedValue: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
expiresAt: z.string(),
|
expiresAt: z.string(),
|
||||||
@@ -109,13 +114,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { encryptedValue, iv, tag, expiresAt, expiresAfterViews } = req.body;
|
|
||||||
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
|
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
|
||||||
encryptedValue,
|
...req.body,
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
expiresAt,
|
|
||||||
expiresAfterViews,
|
|
||||||
accessType: SecretSharingAccessType.Anyone
|
accessType: SecretSharingAccessType.Anyone
|
||||||
});
|
});
|
||||||
return { id: sharedSecret.id };
|
return { id: sharedSecret.id };
|
||||||
@@ -132,6 +132,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().max(50).optional(),
|
name: z.string().max(50).optional(),
|
||||||
encryptedValue: z.string(),
|
encryptedValue: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
expiresAt: z.string(),
|
expiresAt: z.string(),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
TCreatePublicSharedSecretDTO,
|
TCreatePublicSharedSecretDTO,
|
||||||
TCreateSharedSecretDTO,
|
TCreateSharedSecretDTO,
|
||||||
TDeleteSharedSecretDTO,
|
TDeleteSharedSecretDTO,
|
||||||
|
TGetActiveSharedSecretByIdDTO,
|
||||||
TGetSharedSecretsDTO
|
TGetSharedSecretsDTO
|
||||||
} from "./secret-sharing-types";
|
} from "./secret-sharing-types";
|
||||||
|
|
||||||
@@ -24,21 +25,21 @@ export const secretSharingServiceFactory = ({
|
|||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
orgDAL
|
orgDAL
|
||||||
}: TSecretSharingServiceFactoryDep) => {
|
}: TSecretSharingServiceFactoryDep) => {
|
||||||
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
|
const createSharedSecret = async ({
|
||||||
const {
|
actor,
|
||||||
actor,
|
actorId,
|
||||||
actorId,
|
orgId,
|
||||||
orgId,
|
actorAuthMethod,
|
||||||
actorAuthMethod,
|
actorOrgId,
|
||||||
actorOrgId,
|
encryptedValue,
|
||||||
encryptedValue,
|
hashedHex,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
name,
|
name,
|
||||||
accessType,
|
accessType,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
expiresAfterViews
|
expiresAfterViews
|
||||||
} = createSharedSecretInput;
|
}: TCreateSharedSecretDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
name,
|
name,
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
|
hashedHex,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
@@ -74,8 +76,15 @@ export const secretSharingServiceFactory = ({
|
|||||||
return { id: newSharedSecret.id };
|
return { id: newSharedSecret.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
|
const createPublicSharedSecret = async ({
|
||||||
const { encryptedValue, iv, tag, expiresAt, expiresAfterViews, accessType } = createSharedSecretInput;
|
encryptedValue,
|
||||||
|
hashedHex,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
expiresAt,
|
||||||
|
expiresAfterViews,
|
||||||
|
accessType
|
||||||
|
}: TCreatePublicSharedSecretDTO) => {
|
||||||
if (new Date(expiresAt) < new Date()) {
|
if (new Date(expiresAt) < new Date()) {
|
||||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||||
}
|
}
|
||||||
@@ -95,6 +104,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
|
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
|
hashedHex,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
@@ -142,8 +152,11 @@ export const secretSharingServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getActiveSharedSecretById = async (sharedSecretId: string, orgId?: string) => {
|
const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => {
|
||||||
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId });
|
const sharedSecret = await secretSharingDAL.findOne({
|
||||||
|
id: sharedSecretId,
|
||||||
|
hashedHex
|
||||||
|
});
|
||||||
if (!sharedSecret)
|
if (!sharedSecret)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Shared secret not found"
|
message: "Shared secret not found"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type TSharedSecretPermission = {
|
|||||||
|
|
||||||
export type TCreatePublicSharedSecretDTO = {
|
export type TCreatePublicSharedSecretDTO = {
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
|
hashedHex: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
@@ -26,6 +27,12 @@ export type TCreatePublicSharedSecretDTO = {
|
|||||||
accessType: SecretSharingAccessType;
|
accessType: SecretSharingAccessType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetActiveSharedSecretByIdDTO = {
|
||||||
|
sharedSecretId: string;
|
||||||
|
hashedHex: string;
|
||||||
|
orgId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||||
|
|
||||||
export type TDeleteSharedSecretDTO = {
|
export type TDeleteSharedSecretDTO = {
|
||||||
|
|||||||
@@ -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]>({
|
return useQuery<TViewSharedSecretResponse, [string]>({
|
||||||
enabled: Boolean(secretId),
|
enabled: Boolean(sharedSecretId) && Boolean(hashedHex),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
hashedHex
|
||||||
|
});
|
||||||
|
|
||||||
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
||||||
`/api/v1/secret-sharing/public/${secretId}`
|
`/api/v1/secret-sharing/public/${sharedSecretId}`,
|
||||||
|
{
|
||||||
|
params
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
encryptedValue: data.encryptedValue,
|
encryptedValue: data.encryptedValue,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type TSharedSecret = {
|
|||||||
export type TCreateSharedSecretRequest = {
|
export type TCreateSharedSecretRequest = {
|
||||||
name?: string;
|
name?: string;
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
|
hashedHex: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { secretKeys } from "@app/hooks/api/secrets/queries";
|
|||||||
import { DecryptedSecret, SecretType } from "@app/hooks/api/secrets/types";
|
import { DecryptedSecret, SecretType } from "@app/hooks/api/secrets/types";
|
||||||
import { secretSnapshotKeys } from "@app/hooks/api/secretSnapshots/queries";
|
import { secretSnapshotKeys } from "@app/hooks/api/secretSnapshots/queries";
|
||||||
import { UserWsKeyPair, WsTag } from "@app/hooks/api/types";
|
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 { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
||||||
import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types";
|
import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types";
|
||||||
@@ -404,7 +404,7 @@ export const SecretListView = ({
|
|||||||
isOpen={popUp.createTag.isOpen}
|
isOpen={popUp.createTag.isOpen}
|
||||||
onToggle={(isOpen) => handlePopUpToggle("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;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddShareSecretModal2 = ({ popUp, handlePopUpToggle }: Props) => {
|
export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.createSharedSecret?.isOpen}
|
isOpen={popUp?.createSharedSecret?.isOpen}
|
||||||
@@ -7,7 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
|
|||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
|
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
import { AddShareSecretModal2 } from "./AddShareSecretModal2";
|
import { AddShareSecretModal } from "./AddShareSecretModal";
|
||||||
import { ShareSecretsTable } from "./ShareSecretsTable";
|
import { ShareSecretsTable } from "./ShareSecretsTable";
|
||||||
|
|
||||||
type DeleteModalData = { name: string; id: string };
|
type DeleteModalData = { name: string; id: string };
|
||||||
@@ -59,7 +59,7 @@ export const ShareSecretSection = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
<AddShareSecretModal2 popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
|
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
|
||||||
title={`Delete ${
|
title={`Delete ${
|
||||||
|
|||||||
@@ -59,15 +59,18 @@ export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
))}
|
))}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{!isLoading && data?.totalCount !== undefined && (
|
{!isLoading &&
|
||||||
<Pagination
|
data?.secrets &&
|
||||||
count={data.totalCount}
|
data.secrets.length >= perPage &&
|
||||||
page={page}
|
data?.totalCount !== undefined && (
|
||||||
perPage={perPage}
|
<Pagination
|
||||||
onChangePage={(newPage) => setPage(newPage)}
|
count={data.totalCount}
|
||||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
page={page}
|
||||||
/>
|
perPage={perPage}
|
||||||
)}
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!isLoading && !data?.secrets?.length && (
|
{!isLoading && !data?.secrets?.length && (
|
||||||
<EmptyState title="No secrets shared yet" icon={faKey} />
|
<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 expiresAt = new Date(new Date().getTime() + Number(expiresIn));
|
||||||
|
|
||||||
const key = crypto.randomBytes(16).toString("hex");
|
const key = crypto.randomBytes(16).toString("hex");
|
||||||
|
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
plaintext: secret,
|
plaintext: secret,
|
||||||
key
|
key
|
||||||
@@ -80,6 +81,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
|||||||
const { id } = await createSharedSecret.mutateAsync({
|
const { id } = await createSharedSecret.mutateAsync({
|
||||||
name,
|
name,
|
||||||
encryptedValue: ciphertext,
|
encryptedValue: ciphertext,
|
||||||
|
hashedHex,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
@@ -87,8 +89,11 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
|||||||
accessType
|
accessType
|
||||||
});
|
});
|
||||||
|
|
||||||
setSecretLink(`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(key)}`);
|
setSecretLink(
|
||||||
|
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
||||||
|
hashedHex
|
||||||
|
)}-${encodeURIComponent(key)}`
|
||||||
|
);
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
setCopyTextSecret("secret");
|
setCopyTextSecret("secret");
|
||||||
|
|||||||
@@ -11,9 +11,15 @@ import { SecretContainer, SecretErrorContainer } from "./components";
|
|||||||
export const ViewSecretPublicPage = () => {
|
export const ViewSecretPublicPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id, key: urlEncodedPublicKey } = router.query;
|
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 (
|
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]">
|
<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;
|
secretKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// note: implementation currently doesn't account
|
|
||||||
// for in-org (authenticated) secret sharing
|
|
||||||
export const SecretContainer = ({ secret, secretKey: key }: Props) => {
|
export const SecretContainer = ({ secret, secretKey: key }: Props) => {
|
||||||
const [isVisible, setIsVisible] = useToggle(false);
|
const [isVisible, setIsVisible] = useToggle(false);
|
||||||
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
|
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
|
||||||
|
|||||||
Reference in New Issue
Block a user