diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 00b0fb9b3..7a909cae4 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -48,7 +48,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }); server.route({ - method: "GET", + method: "POST", url: "/public/:id", config: { rateLimit: publicEndpointLimit @@ -57,92 +57,37 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => params: z.object({ id: z.string().uuid() }), - querystring: z.object({ - hashedHex: z.string().min(1) + body: z.object({ + hashedHex: z.string().min(1), + password: z.string().optional() }), response: { - 200: SecretSharingSchema.pick({ - encryptedValue: true, - iv: true, - tag: true, - expiresAt: true, - expiresAfterViews: true, - accessType: true - }).extend({ - orgName: z.string().optional() + 200: z.object({ + isPasswordProtected: z.boolean(), + secret: SecretSharingSchema.pick({ + encryptedValue: true, + iv: true, + tag: true, + expiresAt: true, + expiresAfterViews: true, + accessType: true + }) + .extend({ + orgName: z.string().optional() + }) + .optional() }) } }, handler: async (req) => { - const sharedSecret = await req.server.services.secretSharing.getPasswordlessSecretByID({ + const sharedSecret = await req.server.services.secretSharing.getSharedSecretById({ sharedSecretId: req.params.id, - hashedHex: req.query.hashedHex, + hashedHex: req.body.hashedHex, + password: req.body.password, orgId: req.permission?.orgId }); - if (!sharedSecret) return undefined; - - return { - encryptedValue: sharedSecret.encryptedValue, - iv: sharedSecret.iv, - tag: sharedSecret.tag, - expiresAt: sharedSecret.expiresAt, - expiresAfterViews: sharedSecret.expiresAfterViews, - accessType: sharedSecret.accessType, - orgName: sharedSecret.orgName - }; - } - }); - - server.route({ - method: "POST", - url: "/public/:id/validate", - config: { - rateLimit: publicEndpointLimit - }, - schema: { - params: z.object({ - id: z.string().uuid() - }), - body: z.object({ - password: z.string().min(1), - hashedHex: z.string() - }), - response: { - 200: SecretSharingSchema.pick({ - encryptedValue: true, - iv: true, - tag: true, - expiresAt: true, - expiresAfterViews: true, - accessType: true - }).extend({ - orgName: z.string().optional() - }) - } - }, - handler: async (req) => { - const { id } = req.params; - const { password, hashedHex } = req.body; - - const sharedSecret = await req.server.services.secretSharing.getValidatedSecretByID({ - sharedSecretId: id, - hashedHex, - orgId: req.permission?.orgId, - password - }); - - if (!sharedSecret) return undefined; - - return { - encryptedValue: sharedSecret.encryptedValue, - iv: sharedSecret.iv, - tag: sharedSecret.tag, - expiresAt: sharedSecret.expiresAt, - expiresAfterViews: sharedSecret.expiresAfterViews, - accessType: sharedSecret.accessType, - orgName: sharedSecret.orgName - }; + return sharedSecret; } }); diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 6961a9e6d..6133db559 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,15 +1,9 @@ import bcrypt from "bcrypt"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { - BadRequestError, - ForbiddenRequestError, - InternalServerError, - NotFoundError, - UnauthorizedError -} from "@app/lib/errors"; -import { SecretSharingAccessType } from "@app/lib/types"; import { TSecretSharing } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { SecretSharingAccessType } from "@app/lib/types"; import { TOrgDALFactory } from "../org/org-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal"; @@ -18,8 +12,7 @@ import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TGetActiveSharedSecretByIdDTO, - TGetSharedSecretsDTO, - TValidateActiveSharedSecretDTO + TGetSharedSecretsDTO } from "./secret-sharing-types"; type TSecretSharingServiceFactoryDep = { @@ -169,10 +162,39 @@ export const secretSharingServiceFactory = ({ }; }; - /** Checks if secret is expired and throws error if true */ - const checkIfSecretIsExpired = async (sharedSecret: TSecretSharing, sharedSecretId: string) => { - const { expiresAt, expiresAfterViews } = sharedSecret; + const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => { + const { expiresAfterViews } = sharedSecret; + if (expiresAfterViews) { + // decrement view count if view count expiry set + await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } }); + } + + await secretSharingDAL.updateById(sharedSecretId, { + lastViewedAt: new Date() + }); + }; + + /** Get's passwordless secret. validates all secret's requested (must be fresh). */ + const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => { + const sharedSecret = await secretSharingDAL.findOne({ + id: sharedSecretId, + hashedHex + }); + if (!sharedSecret) + throw new NotFoundError({ + message: "Shared secret not found" + }); + + const { accessType, expiresAt, expiresAfterViews } = sharedSecret; + + const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; + + if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) + throw new UnauthorizedError(); + + // all secrets pass through here, meaning we check if its expired first and then check if it needs verification + // or can be safely sent to the client. if (expiresAt !== null && expiresAt < new Date()) { // check lifetime expiry await secretSharingDAL.softDeleteById(sharedSecretId); @@ -188,97 +210,30 @@ export const secretSharingServiceFactory = ({ message: "Access denied: Secret has expired by view count" }); } - }; - const decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => { - const { expiresAfterViews } = sharedSecret; - - if (expiresAfterViews) { - // decrement view count if view count expiry set - await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } }); + const isPasswordProtected = Boolean(sharedSecret.password); + const hasProvidedPassword = Boolean(password); + if (isPasswordProtected) { + if (hasProvidedPassword) { + const isMatch = await bcrypt.compare(password as string, sharedSecret.password as string); + if (!isMatch) throw new UnauthorizedError({ message: "Invalid credentials" }); + } else { + return { isPasswordProtected }; + } } - await secretSharingDAL.updateById(sharedSecretId, { - lastViewedAt: new Date() - }); - }; - - /** Get's passwordless secret. validates all secret's requested (must be fresh). */ - const getPasswordlessSecretByID = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => { - const sharedSecret = await secretSharingDAL.findOne({ - id: sharedSecretId, - hashedHex - }); - if (!sharedSecret) - throw new NotFoundError({ - message: "Shared secret not found" - }); - - const { accessType } = sharedSecret; - - const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; - - if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) - throw new UnauthorizedError(); - - // all secrets pass through here, meaning we check if its expired first and then check if it needs verification - // or can be safely sent to the client. - await checkIfSecretIsExpired(sharedSecret, sharedSecretId); - - if (sharedSecret.password !== null) return undefined; - // decrement when we are sure the user will view secret. - await decrementSecretViewCount(sharedSecret, sharedSecretId); + await $decrementSecretViewCount(sharedSecret, sharedSecretId); return { - ...sharedSecret, - orgName: - sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId - ? orgName - : undefined - }; - }; - - /** Get's the requested secret if password passed is valid */ - const getValidatedSecretByID = async ({ - sharedSecretId, - hashedHex, - orgId, - password - }: TValidateActiveSharedSecretDTO) => { - const sharedSecret = await secretSharingDAL.findOne({ - id: sharedSecretId, - hashedHex - }); - if (!sharedSecret) - throw new NotFoundError({ - message: "Shared secret not found" - }); - - const { accessType } = sharedSecret; - - const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; - - if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) - throw new UnauthorizedError(); - - if (!sharedSecret.password) - throw new InternalServerError({ - message: "Something went wrong" - }); - - const isMatch = await bcrypt.compare(password, sharedSecret.password); - if (!isMatch) return undefined; - - // reduce the view count when the password matches (will be returned to the client). - await decrementSecretViewCount(sharedSecret, sharedSecretId); - - return { - ...sharedSecret, - orgName: - sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId - ? orgName - : undefined + isPasswordProtected, + secret: { + ...sharedSecret, + orgName: + sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId + ? orgName + : undefined + } }; }; @@ -295,7 +250,6 @@ export const secretSharingServiceFactory = ({ createPublicSharedSecret, getSharedSecrets, deleteSharedSecretById, - getPasswordlessSecretByID, - getValidatedSecretByID + getSharedSecretById }; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index e96a68e64..794d99a33 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -33,6 +33,7 @@ export type TGetActiveSharedSecretByIdDTO = { sharedSecretId: string; hashedHex: string; orgId?: string; + password?: string; }; export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {