diff --git a/apps/web/app/api/auth/forgot-password/route.ts b/apps/web/app/api/auth/forgot-password/route.ts index 064dfce857..9611abdf4b 100644 --- a/apps/web/app/api/auth/forgot-password/route.ts +++ b/apps/web/app/api/auth/forgot-password/route.ts @@ -7,6 +7,7 @@ import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetReq import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { emailSchema } from "@calcom/lib/emailSchema"; import prisma from "@calcom/prisma"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; async function handler(req: NextRequest) { const body = await parseRequestData(req); @@ -28,7 +29,7 @@ async function handler(req: NextRequest) { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: ip, + identifier: piiHasher.hash(ip), }); try { diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 2ef22ba9d8..b60363fd7d 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -7,6 +7,7 @@ import getIP from "@calcom/lib/getIP"; import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; import { CreationSource } from "@calcom/prisma/enums"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; async function handler(req: NextApiRequest & { userId?: number }) { const userIp = getIP(req); @@ -20,7 +21,7 @@ async function handler(req: NextApiRequest & { userId?: number }) { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: userIp, + identifier: piiHasher.hash(userIp), }); const session = await getServerSession({ req }); diff --git a/apps/web/pages/api/book/instant-event.ts b/apps/web/pages/api/book/instant-event.ts index 5a42e81ee6..91bb97747a 100644 --- a/apps/web/pages/api/book/instant-event.ts +++ b/apps/web/pages/api/book/instant-event.ts @@ -6,13 +6,14 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE import getIP from "@calcom/lib/getIP"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; import { CreationSource } from "@calcom/prisma/enums"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; async function handler(req: NextApiRequest & { userId?: number }) { const userIp = getIP(req); await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: `instant.event-${userIp}`, + identifier: `instant.event-${piiHasher.hash(userIp)}`, }); const session = await getServerSession({ req }); diff --git a/apps/web/pages/api/book/recurring-event.ts b/apps/web/pages/api/book/recurring-event.ts index 840a1da936..69150c016b 100644 --- a/apps/web/pages/api/book/recurring-event.ts +++ b/apps/web/pages/api/book/recurring-event.ts @@ -7,6 +7,7 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE import getIP from "@calcom/lib/getIP"; import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; // @TODO: Didn't look at the contents of this function in order to not break old booking page. @@ -37,7 +38,7 @@ async function handler(req: NextApiRequest & RequestMeta) { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: userIp, + identifier: piiHasher.hash(userIp), }); const session = await getServerSession({ req }); /* To mimic API behavior and comply with types */ diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index a083b62c78..51b76b7eb6 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -48,6 +48,7 @@ import { dub } from "./dub"; import { isPasswordValid } from "./isPasswordValid"; import CalComAdapter from "./next-auth-custom-adapter"; import { verifyPassword } from "./verifyPassword"; +import { hashEmail } from "@calcom/lib/server/PiiHasher"; const log = logger.getSubLogger({ prefix: ["next-auth-options"] }); const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "{}"; @@ -131,7 +132,7 @@ const providers: Provider[] = [ } await checkRateLimitAndThrowError({ - identifier: user.email, + identifier: hashEmail(user.email), }); if (!user.password?.hash && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) { diff --git a/packages/features/auth/lib/verifyEmail.ts b/packages/features/auth/lib/verifyEmail.ts index fff51e469a..192b7ba463 100644 --- a/packages/features/auth/lib/verifyEmail.ts +++ b/packages/features/auth/lib/verifyEmail.ts @@ -13,6 +13,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; import { prisma } from "@calcom/prisma"; +import { hashEmail } from "@calcom/lib/server/PiiHasher"; const log = logger.getSubLogger({ prefix: [`[[Auth] `] }); @@ -54,7 +55,7 @@ export const sendEmailVerification = async ({ await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: email, + identifier: hashEmail(email), }); await prisma.verificationToken.create({ @@ -142,7 +143,7 @@ export const sendChangeOfEmailVerification = async ({ user, language }: ChangeOf await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: user.emailFrom, + identifier: hashEmail(user.emailFrom), }); await prisma.verificationToken.create({ diff --git a/packages/lib/server/PiiHasher.test.ts b/packages/lib/server/PiiHasher.test.ts new file mode 100644 index 0000000000..a233f9f522 --- /dev/null +++ b/packages/lib/server/PiiHasher.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest"; +import { hashEmail, Md5PiiHasher } from "./PiiHasher"; + +describe("PII Hasher Test Suite", () => { + + const hasher = new Md5PiiHasher("test-salt"); + + it("can hash email addresses", async () => { + const email = "sensitive_data@example.com"; + const hashedEmail = hashEmail(email, hasher); + expect(hashedEmail).toBe("2e74ca9edc8add1709b0d049aa2a0959@example.com"); + }); + + it("can hash PII with saltyMd5", async () => { + const pii = "sensitive_data"; + const hashedPii = hasher.hash(pii); + expect(hashedPii).toBe("2e74ca9edc8add1709b0d049aa2a0959"); + }); + + it("handles hashing with different salt", () => { + const differentHasher = new Md5PiiHasher("different-salt"); + const pii = "sensitive_data"; + const hashedPii = differentHasher.hash(pii); + expect(hashedPii).not.toBe(hasher.hash(pii)); + }); +}); diff --git a/packages/lib/server/PiiHasher.ts b/packages/lib/server/PiiHasher.ts new file mode 100644 index 0000000000..95d9cc8647 --- /dev/null +++ b/packages/lib/server/PiiHasher.ts @@ -0,0 +1,20 @@ +import { createHash } from "crypto"; + +export interface PiiHasher { + hash(input: string): string; +} + +export class Md5PiiHasher implements PiiHasher { + constructor(private readonly salt: string) {} + hash(input: string) { + return createHash("md5").update(this.salt + input).digest("hex"); + } +} + +export const piiHasher: PiiHasher = new Md5PiiHasher(process.env.CALENDSO_ENCRYPTION_KEY!); + +export const hashEmail = (email: string, hasher: PiiHasher = piiHasher): string => { + const [localPart, domain] = email.split("@"); + // Simple hash function for email, can be replaced with a more complex one if needed + return hasher.hash(localPart) + "@" + domain; +} \ No newline at end of file diff --git a/packages/sms/sms-manager.ts b/packages/sms/sms-manager.ts index a7119bb884..1c5c3edc79 100644 --- a/packages/sms/sms-manager.ts +++ b/packages/sms/sms-manager.ts @@ -4,6 +4,7 @@ import { sendSmsOrFallbackEmail } from "@calcom/features/ee/workflows/lib/remind import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError"; import { SENDER_ID } from "@calcom/lib/constants"; import isSmsCalEmail from "@calcom/lib/isSmsCalEmail"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; import { TimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; @@ -32,7 +33,7 @@ const handleSendingSMS = async ({ ? `handleSendingSMS:team:${teamId}` : organizerUserId ? `handleSendingSMS:user:${organizerUserId}` - : `handleSendingSMS:user:${reminderPhone}`, + : `handleSendingSMS:user:${piiHasher.hash(reminderPhone)}`, rateLimitingType: "sms", }); diff --git a/packages/trpc/server/routers/loggedInViewer/addSecondaryEmail.handler.ts b/packages/trpc/server/routers/loggedInViewer/addSecondaryEmail.handler.ts index 8b1564a40e..d6ffc95935 100644 --- a/packages/trpc/server/routers/loggedInViewer/addSecondaryEmail.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/addSecondaryEmail.handler.ts @@ -22,7 +22,7 @@ export const addSecondaryEmailHandler = async ({ ctx, input }: AddSecondaryEmail await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: `addSecondaryEmail.${user.email}`, + identifier: `addSecondaryEmail.${user.id}`, }); const existingPrimaryEmail = await prisma.user.findUnique({ diff --git a/packages/trpc/server/routers/viewer/auth/resendVerifyEmail.handler.ts b/packages/trpc/server/routers/viewer/auth/resendVerifyEmail.handler.ts index 2768037149..81d05c8f9d 100644 --- a/packages/trpc/server/routers/viewer/auth/resendVerifyEmail.handler.ts +++ b/packages/trpc/server/routers/viewer/auth/resendVerifyEmail.handler.ts @@ -21,11 +21,10 @@ const log = logger.getSubLogger({ prefix: [`[[Auth] `] }); export const resendVerifyEmail = async ({ input, ctx }: ResendEmailOptions) => { let emailToVerify = ctx.user.email; - const identifer = emailToVerify; await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: `resendVerifyEmail.${identifer}`, + identifier: `resendVerifyEmail.${ctx.user.id}`, }); let emailVerified = Boolean(ctx.user.emailVerified); diff --git a/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts b/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts index 1cca32bbcc..9681bfc293 100644 --- a/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts +++ b/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts @@ -6,6 +6,7 @@ import getIP from "@calcom/lib/getIP"; import type { TRPCContext } from "../../../createContext"; import type { TSendVerifyEmailCodeSchema } from "./sendVerifyEmailCode.schema"; +import { hashEmail, piiHasher } from "@calcom/lib/server/PiiHasher"; type SendVerifyEmailCode = { input: TSendVerifyEmailCodeSchema; @@ -13,11 +14,11 @@ type SendVerifyEmailCode = { }; export const sendVerifyEmailCodeHandler = async ({ input, req }: SendVerifyEmailCode) => { - const identifer = req ? getIP(req as NextApiRequest) : input.email; + const identifier = req ? piiHasher.hash(getIP(req as NextApiRequest)) : hashEmail(input.email); await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: `emailVerifyByCode.${identifer}`, + identifier: `emailVerifyByCode.${identifier}`, }); const email = await sendEmailVerificationByCode({ diff --git a/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts b/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts index f234a8b6ea..1aa8571720 100644 --- a/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts @@ -8,6 +8,7 @@ import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; +import { hashEmail } from "@calcom/lib/server/PiiHasher"; type VerifyCodeOptions = { ctx: { @@ -34,7 +35,7 @@ export const verifyCodeHandler = async ({ ctx, input }: VerifyCodeOptions) => { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: email, + identifier: hashEmail(email), }); const secret = createHash("md5") diff --git a/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts b/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts index 640891fb48..1b630e4cd4 100644 --- a/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts @@ -1,5 +1,4 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; -import logger from "@calcom/lib/logger"; import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams"; import { TeamService } from "@calcom/lib/server/service/teamService"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; @@ -8,7 +7,6 @@ import { TRPCError } from "@trpc/server"; import type { TRemoveMemberInputSchema } from "./removeMember.schema"; -const log = logger.getSubLogger({ prefix: ["viewer/teams/removeMember.handler"] }); type RemoveMemberOptions = { ctx: { user: NonNullable; @@ -19,7 +17,7 @@ type RemoveMemberOptions = { export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) => { await checkRateLimitAndThrowError({ - identifier: `removeMember.${ctx.sourceIp}`, + identifier: `removeMember.${ctx.user.id}`, }); const { memberIds, teamIds, isOrg } = input;