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..e23d134db --- /dev/null +++ b/backend/src/db/migrations/20240530044702_universal-text-in-secret-sharing.ts @@ -0,0 +1,33 @@ +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"); + } + + 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"); + } + + 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..6fa104ebe 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -9,7 +9,6 @@ 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(), @@ -18,7 +17,8 @@ export const SecretSharingSchema = z.object({ 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/lib/knex/index.ts b/backend/src/lib/knex/index.ts index bf057cc73..0faeba290 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -128,7 +128,7 @@ export const ormify = (db: Kne } if ($decr) { Object.entries($decr).forEach(([incrementField, incrementValue]) => { - void query.increment(incrementField, incrementValue); + void query.decrement(incrementField, incrementValue); }); } const [docs] = await query; diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 67751395b..6cb551698 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,14 @@ 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() + .refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"), + expiresAfterViews: z.number() }), response: { 200: z.object({ @@ -89,19 +95,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: new Date(expiresAt), + 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..ccbce0a52 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,16 @@ 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, { $decr: { 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..5f35b2848 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; + expiresAfterViews: number; } & TSharedSecretPermission; export type TDeleteSharedSecretDTO = { diff --git a/docs/documentation/platform/secret-sharing.mdx b/docs/documentation/platform/secret-sharing.mdx index 4e4761924..680751820 100644 --- a/docs/documentation/platform/secret-sharing.mdx +++ b/docs/documentation/platform/secret-sharing.mdx @@ -1,37 +1,36 @@ --- title: "Secret Sharing" sidebarTitle: "Secret Sharing" -description: "Learn how to share time-bound secrets securely with anyone on the internet." +description: "Learn how to share time & view-count bound secrets securely with anyone on the internet." --- Developers frequently need to share secrets with team members, contractors, or other third parties, which can be risky due to potential leaks or misuse. -Infisical offers a secure solution for sharing secrets over the internet in a time-bound manner. +Infisical offers a secure solution for sharing secrets over the internet in a time and view count bound manner. + With its zero-knowledge architecture, secrets shared via Infisical remain unreadable even to Infisical itself. ## Share a Secret -1. Navigate to the **Projects** page. +1. Navigate to the **Organization** page. 2. Click on the **Secret Sharing** tab from the sidebar. ![Secret Sharing](../../images/platform/secret-sharing/overview.png) -3. Click on the **Share Secret** button. - - Infisical does not have access to the shared secrets. This is a part of our zero - knowledge architecture. + Infisical does not have access to the shared secrets. This is a part of our + zero knowledge architecture. -4. Enter the secret you want to share and set the expiration time. Click on the **Share Secret** button. +3. Click on the **Share Secret** button. Set the secret, its expiration time as well as the number of views allowed. It expires as soon as any of the conditions are met. -![Add Sharing Secret](../../images/platform/secret-sharing/new-secret.png) + ![Add View-Bound Sharing Secret](../../images/platform/secret-sharing/create-new-secret.png) Secret once set cannot be changed. This is to ensure that the secret is not tampered with. -5. Copy the link and share it with the intended recipient. Anyone with the link can access the secret before its expiration time. Hence, it is recommended to share the link only with the intended recipient. +5. Copy the link and share it with the intended recipient. Anyone with the link can access the secret before its expiration condition. Hence, it is recommended to share the link only with the intended recipient. ![Copy URL](../../images/platform/secret-sharing/copy-url.png) diff --git a/docs/images/platform/secret-sharing/create-new-secret.png b/docs/images/platform/secret-sharing/create-new-secret.png new file mode 100644 index 000000000..335fca2b2 Binary files /dev/null and b/docs/images/platform/secret-sharing/create-new-secret.png differ diff --git a/docs/images/platform/secret-sharing/new-secret.png b/docs/images/platform/secret-sharing/new-secret.png deleted file mode 100644 index 13a587ec9..000000000 Binary files a/docs/images/platform/secret-sharing/new-secret.png and /dev/null differ diff --git a/docs/images/platform/secret-sharing/overview.png b/docs/images/platform/secret-sharing/overview.png index 3bdbe8878..428110517 100644 Binary files a/docs/images/platform/secret-sharing/overview.png and b/docs/images/platform/secret-sharing/overview.png differ diff --git a/docs/images/platform/secret-sharing/public-view.png b/docs/images/platform/secret-sharing/public-view.png index f1bd9482f..8b4077c65 100644 Binary files a/docs/images/platform/secret-sharing/public-view.png and b/docs/images/platform/secret-sharing/public-view.png differ diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index b4cf71531..c7970fabc 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -8,9 +8,7 @@ export const useGetSharedSecrets = () => { return useQuery({ queryKey: ["sharedSecrets"], queryFn: async () => { - const { data } = await apiRequest.get( - "/api/v1/secret-sharing/" - ); + const { data } = await apiRequest.get("/api/v1/secret-sharing/"); return data; } }); @@ -23,11 +21,9 @@ export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: `/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}` ); return { - name: data.name, encryptedValue: data.encryptedValue, iv: data.iv, tag: data.tag, - expiresAt: data.expiresAt }; } }); diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index 2fbddfb35..424e3525c 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -1,31 +1,24 @@ 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; + expiresAfterViews: number; }; export type TViewSharedSecretResponse = { - name: string; encryptedValue: string; iv: string; tag: string; - expiresAt: Date; }; export type TDeleteSharedSecretRequest = { 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

diff --git a/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx b/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx index 00ee27b44..2fda35be4 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, @@ -49,22 +47,12 @@ const expirationUnitsAndActions = [ unit: "Weeks", action: (expiresAt: Date, expiresInValue: number) => expiresAt.setDate(expiresAt.getDate() + expiresInValue * 7) - }, - { - unit: "Months", - action: (expiresAt: Date, expiresInValue: number) => - expiresAt.setMonth(expiresAt.getMonth() + expiresInValue) - }, - { - unit: "Years", - action: (expiresAt: Date, expiresInValue: number) => - expiresAt.setFullYear(expiresAt.getFullYear() + expiresInValue) } ]; const schema = yup.object({ - name: yup.string().max(100).required().label("Shared Secret Name"), - value: yup.string().max(1000).required().label("Shared Secret Value"), + value: yup.string().max(10000).required().label("Shared Secret Value"), + expiresAfterViews: yup.number().min(1).required().label("Expires After Views"), expiresInValue: yup.number().min(1).required().label("Expiration Value"), expiresInUnit: yup.string().required().label("Expiration Unit") }); @@ -93,7 +81,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { const [newSharedSecret, setnewSharedSecret] = useState(""); const hasSharedSecret = Boolean(newSharedSecret); const [isUrlCopied, , setIsUrlCopied] = useTimedReset({ - initialState: false, + initialState: false }); const copyUrlToClipboard = () => { @@ -106,10 +94,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 +109,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, + expiresAfterViews }); 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 +161,100 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { setnewSharedSecret(""); }} > - + {!hasSharedSecret ? (
- ( - - - - )} - /> ( )} /> -
-
+
+
( - + )} />
-
- ( - - - - )} - /> +
+

OR

+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
-
+