From c097c918edc56c7e4c3231bf7a876da7231e5e69 Mon Sep 17 00:00:00 2001 From: ShubhamPalriwala Date: Thu, 30 May 2024 10:06:14 +0530 Subject: [PATCH 1/8] fix: docs to open in new tab --- frontend/src/views/ShareSecretPage/ShareSecretPage.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/ShareSecretPage/ShareSecretPage.tsx b/frontend/src/views/ShareSecretPage/ShareSecretPage.tsx index e1636446e..95861ec61 100644 --- a/frontend/src/views/ShareSecretPage/ShareSecretPage.tsx +++ b/frontend/src/views/ShareSecretPage/ShareSecretPage.tsx @@ -13,14 +13,16 @@ export const ShareSecretPage = () => {

Share secrets securely using a shareable link

- - + + +
Documentation{" "} - +
+
From 3438dbc70d9fabf16da6acf242f57d4bbfba6ba6 Mon Sep 17 00:00:00 2001 From: ShubhamPalriwala Date: Fri, 31 May 2024 00:00:48 +0530 Subject: [PATCH 2/8] feat: secret sharing supports expiry on view count + multi-line secret value --- ...044702_universal-text-in-secret-sharing.ts | 35 ++++ backend/src/db/schemas/secret-sharing.ts | 6 +- backend/src/db/schemas/users.ts | 4 +- .../server/routes/v1/secret-sharing-router.ts | 27 +-- .../secret-sharing/secret-sharing-service.ts | 34 +++- .../secret-sharing/secret-sharing-types.ts | 4 +- frontend/src/hooks/api/secretSharing/types.ts | 17 +- .../components/AddShareSecretModal.tsx | 167 ++++++++++-------- .../components/ShareSecretsRow.tsx | 73 +++++--- .../components/ShareSecretsTable.tsx | 14 +- 10 files changed, 251 insertions(+), 130 deletions(-) create mode 100644 backend/src/db/migrations/20240530044702_universal-text-in-secret-sharing.ts diff --git a/backend/src/db/migrations/20240530044702_universal-text-in-secret-sharing.ts b/backend/src/db/migrations/20240530044702_universal-text-in-secret-sharing.ts new file mode 100644 index 000000000..63d9dd81c --- /dev/null +++ b/backend/src/db/migrations/20240530044702_universal-text-in-secret-sharing.ts @@ -0,0 +1,35 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasExpiresAfterViewsColumn = await knex.schema.hasColumn(TableName.SecretSharing, "expiresAfterViews"); + const hasSecretNameColumn = await knex.schema.hasColumn(TableName.SecretSharing, "name"); + + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + if (!hasExpiresAfterViewsColumn) { + t.integer("expiresAfterViews").nullable(); + t.timestamp("expiresAt").nullable().alter(); + } + + if (hasSecretNameColumn) { + t.dropColumn("name"); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasExpiresAfterViewsColumn = await knex.schema.hasColumn(TableName.SecretSharing, "expiresAfterViews"); + const hasSecretNameColumn = await knex.schema.hasColumn(TableName.SecretSharing, "name"); + + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + if (hasExpiresAfterViewsColumn) { + t.dropColumn("expiresAfterViews"); + t.timestamp("expiresAt").notNullable().alter(); + } + + if (!hasSecretNameColumn) { + t.string("name").notNullable(); + } + }); +} diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index a412221b2..541cd1fc0 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -9,16 +9,16 @@ import { TImmutableDBKeys } from "./models"; export const SecretSharingSchema = z.object({ id: z.string().uuid(), - name: z.string(), encryptedValue: z.string(), iv: z.string(), tag: z.string(), hashedHex: z.string(), - expiresAt: z.date(), + expiresAt: z.date().nullable().optional(), userId: z.string().uuid(), orgId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + expiresAfterViews: z.number().nullable().optional() }); export type TSecretSharing = z.infer; diff --git a/backend/src/db/schemas/users.ts b/backend/src/db/schemas/users.ts index c10af4ba4..9e0b9a3b5 100644 --- a/backend/src/db/schemas/users.ts +++ b/backend/src/db/schemas/users.ts @@ -23,8 +23,8 @@ export const UsersSchema = z.object({ isGhost: z.boolean().default(false), username: z.string(), isEmailVerified: z.boolean().default(false).nullable().optional(), - consecutiveFailedMfaAttempts: z.number().optional(), - isLocked: z.boolean().optional(), + consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(), + isLocked: z.boolean().default(false).nullable().optional(), temporaryLockDateEnd: z.date().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 67751395b..f1016576c 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -45,7 +45,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => hashedHex: z.string() }), response: { - 200: SecretSharingSchema.pick({ name: true, encryptedValue: true, iv: true, tag: true, expiresAt: true }) + 200: SecretSharingSchema.pick({ + encryptedValue: true, + iv: true, + tag: true, + expiresAt: true, + expiresAfterViews: true + }) } }, handler: async (req) => { @@ -55,11 +61,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => ); if (!sharedSecret) return undefined; return { - name: sharedSecret.name, encryptedValue: sharedSecret.encryptedValue, iv: sharedSecret.iv, tag: sharedSecret.tag, - expiresAt: sharedSecret.expiresAt + expiresAt: sharedSecret.expiresAt, + expiresAfterViews: sharedSecret.expiresAfterViews }; } }); @@ -72,14 +78,15 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }, schema: { body: z.object({ - name: z.string(), encryptedValue: z.string(), iv: z.string(), tag: z.string(), hashedHex: z.string(), - expiresAt: z.string().refine((date) => new Date(date) > new Date(), { - message: "Expires at should be a future date" - }) + expiresAt: z + .string() + .optional() + .refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"), + expiresAfterViews: z.number().optional() }), response: { 200: z.object({ @@ -89,19 +96,19 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { name, encryptedValue, iv, tag, hashedHex, expiresAt } = req.body; + const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body; const sharedSecret = await req.server.services.secretSharing.createSharedSecret({ actor: req.permission.type, actorId: req.permission.id, orgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - name, encryptedValue, iv, tag, hashedHex, - expiresAt: new Date(expiresAt) + expiresAt: expiresAt ? new Date(expiresAt) : undefined, + expiresAfterViews }); return { id: sharedSecret.id }; } diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 85cfe97f6..23d39ea9e 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -16,17 +16,28 @@ export const secretSharingServiceFactory = ({ secretSharingDAL }: TSecretSharingServiceFactoryDep) => { const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => { - const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, encryptedValue, iv, tag, hashedHex, expiresAt } = - createSharedSecretInput; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - if (!permission) throw new UnauthorizedError({ name: "User not in org" }); - const newSharedSecret = await secretSharingDAL.create({ - name, + const { + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, encryptedValue, iv, tag, hashedHex, expiresAt, + expiresAfterViews + } = createSharedSecretInput; + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + if (!permission) throw new UnauthorizedError({ name: "User not in org" }); + const newSharedSecret = await secretSharingDAL.create({ + encryptedValue, + iv, + tag, + hashedHex, + expiresAt, + expiresAfterViews, userId: actorId, orgId }); @@ -43,9 +54,18 @@ export const secretSharingServiceFactory = ({ const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => { const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex }); - if (sharedSecret && sharedSecret.expiresAt < new Date()) { + if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) { return; } + if (sharedSecret.expiresAfterViews != null && sharedSecret.expiresAfterViews >= 0) { + if (sharedSecret.expiresAfterViews === 0) { + await secretSharingDAL.deleteById(sharedSecretId); + return; + } + await secretSharingDAL.updateById(sharedSecretId, { + expiresAfterViews: sharedSecret.expiresAfterViews - 1 + }); + } return sharedSecret; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 2d14ed12c..5af475c52 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -9,12 +9,12 @@ export type TSharedSecretPermission = { }; export type TCreateSharedSecretDTO = { - name: string; encryptedValue: string; iv: string; tag: string; hashedHex: string; - expiresAt: Date; + expiresAt?: Date; + expiresAfterViews?: number; } & TSharedSecretPermission; export type TDeleteSharedSecretDTO = { diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index 2fbddfb35..04bd19de7 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -1,31 +1,26 @@ export type TSharedSecret = { id: string; - name: string; - encryptedValue: string; - iv: string; - tag: string; - hashedHex: string; userId: string; - expiresAt: Date; + orgId: string; createdAt: Date; updatedAt: Date; -}; +} & TCreateSharedSecretRequest; export type TCreateSharedSecretRequest = { - name: string; encryptedValue: string; iv: string; tag: string; hashedHex: string; - expiresAt: Date; + expiresAt?: Date; + expiresAfterViews?: number; }; export type TViewSharedSecretResponse = { - name: string; encryptedValue: string; iv: string; tag: string; - expiresAt: Date; + expiresAt?: Date; + expiresAfterViews?: number; }; export type TDeleteSharedSecretRequest = { diff --git a/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx b/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx index 00ee27b44..dcaf91b46 100644 --- a/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx +++ b/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx @@ -9,9 +9,7 @@ import { AxiosError } from "axios"; import * as yup from "yup"; import { createNotification } from "@app/components/notifications"; -import { - encryptSymmetric, -} from "@app/components/utilities/cryptography/crypto"; +import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto"; import { Button, FormControl, @@ -22,7 +20,8 @@ import { ModalContent, SecretInput, Select, - SelectItem + SelectItem, + Switch } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { useTimedReset } from "@app/hooks"; @@ -63,10 +62,10 @@ const expirationUnitsAndActions = [ ]; const schema = yup.object({ - name: yup.string().max(100).required().label("Shared Secret Name"), value: yup.string().max(1000).required().label("Shared Secret Value"), - expiresInValue: yup.number().min(1).required().label("Expiration Value"), - expiresInUnit: yup.string().required().label("Expiration Unit") + expiresAfterViews: yup.number().min(1).optional().label("Expires After Views"), + expiresInValue: yup.number().min(1).optional().label("Expiration Value"), + expiresInUnit: yup.string().optional().label("Expiration Unit") }); export type FormData = yup.InferType; @@ -91,9 +90,10 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { const createSharedSecret = useCreateSharedSecret(); const { currentOrg } = useOrganization(); const [newSharedSecret, setnewSharedSecret] = useState(""); + const [expiryOption, setExpiryOption] = useState<"time" | "views">("time"); const hasSharedSecret = Boolean(newSharedSecret); const [isUrlCopied, , setIsUrlCopied] = useTimedReset({ - initialState: false, + initialState: false }); const copyUrlToClipboard = () => { @@ -106,10 +106,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { } }, [isUrlCopied]); - const onFormSubmit = async ({ name, value, expiresInValue, expiresInUnit }: FormData) => { + const onFormSubmit = async ({ + value, + expiresInValue, + expiresInUnit, + 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({ @@ -117,25 +121,26 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { key }); - const expiresAt = new Date(); const updateExpiresAt = expirationUnitsAndActions.find( (item) => item.unit === expiresInUnit )?.action; - if (updateExpiresAt) { + if (updateExpiresAt && expiresInValue) { updateExpiresAt(expiresAt, expiresInValue); } const { id } = await createSharedSecret.mutateAsync({ - name, encryptedValue: ciphertext, iv, tag, hashedHex, - expiresAt, + expiresAt: expiryOption === "time" ? expiresAt : undefined, + expiresAfterViews: expiryOption === "views" ? expiresAfterViews : undefined }); setnewSharedSecret( - `${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(hashedHex)}-${encodeURIComponent(key)}` + `${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent( + hashedHex + )}-${encodeURIComponent(key)}` ); createNotification({ @@ -168,90 +173,114 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { setnewSharedSecret(""); }} > - + {!hasSharedSecret ? (
- ( - - - - )} - /> ( )} /> -
-
+
+

+ Set Expiry Based On +

+
+

+ Time +

+ setExpiryOption(value ? "views" : "time")} + isChecked={expiryOption === "views"} + /> +

+ Views +

+
+
+
+ {expiryOption === "views" ? ( ( - + )} /> -
-
- ( - - - - )} - /> -
+ ) : ( +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+ )}
-
+