From d5dd2e8bfd2abd706afd22257b7776d5cb69b2d0 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 30 Jul 2025 04:25:27 +0400 Subject: [PATCH] feat: srp removal --- .../migrations/20250723220500_remove-srp.ts | 18 ++ .../access-approval-policies-environments.ts | 25 ++ backend/src/db/schemas/projects.ts | 3 +- .../secret-approval-policies-environments.ts | 25 ++ .../src/db/schemas/user-encryption-keys.ts | 12 +- backend/src/db/seed-data.ts | 4 + backend/src/db/seeds/1-user.ts | 7 +- backend/src/db/seeds/3-project.ts | 12 + backend/src/db/seeds/5-machine-identity.ts | 8 + backend/src/ee/services/group/group-fns.ts | 26 ++- .../src/ee/services/group/group-service.ts | 2 +- backend/src/ee/services/group/group-types.ts | 6 +- .../ldap-config/ldap-config-service.ts | 2 +- .../ee/services/oidc/oidc-config-service.ts | 2 +- backend/src/ee/services/scim/scim-service.ts | 2 +- backend/src/lib/crypto/secret-encryption.ts | 18 +- backend/src/lib/crypto/srp.ts | 18 ++ backend/src/server/routes/index.ts | 4 - .../server/routes/v1/organization-router.ts | 2 +- .../src/server/routes/v1/password-router.ts | 110 --------- .../src/server/routes/v1/project-router.ts | 2 +- backend/src/server/routes/v1/user-router.ts | 40 +--- backend/src/server/routes/v2/mfa-router.ts | 14 +- .../server/routes/v2/organization-router.ts | 2 +- .../src/server/routes/v2/project-router.ts | 2 +- backend/src/server/routes/v3/login-router.ts | 75 +++++- backend/src/server/routes/v3/signup-router.ts | 18 -- backend/src/services/auth/auth-fns.ts | 59 ++++- .../src/services/auth/auth-login-service.ts | 120 ++++++---- .../services/auth/auth-password-service.ts | 214 +----------------- .../src/services/auth/auth-password-type.ts | 23 -- .../src/services/auth/auth-signup-service.ts | 212 ++--------------- backend/src/services/auth/auth-signup-type.ts | 18 -- .../group-project/group-project-service.ts | 16 +- .../services/org-admin/org-admin-service.ts | 36 +-- backend/src/services/org/org-service.ts | 146 +----------- .../services/project-bot/project-bot-fns.ts | 7 + .../services/project-key/project-key-dal.ts | 4 +- backend/src/services/project/project-fns.ts | 4 + backend/src/services/project/project-queue.ts | 12 + .../src/services/project/project-service.ts | 105 +-------- backend/src/services/project/project-types.ts | 2 +- backend/src/services/user/user-service.ts | 22 -- cli/packages/api/api.go | 21 ++ cli/packages/api/model.go | 9 + cli/packages/cmd/login.go | 134 +++-------- frontend/src/components/auth/UserInfoStep.tsx | 143 +++--------- .../utilities/attemptChangePassword.ts | 108 --------- .../components/utilities/attemptCliLogin.ts | 117 +++++----- .../utilities/attemptCliLoginMfa.ts | 109 --------- .../src/components/utilities/attemptLogin.ts | 113 ++++----- .../components/utilities/attemptLoginMfa.ts | 95 -------- .../utilities/cryptography/issueBackupKey.ts | 114 ---------- .../utilities/saveTokenToLocalStorage.ts | 67 ------ frontend/src/hooks/api/auth/queries.tsx | 55 +---- frontend/src/hooks/api/auth/types.ts | 62 ++--- frontend/src/hooks/api/users/queries.tsx | 8 - frontend/src/lib/crypto/index.ts | 47 ---- .../src/pages/admin/SignUpPage/SignUpPage.tsx | 8 - .../components/PasswordStep/PasswordStep.tsx | 7 +- .../PasswordSetupPage/PasswordSetupPage.tsx | 102 ++------- .../auth/SelectOrgPage/SelectOrgSection.tsx | 5 +- .../SignUpInvitePage/SignUpInvitePage.tsx | 165 ++++---------- .../UserInfoSSOStep/UserInfoSSOStep.tsx | 182 +++++---------- .../ChangePasswordSection.tsx | 8 +- .../EmergencyKitSection.tsx | 80 ------- .../components/EmergencyKitSection/index.tsx | 1 - .../PersonalGeneralTab/PersonalGeneralTab.tsx | 8 - 68 files changed, 817 insertions(+), 2410 deletions(-) create mode 100644 backend/src/db/migrations/20250723220500_remove-srp.ts create mode 100644 backend/src/db/schemas/access-approval-policies-environments.ts create mode 100644 backend/src/db/schemas/secret-approval-policies-environments.ts delete mode 100644 frontend/src/components/utilities/attemptChangePassword.ts delete mode 100644 frontend/src/components/utilities/attemptCliLoginMfa.ts delete mode 100644 frontend/src/components/utilities/attemptLoginMfa.ts delete mode 100644 frontend/src/components/utilities/cryptography/issueBackupKey.ts delete mode 100644 frontend/src/components/utilities/saveTokenToLocalStorage.ts delete mode 100644 frontend/src/pages/user/PersonalSettingsPage/components/EmergencyKitSection/EmergencyKitSection.tsx delete mode 100644 frontend/src/pages/user/PersonalSettingsPage/components/EmergencyKitSection/index.tsx diff --git a/backend/src/db/migrations/20250723220500_remove-srp.ts b/backend/src/db/migrations/20250723220500_remove-srp.ts new file mode 100644 index 0000000000..0c6ebd35ea --- /dev/null +++ b/backend/src/db/migrations/20250723220500_remove-srp.ts @@ -0,0 +1,18 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.UserEncryptionKey, (table) => { + table.text("encryptedPrivateKey").nullable().alter(); + table.text("publicKey").nullable().alter(); + table.text("iv").nullable().alter(); + table.text("tag").nullable().alter(); + table.text("salt").nullable().alter(); + table.text("verifier").nullable().alter(); + }); +} + +export async function down(knex: Knex): Promise { + // do nothing for now to avoid breaking down migrations +} diff --git a/backend/src/db/schemas/access-approval-policies-environments.ts b/backend/src/db/schemas/access-approval-policies-environments.ts new file mode 100644 index 0000000000..fa2a859c2a --- /dev/null +++ b/backend/src/db/schemas/access-approval-policies-environments.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AccessApprovalPoliciesEnvironmentsSchema = z.object({ + id: z.string().uuid(), + policyId: z.string().uuid(), + envId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAccessApprovalPoliciesEnvironments = z.infer; +export type TAccessApprovalPoliciesEnvironmentsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TAccessApprovalPoliciesEnvironmentsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 059565a944..08dc1eee03 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -30,7 +30,8 @@ export const ProjectsSchema = z.object({ hasDeleteProtection: z.boolean().default(false).nullable().optional(), secretSharing: z.boolean().default(true), showSnapshotsLegacy: z.boolean().default(false), - defaultProduct: z.string().nullable().optional() + defaultProduct: z.string().nullable().optional(), + secretDetectionIgnoreValues: z.string().array().nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/db/schemas/secret-approval-policies-environments.ts b/backend/src/db/schemas/secret-approval-policies-environments.ts new file mode 100644 index 0000000000..0420fe75dd --- /dev/null +++ b/backend/src/db/schemas/secret-approval-policies-environments.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SecretApprovalPoliciesEnvironmentsSchema = z.object({ + id: z.string().uuid(), + policyId: z.string().uuid(), + envId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TSecretApprovalPoliciesEnvironments = z.infer; +export type TSecretApprovalPoliciesEnvironmentsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TSecretApprovalPoliciesEnvironmentsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/user-encryption-keys.ts b/backend/src/db/schemas/user-encryption-keys.ts index fd9d21a9d5..cfe61fdc57 100644 --- a/backend/src/db/schemas/user-encryption-keys.ts +++ b/backend/src/db/schemas/user-encryption-keys.ts @@ -15,12 +15,12 @@ export const UserEncryptionKeysSchema = z.object({ protectedKey: z.string().nullable().optional(), protectedKeyIV: z.string().nullable().optional(), protectedKeyTag: z.string().nullable().optional(), - publicKey: z.string(), - encryptedPrivateKey: z.string(), - iv: z.string(), - tag: z.string(), - salt: z.string(), - verifier: z.string(), + publicKey: z.string().nullable().optional(), + encryptedPrivateKey: z.string().nullable().optional(), + iv: z.string().nullable().optional(), + tag: z.string().nullable().optional(), + salt: z.string().nullable().optional(), + verifier: z.string().nullable().optional(), userId: z.string().uuid(), hashedPassword: z.string().nullable().optional(), serverEncryptedPrivateKey: z.string().nullable().optional(), diff --git a/backend/src/db/seed-data.ts b/backend/src/db/seed-data.ts index 2aee85fb1b..f1a70ca057 100644 --- a/backend/src/db/seed-data.ts +++ b/backend/src/db/seed-data.ts @@ -115,6 +115,10 @@ export const generateUserSrpKeys = async (password: string) => { }; export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => { + if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) { + throw new Error("User encrypted private key not found"); + } + const derivedKey = await argon2.hash(password, { salt: Buffer.from(user.salt), memoryCost: 65536, diff --git a/backend/src/db/seeds/1-user.ts b/backend/src/db/seeds/1-user.ts index 5c6245382f..43ce4dadfc 100644 --- a/backend/src/db/seeds/1-user.ts +++ b/backend/src/db/seeds/1-user.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; -import { crypto } from "@app/lib/crypto"; -import { initLogger } from "@app/lib/logger"; +import { initEnvConfig } from "@app/lib/config/env"; +import { initLogger, logger } from "@app/lib/logger"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { AuthMethod } from "../../services/auth/auth-type"; @@ -17,7 +17,7 @@ export async function seed(knex: Knex): Promise { initLogger(); const superAdminDAL = superAdminDALFactory(knex); - await crypto.initialize(superAdminDAL); + await initEnvConfig(superAdminDAL, logger); await knex(TableName.SuperAdmin).insert([ // eslint-disable-next-line @@ -25,6 +25,7 @@ export async function seed(knex: Knex): Promise { { id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true } ]); // Inserts seed entries + const [user] = await knex(TableName.Users) .insert([ { diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts index 26f96eafb4..bdafe3bcce 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -1,6 +1,9 @@ import { Knex } from "knex"; +import { initEnvConfig } from "@app/lib/config/env"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; +import { initLogger, logger } from "@app/lib/logger"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data"; @@ -17,6 +20,11 @@ export async function seed(knex: Knex): Promise { await knex(TableName.Environment).del(); await knex(TableName.SecretFolder).del(); + initLogger(); + + const superAdminDAL = superAdminDALFactory(knex); + await initEnvConfig(superAdminDAL, logger); + const [project] = await knex(TableName.Project) .insert({ name: seedData1.project.name, @@ -43,6 +51,10 @@ export async function seed(knex: Knex): Promise { const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first(); if (!user) throw new Error("User not found"); + if (!user.publicKey) { + throw new Error("User public key not found"); + } + const userPrivateKey = await getUserPrivateKey(seedData1.password, user); const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey); await knex(TableName.ProjectKeys).insert({ diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index 391f785ecb..ae3d04514a 100644 --- a/backend/src/db/seeds/5-machine-identity.ts +++ b/backend/src/db/seeds/5-machine-identity.ts @@ -1,6 +1,9 @@ import { Knex } from "knex"; +import { initEnvConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; +import { initLogger, logger } from "@app/lib/logger"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; @@ -10,6 +13,11 @@ export async function seed(knex: Knex): Promise { await knex(TableName.Identity).del(); await knex(TableName.IdentityOrgMembership).del(); + initLogger(); + + const superAdminDAL = superAdminDALFactory(knex); + await initEnvConfig(superAdminDAL, logger); + // Inserts seed entries await knex(TableName.Identity).insert([ { diff --git a/backend/src/ee/services/group/group-fns.ts b/backend/src/ee/services/group/group-fns.ts index 436b0b79e0..56f8df6c07 100644 --- a/backend/src/ee/services/group/group-fns.ts +++ b/backend/src/ee/services/group/group-fns.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; +import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors"; @@ -65,6 +65,18 @@ const addAcceptedUsersToGroup = async ({ const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`)); for await (const projectId of projectIds) { + const project = await projectDAL.findById(projectId, tx); + if (!project) { + throw new NotFoundError({ + message: `Failed to find project with ID '${projectId}'` + }); + } + + if (project.version !== ProjectVersion.V1 && project.version !== ProjectVersion.V2) { + // eslint-disable-next-line no-continue + continue; + } + const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`)); if (usersToAddProjectKeyFor.length) { @@ -86,6 +98,12 @@ const addAcceptedUsersToGroup = async ({ }); } + if (!ghostUserLatestKey.sender.publicKey) { + throw new NotFoundError({ + message: `Failed to find project owner's public key in project with ID '${projectId}'` + }); + } + const bot = await projectBotDAL.findOne({ projectId }, tx); if (!bot) { @@ -112,6 +130,12 @@ const addAcceptedUsersToGroup = async ({ }); const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => { + if (!user.publicKey) { + throw new NotFoundError({ + message: `Failed to find user's public key in project with ID '${projectId}'` + }); + } + const { ciphertext: encryptedKey, nonce } = crypto .encryption() .asymmetric() diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 55665c1469..46c9831b08 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = { TUserGroupMembershipDALFactory, "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find" >; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; projectKeyDAL: Pick; permissionService: Pick; diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index 1d7c5fc71f..e91f7a47b4 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -65,7 +65,7 @@ export type TAddUsersToGroup = { userGroupMembershipDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; tx: Knex; }; @@ -78,7 +78,7 @@ export type TAddUsersToGroupByUserIds = { orgDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; tx?: Knex; }; @@ -102,7 +102,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = { >; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; tx?: Knex; }; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index f3aac5b92b..b1a10056c2 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -55,7 +55,7 @@ type TLdapConfigServiceFactoryDep = { groupDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index ca6f67235a..101f744c3a 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -79,7 +79,7 @@ type TOidcConfigServiceFactoryDep = { >; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; auditLogService: Pick; kmsService: Pick; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index ecb900e531..0a1b9db401 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -59,7 +59,7 @@ type TScimServiceFactoryDep = { TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById" | "update" >; - projectDAL: Pick; + projectDAL: Pick; projectMembershipDAL: Pick; groupDAL: Pick< TGroupDALFactory, diff --git a/backend/src/lib/crypto/secret-encryption.ts b/backend/src/lib/crypto/secret-encryption.ts index 7355d4bd69..f22c918ac1 100644 --- a/backend/src/lib/crypto/secret-encryption.ts +++ b/backend/src/lib/crypto/secret-encryption.ts @@ -53,7 +53,7 @@ type DecryptedIntegrationAuths = z.infer type TLatestKey = TProjectKeys & { sender: { - publicKey: string; + publicKey?: string; }; }; @@ -91,6 +91,10 @@ const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: s return results; }; export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => { + if (!latestKey.sender.publicKey) { + throw new Error("Latest key sender public key not found"); + } + const key = crypto.encryption().asymmetric().decrypt({ ciphertext: latestKey.encryptedKey, nonce: latestKey.nonce, @@ -143,6 +147,10 @@ export const decryptSecretVersions = ( privateKey: string, latestKey: TLatestKey ) => { + if (!latestKey.sender.publicKey) { + throw new Error("Latest key sender public key not found"); + } + const key = crypto.encryption().asymmetric().decrypt({ ciphertext: latestKey.encryptedKey, nonce: latestKey.nonce, @@ -195,6 +203,10 @@ export const decryptSecretApprovals = ( privateKey: string, latestKey: TLatestKey ) => { + if (!latestKey.sender.publicKey) { + throw new Error("Latest key sender public key not found"); + } + const key = crypto.encryption().asymmetric().decrypt({ ciphertext: latestKey.encryptedKey, nonce: latestKey.nonce, @@ -247,6 +259,10 @@ export const decryptIntegrationAuths = ( privateKey: string, latestKey: TLatestKey ) => { + if (!latestKey.sender.publicKey) { + throw new Error("Latest key sender public key not found"); + } + const key = crypto.encryption().asymmetric().decrypt({ ciphertext: latestKey.encryptedKey, nonce: latestKey.nonce, diff --git a/backend/src/lib/crypto/srp.ts b/backend/src/lib/crypto/srp.ts index 3f403405e9..2623e5756c 100644 --- a/backend/src/lib/crypto/srp.ts +++ b/backend/src/lib/crypto/srp.ts @@ -4,6 +4,7 @@ import jsrp from "jsrp"; import { TUserEncryptionKeys } from "@app/db/schemas"; import { UserEncryption } from "@app/services/user/user-types"; +import { BadRequestError } from "../errors"; import { crypto, SymmetricKeySize } from "./cryptography"; export const generateSrpServerKey = async (salt: string, verifier: string) => { @@ -127,6 +128,10 @@ export const getUserPrivateKey = async ( > ) => { if (user.encryptionVersion === UserEncryption.V1) { + if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) { + throw new BadRequestError({ message: "User encrypted private key not found" }); + } + return crypto .encryption() .symmetric() @@ -138,12 +143,25 @@ export const getUserPrivateKey = async ( keySize: SymmetricKeySize.Bits128 }); } + // still used for legacy things if ( user.encryptionVersion === UserEncryption.V2 && user.protectedKey && user.protectedKeyIV && user.protectedKeyTag ) { + if ( + !user.salt || + !user.protectedKey || + !user.protectedKeyIV || + !user.protectedKeyTag || + !user.encryptedPrivateKey || + !user.iv || + !user.tag + ) { + throw new BadRequestError({ message: "User encrypted private key not found" }); + } + const derivedKey = await argon2.hash(password, { salt: Buffer.from(user.salt), memoryCost: 65536, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index ea4cf96763..7578da4409 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -761,7 +761,6 @@ export const registerRoutes = async ( orgRoleDAL, permissionService, orgDAL, - projectBotDAL, incidentContactDAL, tokenService, projectUserAdditionalPrivilegeDAL, @@ -1114,11 +1113,9 @@ export const registerRoutes = async ( projectBotService, identityProjectDAL, identityOrgMembershipDAL, - projectKeyDAL, userDAL, projectEnvDAL, orgDAL, - orgService, projectMembershipDAL, projectRoleDAL, folderDAL, @@ -1138,7 +1135,6 @@ export const registerRoutes = async ( identityProjectMembershipRoleDAL, keyStore, kmsService, - projectBotDAL, certificateTemplateDAL, projectSlackConfigDAL, slackIntegrationDAL, diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index e1669c784d..323354bc1d 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -247,7 +247,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { lastName: true, id: true, superAdmin: true - }).merge(z.object({ publicKey: z.string().nullable() })) + }).merge(z.object({ publicKey: z.string().nullable().optional() })) }) ) .omit({ createdAt: true, updatedAt: true }) diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index 3396cebe96..72d20db57f 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -9,73 +9,6 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { UserEncryption } from "@app/services/user/user-types"; export const registerPasswordRouter = async (server: FastifyZodProvider) => { - server.route({ - method: "POST", - url: "/srp1", - config: { - rateLimit: authRateLimit - }, - schema: { - body: z.object({ - clientPublicKey: z.string().trim() - }), - response: { - 200: z.object({ - serverPublicKey: z.string(), - salt: z.string() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const { salt, serverPublicKey } = await server.services.password.generateServerPubKey( - req.permission.id, - req.body.clientPublicKey - ); - return { salt, serverPublicKey }; - } - }); - - server.route({ - method: "POST", - url: "/change-password", - config: { - rateLimit: authRateLimit - }, - schema: { - body: z.object({ - clientProof: z.string().trim(), - protectedKey: z.string().trim(), - protectedKeyIV: z.string().trim(), - protectedKeyTag: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - encryptedPrivateKeyIV: z.string().trim(), - encryptedPrivateKeyTag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim(), - password: z.string().trim() - }), - response: { - 200: z.object({ - message: z.string() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req, res) => { - const appCfg = getConfig(); - await server.services.password.changePassword({ ...req.body, userId: req.permission.id }); - - void res.cookie("jid", "", { - httpOnly: true, - path: "/", - sameSite: "strict", - secure: appCfg.HTTPS_ENABLED - }); - return { message: "Successfully changed password" }; - } - }); - server.route({ method: "POST", url: "/email/password-reset", @@ -131,41 +64,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { } }); - server.route({ - method: "POST", - url: "/backup-private-key", - config: { - rateLimit: authRateLimit - }, - onRequest: verifyAuth([AuthMode.JWT]), - schema: { - body: z.object({ - clientProof: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - iv: z.string().trim(), - tag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim() - }), - response: { - 200: z.object({ - message: z.string(), - backupPrivateKey: BackupPrivateKeySchema.omit({ verifier: true }) - }) - } - }, - handler: async (req) => { - const token = validateSignUpAuthorization(req.headers.authorization as string, "", false)!; - const backupPrivateKey = await server.services.password.createBackupPrivateKey({ - ...req.body, - userId: token.userId - }); - if (!backupPrivateKey) throw new Error("Failed to create backup key"); - - return { message: "Successfully updated backup private key", backupPrivateKey }; - } - }); - server.route({ method: "GET", url: "/backup-private-key", @@ -257,14 +155,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - protectedKey: z.string().trim(), - protectedKeyIV: z.string().trim(), - protectedKeyTag: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - encryptedPrivateKeyIV: z.string().trim(), - encryptedPrivateKeyTag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim(), password: z.string().trim(), token: z.string().trim() }), diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 05aade960e..ab27ce7533 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 200: z.object({ publicKeys: z .object({ - publicKey: z.string().optional(), + publicKey: z.string().nullable().optional(), userId: z.string() }) .array() diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index a0c3592f7e..b519b8ac31 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -19,23 +19,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - user: UsersSchema.merge( - UserEncryptionKeysSchema.pick({ - clientPublicKey: true, - serverPrivateKey: true, - encryptionVersion: true, - protectedKey: true, - protectedKeyIV: true, - protectedKeyTag: true, - publicKey: true, - encryptedPrivateKey: true, - iv: true, - tag: true, - salt: true, - verifier: true, - userId: true - }) - ) + user: UsersSchema }) } }, @@ -94,26 +78,6 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { } }); - server.route({ - method: "GET", - url: "/private-key", - config: { - rateLimit: readLimit - }, - schema: { - response: { - 200: z.object({ - privateKey: z.string() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), - handler: async (req) => { - const privateKey = await server.services.user.getUserPrivateKey(req.permission.id); - return { privateKey }; - } - }); - server.route({ method: "GET", url: "/:userId/unlock", diff --git a/backend/src/server/routes/v2/mfa-router.ts b/backend/src/server/routes/v2/mfa-router.ts index 59a3943f77..d8a57d29a7 100644 --- a/backend/src/server/routes/v2/mfa-router.ts +++ b/backend/src/server/routes/v2/mfa-router.ts @@ -97,13 +97,13 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ encryptionVersion: z.number().default(1).nullable().optional(), - protectedKey: z.string().nullable(), - protectedKeyIV: z.string().nullable(), - protectedKeyTag: z.string().nullable(), - publicKey: z.string(), - encryptedPrivateKey: z.string(), - iv: z.string(), - tag: z.string(), + protectedKey: z.string().nullish(), + protectedKeyIV: z.string().nullish(), + protectedKeyTag: z.string().nullish(), + publicKey: z.string().nullish(), + encryptedPrivateKey: z.string().nullish(), + iv: z.string().nullish(), + tag: z.string().nullish(), token: z.string() }) } diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index c17200a306..2c8f3410ac 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -153,7 +153,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { firstName: true, lastName: true, id: true - }).extend({ publicKey: z.string().nullable() }) + }).extend({ publicKey: z.string().nullish() }) }).omit({ createdAt: true, updatedAt: true }) }) } diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 2a883dcb64..b71a744fd0 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 200: ProjectKeysSchema.merge( z.object({ sender: z.object({ - publicKey: z.string() + publicKey: z.string().optional() }) }) ) diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 3a8510f343..5f83b19f48 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -20,8 +20,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - serverPublicKey: z.string(), - salt: z.string() + serverPublicKey: z.string().nullish(), + salt: z.string().nullish() }) } }, @@ -124,14 +124,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - encryptionVersion: z.number().default(1).nullable().optional(), - protectedKey: z.string().nullable(), - protectedKeyIV: z.string().nullable(), - protectedKeyTag: z.string().nullable(), - publicKey: z.string(), - encryptedPrivateKey: z.string(), - iv: z.string(), - tag: z.string(), + encryptionVersion: z.number().default(1).nullish(), + protectedKey: z.string().nullish(), + protectedKeyIV: z.string().nullish(), + protectedKeyTag: z.string().nullish(), + publicKey: z.string().nullish(), + encryptedPrivateKey: z.string().nullish(), + iv: z.string().nullish(), + tag: z.string().nullish(), token: z.string() }) } @@ -181,4 +181,59 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { } as const; } }); + + // New login route that doesn't use SRP + server.route({ + method: "POST", + url: "/login", + config: { + rateLimit: authRateLimit + }, + schema: { + body: z.object({ + email: z.string().trim(), + password: z.string().trim(), + providerAuthToken: z.string().trim().optional(), + captchaToken: z.string().trim().optional() + }), + response: { + 200: z.object({ + accessToken: z.string() + }) + } + }, + handler: async (req, res) => { + const userAgent = req.headers["user-agent"]; + if (!userAgent) throw new Error("user agent header is required"); + + const { tokens, mfaEnabled } = await server.services.login.login({ + email: req.body.email, + password: req.body.password, + ip: req.realIp, + userAgent, + providerAuthToken: req.body.providerAuthToken, + captchaToken: req.body.captchaToken + }); + const appCfg = getConfig(); + + void res.setCookie("jid", tokens.refreshToken, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + addAuthOriginDomainCookie(res); + + void res.cookie("infisical-project-assume-privileges", "", { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED, + maxAge: 0 + }); + + return { accessToken: tokens.accessToken, mfaEnabled }; + } + }); }; diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 391c459a25..736bc13aaf 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -98,15 +98,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { email: z.string().trim(), firstName: z.string().trim(), lastName: z.string().trim().optional(), - protectedKey: z.string().trim(), - protectedKeyIV: z.string().trim(), - protectedKeyTag: z.string().trim(), - publicKey: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - encryptedPrivateKeyIV: z.string().trim(), - encryptedPrivateKeyTag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim(), providerAuthToken: z.string().trim().optional().nullish(), attributionSource: z.string().trim().optional(), password: z.string() @@ -189,15 +180,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { password: z.string(), firstName: z.string().trim(), lastName: z.string().trim().optional(), - protectedKey: z.string().trim(), - protectedKeyIV: z.string().trim(), - protectedKeyTag: z.string().trim(), - publicKey: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - encryptedPrivateKeyIV: z.string().trim(), - encryptedPrivateKeyTag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim(), tokenMetadata: z.string().optional() }), response: { diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index b38275c8b5..d490486cb4 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -1,8 +1,16 @@ +import { TUsers } from "@app/db/schemas"; +import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; -import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; -import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type"; +import { + AuthMethod, + AuthModeProviderJwtTokenPayload, + AuthModeProviderSignUpTokenPayload, + AuthTokenType +} from "./auth-type"; export const validateProviderAuthToken = (providerToken: string, username?: string) => { if (!providerToken) throw new UnauthorizedError(); @@ -97,3 +105,50 @@ export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: } } }; + +export const verifyCaptcha = async (user: TUsers, captchaToken?: string) => { + const appCfg = getConfig(); + if ( + user.consecutiveFailedPasswordAttempts && + user.consecutiveFailedPasswordAttempts >= 10 && + Boolean(appCfg.CAPTCHA_SECRET) + ) { + if (!captchaToken) { + throw new BadRequestError({ + name: "Captcha Required", + message: "Accomplish the required captcha by logging in via Web" + }); + } + + // validate captcha token + const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", { + response: captchaToken, + secret: appCfg.CAPTCHA_SECRET + }); + + if (!response.data.success) { + throw new BadRequestError({ + name: "Invalid Captcha" + }); + } + } +}; + +export const getAuthMethodAndOrgId = (email: string, providerAuthToken?: string) => { + let authMethod = AuthMethod.EMAIL; + let organizationId: string | undefined; + + if (providerAuthToken) { + const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); + + authMethod = decodedProviderToken.authMethod; + if ( + (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) && + decodedProviderToken.orgId + ) { + organizationId = decodedProviderToken.orgId; + } + } + + return { authMethod, organizationId }; +}; diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index aea90fe11e..35fdd78a60 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -4,7 +4,6 @@ import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSc import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; -import { request } from "@app/lib/config/request"; import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; @@ -22,7 +21,8 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { LoginMethod } from "../super-admin/super-admin-types"; import { TTotpServiceFactory } from "../totp/totp-service"; import { TUserDALFactory } from "../user/user-dal"; -import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns"; +import { UserEncryption } from "../user/user-types"; +import { enforceUserLockStatus, getAuthMethodAndOrgId, validateProviderAuthToken, verifyCaptcha } from "./auth-fns"; import { TLoginClientProofDTO, TLoginGenServerPublicKeyDTO, @@ -208,6 +208,10 @@ export const authLoginServiceFactory = ({ throw new Error("Failed to find user"); } + if (!userEnc.salt || !userEnc.verifier) { + throw new BadRequestError({ message: "Salt or verifier not found" }); + } + if ( serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && @@ -247,8 +251,6 @@ export const authLoginServiceFactory = ({ captchaToken, password }: TLoginClientProofDTO) => { - const appCfg = getConfig(); - // akhilmhdh: case sensitive email resolution const usersByUsername = await userDAL.findUserEncKeyByUsername({ username: email @@ -259,44 +261,11 @@ export const authLoginServiceFactory = ({ const user = await userDAL.findById(userEnc.userId); const cfg = getConfig(); - let authMethod = AuthMethod.EMAIL; - let organizationId: string | undefined; + const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken); + await verifyCaptcha(user, captchaToken); - if (providerAuthToken) { - const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); - - authMethod = decodedProviderToken.authMethod; - if ( - (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) && - decodedProviderToken.orgId - ) { - organizationId = decodedProviderToken.orgId; - } - } - - if ( - user.consecutiveFailedPasswordAttempts && - user.consecutiveFailedPasswordAttempts >= 10 && - Boolean(appCfg.CAPTCHA_SECRET) - ) { - if (!captchaToken) { - throw new BadRequestError({ - name: "Captcha Required", - message: "Accomplish the required captcha by logging in via Web" - }); - } - - // validate captcha token - const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", { - response: captchaToken, - secret: appCfg.CAPTCHA_SECRET - }); - - if (!response.data.success) { - throw new BadRequestError({ - name: "Invalid Captcha" - }); - } + if (!userEnc.salt || !userEnc.verifier) { + throw new BadRequestError({ message: "Salt or verifier not found" }); } if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?"); @@ -371,6 +340,72 @@ export const authLoginServiceFactory = ({ return { token, user: userEnc } as const; }; + const login = async ({ + email, + password, + ip, + userAgent, + providerAuthToken, + captchaToken + }: { + email: string; + password: string; + ip: string; + userAgent: string; + providerAuthToken?: string; + captchaToken?: string; + }) => { + const usersByUsername = await userDAL.findUserEncKeyByUsername({ + username: email + }); + const userEnc = + usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0]; + + if (!userEnc) throw new BadRequestError({ message: "User not found" }); + + if (userEnc.encryptionVersion !== UserEncryption.V2) { + throw new BadRequestError({ message: "Legacy encryption scheme not supported", name: "LegacyEncryptionScheme" }); + } + + if (!userEnc.hashedPassword) { + if (userEnc.authMethods?.includes(AuthMethod.EMAIL)) { + throw new BadRequestError({ + message: "Legacy encryption scheme not supported", + name: "LegacyEncryptionScheme" + }); + } + + throw new BadRequestError({ message: "No password found" }); + } + + const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken); + await verifyCaptcha(userEnc, captchaToken); + + if (!(await crypto.hashing().compareHash(password, userEnc.hashedPassword))) { + throw new BadRequestError({ message: "Invalid username or email" }); + } + + const token = await generateUserTokens({ + user: { + ...userEnc, + id: userEnc.userId + }, + ip, + userAgent, + authMethod, + organizationId + }); + + return { + mfaEnabled: userEnc.isMfaEnabled, + tokens: { + accessToken: token.access, + refreshToken: token.refresh + }, + user: userEnc + } as const; + }; + const selectOrganization = async ({ userAgent, authJwtToken, @@ -862,6 +897,7 @@ export const authLoginServiceFactory = ({ resendMfaToken, verifyMfaToken, selectOrganization, - generateUserTokens + generateUserTokens, + login }; }; diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 0dbdfaf796..8f320f6450 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -1,8 +1,5 @@ -import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto/cryptography"; -import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; @@ -16,8 +13,6 @@ import { UserEncryption } from "../user/user-types"; import { TAuthDALFactory } from "./auth-dal"; import { ResetPasswordV2Type, - TChangePasswordDTO, - TCreateBackupPrivateKeyDTO, TResetPasswordV2DTO, TResetPasswordViaBackupKeyDTO, TSetupPasswordViaBackupKeyDTO @@ -40,79 +35,6 @@ export const authPaswordServiceFactory = ({ smtpService, totpConfigDAL }: TAuthPasswordServiceFactoryDep) => { - /* - * Pre setup for pass change with srp protocol - * Gets srp server user salt and server public key - */ - const generateServerPubKey = async (userId: string, clientPublicKey: string) => { - const userEnc = await userDAL.findUserEncKeyByUserId(userId); - if (!userEnc) throw new Error("Failed to find user"); - - const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier); - const userEncKeys = await userDAL.updateUserEncryptionByUserId(userEnc.userId, { - clientPublicKey, - serverPrivateKey: serverSrpKey.privateKey - }); - if (!userEncKeys) throw new Error("Failed to update encryption key"); - return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey }; - }; - - /* - * Change password to new pass - * */ - const changePassword = async ({ - userId, - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - tokenVersionId, - password - }: TChangePasswordDTO) => { - const userEnc = await userDAL.findUserEncKeyByUserId(userId); - if (!userEnc) throw new Error("Failed to find user"); - - await userDAL.updateUserEncryptionByUserId(userEnc.userId, { - serverPrivateKey: null, - clientPublicKey: null - }); - if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?"); - const isValidClientProof = await srpCheckClientProof( - userEnc.salt, - userEnc.verifier, - userEnc.serverPrivateKey, - userEnc.clientPublicKey, - clientProof - ); - if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?"); - - const appCfg = getConfig(); - const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); - await userDAL.updateUserEncryptionByUserId(userId, { - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - salt, - verifier, - serverPrivateKey: null, - clientPublicKey: null, - hashedPassword - }); - - if (tokenVersionId) { - await tokenService.clearTokenSessionById(userEnc.userId, tokenVersionId); - } - }; - /* * Email password reset flow via email. Step 1 send email */ @@ -211,58 +133,17 @@ export const authPaswordServiceFactory = ({ } } - const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS); - - // we need to get the original private key first for v2 - let privateKey: string; - if ( - user.serverEncryptedPrivateKey && - user.serverEncryptedPrivateKeyTag && - user.serverEncryptedPrivateKeyIV && - user.serverEncryptedPrivateKeyEncoding && - user.encryptionVersion === UserEncryption.V2 - ) { - privateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - iv: user.serverEncryptedPrivateKeyIV, - tag: user.serverEncryptedPrivateKeyTag, - ciphertext: user.serverEncryptedPrivateKey, - keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding - }); - } else { + if (user.encryptionVersion !== UserEncryption.V2) { throw new BadRequestError({ message: "Cannot reset password without current credentials or recovery method", name: "Reset password" }); } - const encKeys = await generateUserSrpKeys(user.username, newPassword, { - publicKey: user.publicKey, - privateKey - }); - - const { tag, iv, ciphertext, encoding } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey); + const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS); await userDAL.updateUserEncryptionByUserId(userId, { - hashedPassword: newHashedPassword, - - // srp params - salt: encKeys.salt, - verifier: encKeys.verifier, - - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, - - serverEncryptedPrivateKey: ciphertext, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyEncoding: encoding + hashedPassword: newHashedPassword }); await tokenService.revokeAllMySessions(userId); @@ -313,66 +194,6 @@ export const authPaswordServiceFactory = ({ }); }; - /* - * backup key creation to give user's their access back when lost their password - * this also needs to do the generateServerPubKey function to be executed first - * then only client proof can be verified - * */ - const createBackupPrivateKey = async ({ - clientProof, - encryptedPrivateKey, - salt, - verifier, - iv, - tag, - userId - }: TCreateBackupPrivateKeyDTO) => { - const userEnc = await userDAL.findUserEncKeyByUserId(userId); - if (!userEnc || (userEnc && !userEnc.isAccepted)) { - throw new Error("Failed to find user"); - } - - if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key"); - const isValidClientProff = await srpCheckClientProof( - userEnc.salt, - userEnc.verifier, - userEnc.serverPrivateKey, - userEnc.clientPublicKey, - clientProof - ); - if (!isValidClientProff) throw new Error("failed to create backup key"); - const backup = await authDAL.transaction(async (tx) => { - const backupKey = await authDAL.upsertBackupKey( - userEnc.userId, - { - encryptedPrivateKey, - iv, - tag, - salt, - verifier, - algorithm: SecretEncryptionAlgo.AES_256_GCM, - keyEncoding: SecretKeyEncoding.UTF8 - }, - tx - ); - - await userDAL.updateUserEncryptionByUserId( - userEnc.userId, - { - serverPrivateKey: null, - clientPublicKey: null - }, - tx - ); - return backupKey; - }); - - return backup; - }; - - /* - * Return user back up - * */ const getBackupPrivateKeyOfUser = async (userId: string) => { const user = await userDAL.findUserEncKeyByUserId(userId); if (!user || (user && !user.isAccepted)) { @@ -416,21 +237,7 @@ export const authPaswordServiceFactory = ({ }); }; - const setupPassword = async ( - { - encryptedPrivateKey, - protectedKeyTag, - protectedKey, - protectedKeyIV, - salt, - verifier, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - password, - token - }: TSetupPasswordViaBackupKeyDTO, - actor: OrgServiceActor - ) => { + const setupPassword = async ({ password, token }: TSetupPasswordViaBackupKeyDTO, actor: OrgServiceActor) => { try { await tokenService.validateTokenForUser({ type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP, @@ -466,15 +273,7 @@ export const authPaswordServiceFactory = ({ await userDAL.updateUserEncryptionByUserId( actor.id, { - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - salt, - verifier, + encryptionVersion: UserEncryption.V2, hashedPassword, serverPrivateKey: null, clientPublicKey: null @@ -487,12 +286,9 @@ export const authPaswordServiceFactory = ({ }; return { - generateServerPubKey, - changePassword, resetPasswordByBackupKey, sendPasswordResetEmail, verifyPasswordResetEmail, - createBackupPrivateKey, getBackupPrivateKeyOfUser, sendPasswordSetupEmail, setupPassword, diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts index b3b14c3b41..ceb70d4118 100644 --- a/backend/src/services/auth/auth-password-type.ts +++ b/backend/src/services/auth/auth-password-type.ts @@ -1,18 +1,3 @@ -export type TChangePasswordDTO = { - userId: string; - clientProof: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; - tokenVersionId?: string; - password: string; -}; - export enum ResetPasswordV2Type { Recovery = "recovery", LoggedInReset = "logged-in-reset" @@ -39,14 +24,6 @@ export type TResetPasswordViaBackupKeyDTO = { }; export type TSetupPasswordViaBackupKeyDTO = { - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; password: string; token: string; }; diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 96cf4121aa..7bc3a5ef68 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -1,11 +1,10 @@ -import { OrgMembershipStatus, SecretKeyEncoding, TableName } from "@app/db/schemas"; +import { OrgMembershipStatus, TableName } from "@app/db/schemas"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { getMinExpiresIn } from "@app/lib/fn"; import { isDisposableEmail } from "@app/lib/validator"; @@ -41,7 +40,7 @@ type TAuthSignupDep = { | "findUserGroupMembershipsInProject" >; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; groupProjectDAL: Pick; orgService: Pick; @@ -147,17 +146,8 @@ export const authSignupServiceFactory = ({ firstName, lastName, providerAuthToken, - salt, - verifier, - publicKey, - protectedKey, - protectedKeyIV, - protectedKeyTag, organizationName, // attributionSource, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, ip, userAgent, authorization, @@ -191,98 +181,18 @@ export const authSignupServiceFactory = ({ } const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); - const privateKey = await getUserPrivateKey(password, { - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - encryptionVersion: UserEncryption.V2 - }); - const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey); const updateduser = await authDAL.transaction(async (tx) => { const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); if (!us) throw new Error("User not found"); - const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx); - let userEncKey; - // below condition is true means this is system generated credentials - // the private key is actually system generated password - // thus we will re-encrypt the system generated private key with the new password - // akhilmhdh: you may find this like why? The reason is simple we are moving away from e2ee and these are pieces of it - // without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things - if ( - systemGeneratedUserEncryptionKey && - !systemGeneratedUserEncryptionKey.hashedPassword && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding - ) { - // get server generated password - const serverGeneratedPassword = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV, - tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag, - ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey, - keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding - }); - const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, { - ...systemGeneratedUserEncryptionKey - }); - const encKeys = await generateUserSrpKeys(email, password, { - publicKey: systemGeneratedUserEncryptionKey.publicKey, - privateKey: serverGeneratedPrivateKey - }); - // now reencrypt server generated key with user provided password - userEncKey = await userDAL.upsertUserEncryptionKey( - us.id, - { - encryptionVersion: UserEncryption.V2, - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - publicKey: encKeys.publicKey, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, - salt: encKeys.salt, - verifier: encKeys.verifier, - hashedPassword, - serverEncryptedPrivateKeyEncoding: encoding, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKey: ciphertext - }, - tx - ); - } else { - userEncKey = await userDAL.upsertUserEncryptionKey( - us.id, - { - encryptionVersion: UserEncryption.V2, - salt, - verifier, - publicKey, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - hashedPassword, - serverEncryptedPrivateKeyEncoding: encoding, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKey: ciphertext - }, - tx - ); - } + const userEncKey = await userDAL.upsertUserEncryptionKey( + us.id, + { + encryptionVersion: UserEncryption.V2, + hashedPassword + }, + tx + ); // If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it if ( @@ -400,19 +310,10 @@ export const authSignupServiceFactory = ({ const completeAccountInvite = async ({ email, ip, - salt, password, - verifier, firstName, - publicKey, userAgent, lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, authorization }: TCompleteAccountInviteDTO) => { const sanitizedEmail = email.trim().toLowerCase(); @@ -437,94 +338,17 @@ export const authSignupServiceFactory = ({ const appCfg = getConfig(); const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); - const privateKey = await getUserPrivateKey(password, { - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - encryptionVersion: 2 - }); - const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey); const updateduser = await authDAL.transaction(async (tx) => { const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); if (!us) throw new Error("User not found"); - const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx); - let userEncKey; - // this means this is system generated credentials - // now replace the private key - if ( - systemGeneratedUserEncryptionKey && - !systemGeneratedUserEncryptionKey.hashedPassword && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding - ) { - // get server generated password - const serverGeneratedPassword = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV, - tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag, - ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey, - keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding - }); - const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, { - ...systemGeneratedUserEncryptionKey - }); - const encKeys = await generateUserSrpKeys(sanitizedEmail, password, { - publicKey: systemGeneratedUserEncryptionKey.publicKey, - privateKey: serverGeneratedPrivateKey - }); - // now reencrypt server generated key with user provided password - userEncKey = await userDAL.upsertUserEncryptionKey( - us.id, - { - encryptionVersion: 2, - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - publicKey: encKeys.publicKey, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, - salt: encKeys.salt, - verifier: encKeys.verifier, - hashedPassword, - serverEncryptedPrivateKeyEncoding: encoding, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKey: ciphertext - }, - tx - ); - } else { - userEncKey = await userDAL.upsertUserEncryptionKey( - us.id, - { - encryptionVersion: UserEncryption.V2, - salt, - verifier, - publicKey, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - hashedPassword, - serverEncryptedPrivateKeyEncoding: encoding, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKey: ciphertext - }, - tx - ); - } + const userEncKey = await userDAL.upsertUserEncryptionKey( + us.id, + { + encryptionVersion: 2, + hashedPassword + }, + tx + ); const updatedMembersips = await orgDAL.updateMembership( { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, diff --git a/backend/src/services/auth/auth-signup-type.ts b/backend/src/services/auth/auth-signup-type.ts index 8bbf302c56..5c4fdc3c95 100644 --- a/backend/src/services/auth/auth-signup-type.ts +++ b/backend/src/services/auth/auth-signup-type.ts @@ -3,15 +3,6 @@ export type TCompleteAccountSignupDTO = { password: string; firstName: string; lastName?: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; organizationName?: string; providerAuthToken?: string | null; attributionSource?: string | undefined; @@ -26,15 +17,6 @@ export type TCompleteAccountInviteDTO = { password: string; firstName: string; lastName?: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; ip: string; userAgent: string; authorization: string; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 50a08e94ff..838d894375 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TGroups } from "@app/db/schemas"; import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types"; import { constructPermissionErrorMessage, @@ -188,7 +188,7 @@ export const groupProjectServiceFactory = ({ // other groups that are in the project const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx); - if (groupMembers.length) { + if (groupMembers.length && (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2)) { const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx); if (!ghostUser) { @@ -205,6 +205,12 @@ export const groupProjectServiceFactory = ({ }); } + if (!ghostUserLatestKey.sender.publicKey) { + throw new NotFoundError({ + message: `Failed to find project owner's latest key in project with name ${project.name}` + }); + } + const bot = await projectBotDAL.findOne({ projectId: project.id }, tx); if (!bot) { @@ -231,6 +237,12 @@ export const groupProjectServiceFactory = ({ }); const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => { + if (!publicKey) { + throw new NotFoundError({ + message: `Failed to find user's public key in project with name ${project.name}` + }); + } + const { ciphertext: encryptedKey, nonce } = crypto .encryption() .asymmetric() diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 6640412c37..60f46dabb6 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -1,13 +1,11 @@ import { ForbiddenError } from "@casl/ability"; -import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TProjectDALFactory } from "../project/project-dal"; -import { assignWorkspaceKeysToMembers } from "../project/project-fns"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; @@ -83,7 +81,7 @@ export const orgAdminServiceFactory = ({ actorAuthMethod, projectId }: TAccessProjectDTO) => { - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -144,29 +142,9 @@ export const orgAdminServiceFactory = ({ }); } - const botPrivateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - keyEncoding: bot.keyEncoding as SecretKeyEncoding, - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey - }); - const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId); if (!userEncryptionKey) throw new NotFoundError({ message: `User encryption key for user with ID '${actorId}' not found` }); - const [newWsMember] = assignWorkspaceKeysToMembers({ - decryptKey: ghostUserLatestKey, - userPrivateKey: botPrivateKey, - members: [ - { - orgMembershipId: membership.id, - userPublicKey: userEncryptionKey.publicKey - } - ] - }); const updatedMembership = await projectMembershipDAL.transaction(async (tx) => { const newProjectMembership = await projectMembershipDAL.create( @@ -181,16 +159,6 @@ export const orgAdminServiceFactory = ({ tx ); - await projectKeyDAL.create( - { - encryptedKey: newWsMember.workspaceEncryptedKey, - nonce: newWsMember.workspaceEncryptedNonce, - senderId: ghostUser.id, - receiverId: actorId, - projectId - }, - tx - ); return newProjectMembership; }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 5f60bfe1ed..b0b97a96fb 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -8,7 +8,6 @@ import { OrgMembershipStatus, ProjectMembershipRole, ProjectVersion, - SecretKeyEncoding, TableName, TProjectMemberships, TProjectUserMembershipRolesInsert, @@ -58,8 +57,6 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; import { TProjectDALFactory } from "../project/project-dal"; -import { assignWorkspaceKeysToMembers, createProjectKey } from "../project/project-fns"; -import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; @@ -130,7 +127,6 @@ type TOrgServiceFactoryDep = { >; projectUserAdditionalPrivilegeDAL: Pick; projectRoleDAL: Pick; - projectBotDAL: Pick; projectUserMembershipRoleDAL: Pick; projectBotService: Pick; loginService: Pick; @@ -162,7 +158,6 @@ export const orgServiceFactory = ({ projectRoleDAL, samlConfigDAL, oidcConfigDAL, - projectBotDAL, projectUserMembershipRoleDAL, identityMetadataDAL, projectBotService, @@ -280,15 +275,7 @@ export const orgServiceFactory = ({ user.id, { encryptionVersion: 2, - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - publicKey: encKeys.publicKey, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, - salt: encKeys.salt, - verifier: encKeys.verifier + publicKey: encKeys.publicKey }, tx ); @@ -878,29 +865,10 @@ export const orgServiceFactory = ({ // So what we do is we generate a random secure password and then encrypt it with a random pub-private key // Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) { - const serverGeneratedPassword = crypto.randomBytes(32).toString("hex"); - const { tag, encoding, ciphertext, iv } = crypto - .encryption() - .symmetric() - .encryptWithRootEncryptionKey(serverGeneratedPassword); - const encKeys = await generateUserSrpKeys(inviteeEmail, serverGeneratedPassword); await userDAL.createUserEncryption( { userId: inviteeUserId, - encryptionVersion: 2, - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - publicKey: encKeys.publicKey, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, - salt: encKeys.salt, - verifier: encKeys.verifier, - serverEncryptedPrivateKeyEncoding: encoding, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKey: ciphertext + encryptionVersion: 2 }, tx ); @@ -1062,106 +1030,6 @@ export const orgServiceFactory = ({ const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - // this will auto generate bot - const { botKey, bot: autoGeneratedBot } = await projectBotService.getBotKey(projectId, true); - - const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx); - let ghostUserId = ghostUser?.id; - - // backfill missing ghost user - if (!ghostUserId) { - const newGhostUser = await addGhostUser(project.orgId, tx); - const projectMembership = await projectMembershipDAL.create( - { - userId: newGhostUser.user.id, - projectId: project.id - }, - tx - ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, - tx - ); - - const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ - publicKey: newGhostUser.keys.publicKey, - privateKey: newGhostUser.keys.plainPrivateKey, - plainProjectKey: botKey - }); - - // 4. Save the project key for the ghost user. - await projectKeyDAL.create( - { - projectId: project.id, - receiverId: newGhostUser.user.id, - encryptedKey: encryptedProjectKey, - nonce: encryptedProjectKeyIv, - senderId: newGhostUser.user.id - }, - tx - ); - - const { iv, tag, ciphertext, encoding, algorithm } = crypto - .encryption() - .symmetric() - .encryptWithRootEncryptionKey(newGhostUser.keys.plainPrivateKey); - if (autoGeneratedBot) { - await projectBotDAL.updateById( - autoGeneratedBot.id, - { - tag, - iv, - encryptedProjectKey, - encryptedProjectKeyNonce: encryptedProjectKeyIv, - encryptedPrivateKey: ciphertext, - isActive: true, - publicKey: newGhostUser.keys.publicKey, - senderId: newGhostUser.user.id, - algorithm, - keyEncoding: encoding - }, - tx - ); - } - ghostUserId = newGhostUser.user.id; - } - - const bot = await projectBotDAL.findOne({ projectId }, tx); - if (!bot) { - throw new NotFoundError({ - name: "InviteUser", - message: `Failed to find project bot for project with ID '${projectId}'` - }); - } - - const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUserId, projectId, tx); - if (!ghostUserLatestKey) { - throw new NotFoundError({ - name: "InviteUser", - message: `Failed to find project owner's latest key for project with ID '${projectId}'` - }); - } - - const botPrivateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - keyEncoding: bot.keyEncoding as SecretKeyEncoding, - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey - }); - - const newWsMembers = assignWorkspaceKeysToMembers({ - decryptKey: ghostUserLatestKey, - userPrivateKey: botPrivateKey, - members: userWithEncryptionKeyInvitedToProject.map((userEnc) => ({ - orgMembershipId: userEnc.userId, - projectMembershipRole: ProjectMembershipRole.Admin, - userPublicKey: userEnc.publicKey - })) - }); - const projectMemberships = await projectMembershipDAL.insertMany( userWithEncryptionKeyInvitedToProject.map((userEnc) => ({ projectId, @@ -1184,16 +1052,6 @@ export const orgServiceFactory = ({ }); await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - await projectKeyDAL.insertMany( - newWsMembers.map((el) => ({ - encryptedKey: el.workspaceEncryptedKey, - nonce: el.workspaceEncryptedNonce, - senderId: ghostUserId, - receiverId: el.orgMembershipId, - projectId - })), - tx - ); mailsForProjectInvitation.push({ email: userWithEncryptionKeyInvitedToProject .filter((el) => !userIdsWithOrgInvitation.has(el.userId)) diff --git a/backend/src/services/project-bot/project-bot-fns.ts b/backend/src/services/project-bot/project-bot-fns.ts index d26669b86f..029f320df3 100644 --- a/backend/src/services/project-bot/project-bot-fns.ts +++ b/backend/src/services/project-bot/project-bot-fns.ts @@ -42,6 +42,13 @@ export const getBotKeyFnFactory = ( message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console.` }); } + + if (!projectV1Keys.senderPublicKey) { + throw new NotFoundError({ + message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console and upgrade the project.` + }); + } + let userPrivateKey = ""; if ( projectV1Keys?.serverEncryptedPrivateKey && diff --git a/backend/src/services/project-key/project-key-dal.ts b/backend/src/services/project-key/project-key-dal.ts index ea4ed813c1..bb91b9c855 100644 --- a/backend/src/services/project-key/project-key-dal.ts +++ b/backend/src/services/project-key/project-key-dal.ts @@ -14,7 +14,7 @@ export const projectKeyDALFactory = (db: TDbClient) => { userId: string, projectId: string, tx?: Knex - ): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => { + ): Promise<(TProjectKeys & { sender: { publicKey?: string } }) | undefined> => { try { const projectKey = await (tx || db.replicaNode())(TableName.ProjectKeys) .join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`) @@ -25,7 +25,7 @@ export const projectKeyDALFactory = (db: TDbClient) => { .select(db.ref("publicKey").withSchema(TableName.UserEncryptionKey)) .first(); if (projectKey) { - return { ...projectKey, sender: { publicKey: projectKey.publicKey } }; + return { ...projectKey, sender: { publicKey: projectKey.publicKey || undefined } }; } } catch (error) { throw new DatabaseError({ error, name: "Find latest project key" }); diff --git a/backend/src/services/project/project-fns.ts b/backend/src/services/project/project-fns.ts index f166ec04f3..fca0074d73 100644 --- a/backend/src/services/project/project-fns.ts +++ b/backend/src/services/project/project-fns.ts @@ -10,6 +10,10 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types"; export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => { + if (!decryptKey.sender.publicKey) { + throw new Error("Decrypt key sender public key not found"); + } + const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({ ciphertext: decryptKey.encryptedKey, nonce: decryptKey.nonce, diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts index 0d7c7dd553..41406ea2f8 100644 --- a/backend/src/services/project/project-queue.ts +++ b/backend/src/services/project/project-queue.ts @@ -121,6 +121,10 @@ export const projectQueueFactory = ({ tag: data.encryptedPrivateKey.encryptedKeyTag }); + if (!oldProjectKey.sender.publicKey) { + throw new Error("Old project key sender public key not found"); + } + const decryptedPlainProjectKey = crypto.encryption().asymmetric().decrypt({ ciphertext: oldProjectKey.encryptedKey, nonce: oldProjectKey.nonce, @@ -187,6 +191,10 @@ export const projectQueueFactory = ({ approvalSecrets.push(...secretApprovals); } + if (!oldProjectKey.sender.publicKey) { + throw new Error("Old project key is not valid"); + } + const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey); const decryptedSecretVersions = decryptSecretVersions(secretVersions, userPrivateKey, oldProjectKey); const decryptedApprovalSecrets = decryptSecretApprovals(approvalSecrets, userPrivateKey, oldProjectKey); @@ -290,6 +298,10 @@ export const projectQueueFactory = ({ continue; } + if (!user.publicKey) { + throw new Error(`User with ID ${key.receiverId} has no public key during upgrade.`); + } + const [newMember] = assignWorkspaceKeysToMembers({ decryptKey: ghostUserLatestKey, userPrivateKey: ghostUser.keys.plainPrivateKey, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 27925c4b96..4ab6dbfdba 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -55,13 +55,10 @@ import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microso import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal"; import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; import { TOrgDALFactory } from "../org/org-dal"; -import { TOrgServiceFactory } from "../org/org-service"; import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal"; import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal"; -import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; -import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; @@ -78,7 +75,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types"; import { TProjectDALFactory } from "./project-dal"; -import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns"; +import { bootstrapSshProject } from "./project-fns"; import { TProjectQueueFactory } from "./project-queue"; import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal"; import { @@ -123,6 +120,7 @@ export const DEFAULT_PROJECT_ENVS = [ type TProjectServiceFactoryDep = { projectDAL: TProjectDALFactory; + identityProjectDAL: Pick; projectSshConfigDAL: Pick; projectQueue: TProjectQueueFactory; userDAL: TUserDALFactory; @@ -132,9 +130,7 @@ type TProjectServiceFactoryDep = { secretV2BridgeDAL: Pick; projectEnvDAL: Pick; identityOrgMembershipDAL: TIdentityOrgDALFactory; - identityProjectDAL: TIdentityProjectDALFactory; identityProjectMembershipRoleDAL: Pick; - projectKeyDAL: Pick; projectMembershipDAL: Pick< TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" @@ -167,12 +163,10 @@ type TProjectServiceFactoryDep = { sshHostDAL: Pick; sshHostGroupDAL: Pick; permissionService: TPermissionServiceFactory; - orgService: Pick; licenseService: Pick; smtpService: Pick; orgDAL: Pick; keyStore: Pick; - projectBotDAL: Pick; projectRoleDAL: Pick; kmsService: Pick< TKmsServiceFactory, @@ -196,27 +190,25 @@ export const projectServiceFactory = ({ secretDAL, secretV2BridgeDAL, projectQueue, - projectKeyDAL, permissionService, projectBotService, orgDAL, userDAL, folderDAL, - orgService, - identityProjectDAL, identityOrgMembershipDAL, projectMembershipDAL, projectEnvDAL, licenseService, projectUserMembershipRoleDAL, projectRoleDAL, - identityProjectMembershipRoleDAL, certificateAuthorityDAL, certificateDAL, certificateTemplateDAL, pkiCollectionDAL, pkiAlertDAL, pkiSubscriberDAL, + identityProjectDAL, + identityProjectMembershipRoleDAL, sshCertificateAuthorityDAL, sshCertificateAuthoritySecretDAL, sshCertificateDAL, @@ -225,7 +217,6 @@ export const projectServiceFactory = ({ sshHostGroupDAL, keyStore, kmsService, - projectBotDAL, projectSlackConfigDAL, projectMicrosoftTeamsConfigDAL, slackIntegrationDAL, @@ -253,7 +244,7 @@ export const projectServiceFactory = ({ type = ProjectType.SecretManager }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); - const { permission, membership: orgMembership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, organization.id, @@ -277,7 +268,6 @@ export const projectServiceFactory = ({ message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces." }); } - const ghostUser = await orgService.addGhostUser(organization.id, tx); if (kmsKeyId) { const kms = await kmsService.getKmsById(kmsKeyId, tx); @@ -329,19 +319,6 @@ export const projectServiceFactory = ({ }); } - // set ghost user as admin of project - const projectMembership = await projectMembershipDAL.create( - { - userId: ghostUser.user.id, - projectId: project.id - }, - tx - ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, - tx - ); - // set default environments and root folder for provided environments let envs: TProjectEnvironments[] = []; if (projectTemplate) { @@ -374,55 +351,6 @@ export const projectServiceFactory = ({ ); } - // 3. Create a random key that we'll use as the project key. - const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ - publicKey: ghostUser.keys.publicKey, - privateKey: ghostUser.keys.plainPrivateKey - }); - - // 4. Save the project key for the ghost user. - await projectKeyDAL.create( - { - projectId: project.id, - receiverId: ghostUser.user.id, - encryptedKey: encryptedProjectKey, - nonce: encryptedProjectKeyIv, - senderId: ghostUser.user.id - }, - tx - ); - - const { iv, tag, ciphertext, encoding, algorithm } = crypto - .encryption() - .symmetric() - .encryptWithRootEncryptionKey(ghostUser.keys.plainPrivateKey); - - // 5. Create & a bot for the project - await projectBotDAL.create( - { - name: "Infisical Bot (Ghost)", - projectId: project.id, - tag, - iv, - encryptedProjectKey, - encryptedProjectKeyNonce: encryptedProjectKeyIv, - encryptedPrivateKey: ciphertext, - isActive: true, - publicKey: ghostUser.keys.publicKey, - senderId: ghostUser.user.id, - algorithm, - keyEncoding: encoding - }, - tx - ); - - // Find the ghost users latest key - const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx); - - if (!latestKey) { - throw new Error("Latest key not found for user"); - } - // If the project is being created by a user, add the user to the project as an admin if (actor === ActorType.USER) { // Find public key of user @@ -432,17 +360,6 @@ export const projectServiceFactory = ({ throw new Error("User not found"); } - const [projectAdmin] = assignWorkspaceKeysToMembers({ - decryptKey: latestKey, - userPrivateKey: ghostUser.keys.plainPrivateKey, - members: [ - { - userPublicKey: user.publicKey, - orgMembershipId: orgMembership.id - } - ] - }); - // Create a membership for the user const userProjectMembership = await projectMembershipDAL.create( { @@ -455,18 +372,6 @@ export const projectServiceFactory = ({ { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, tx ); - - // Create a project key for the user - await projectKeyDAL.create( - { - encryptedKey: projectAdmin.workspaceEncryptedKey, - nonce: projectAdmin.workspaceEncryptedNonce, - senderId: ghostUser.user.id, - receiverId: user.id, - projectId: project.id - }, - tx - ); } // If the project is being created by an identity, add the identity to the project as an admin diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index b8c37a858c..b7c785db5b 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -116,7 +116,7 @@ export type TUpgradeProjectDTO = { } & TProjectPermission; export type AddUserToWsDTO = { - decryptKey: TProjectKeys & { sender: { publicKey: string } }; + decryptKey: TProjectKeys & { sender: { publicKey?: string } }; userPrivateKey: string; members: { orgMembershipId: string; diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b2258447ee..d257be9f9f 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,9 +1,7 @@ import { ForbiddenError } from "@casl/ability"; -import { SecretKeyEncoding } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; @@ -212,25 +210,6 @@ export const userServiceFactory = ({ ); }; - const getUserPrivateKey = async (userId: string) => { - const user = await userDAL.findUserEncKeyByUserId(userId); - if (!user?.serverEncryptedPrivateKey || !user.serverEncryptedPrivateKeyIV || !user.serverEncryptedPrivateKeyTag) { - throw new NotFoundError({ message: `Private key for user with ID '${userId}' not found` }); - } - - const privateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - ciphertext: user.serverEncryptedPrivateKey, - tag: user.serverEncryptedPrivateKeyTag, - iv: user.serverEncryptedPrivateKeyIV, - keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding - }); - - return privateKey; - }; - const getUserProjectFavorites = async (userId: string, orgId: string) => { const orgMembership = await orgMembershipDAL.findOne({ userId, @@ -311,7 +290,6 @@ export const userServiceFactory = ({ listUserGroups, getUserAction, unlockUser, - getUserPrivateKey, getAllMyAccounts, getUserProjectFavorites, removeMyDuplicateAccounts, diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 15f75a57dc..6179df15c0 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -17,6 +17,7 @@ const ( operationCallGetEncryptedWorkspaceKey = "CallGetEncryptedWorkspaceKey" operationCallGetServiceTokenDetails = "CallGetServiceTokenDetails" operationCallLogin1V3 = "CallLogin1V3" + operationCallLoginV3 = "CallLoginV3" operationCallVerifyMfaToken = "CallVerifyMfaToken" operationCallLogin2V3 = "CallLogin2V3" operationCallGetAllOrganizations = "CallGetAllOrganizations" @@ -100,6 +101,26 @@ func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLo return loginOneV2Response, nil } +func CallLoginV3(httpClient *resty.Client, request GetLoginV3Request) (GetLoginV3Response, error) { + var loginV3Response GetLoginV3Response + response, err := httpClient. + R(). + SetResult(&loginV3Response). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v3/auth/login", config.INFISICAL_URL)) + + if err != nil { + return GetLoginV3Response{}, NewGenericRequestError(operationCallLoginV3, err) + } + + if response.IsError() { + return GetLoginV3Response{}, NewAPIErrorWithResponse(operationCallLoginV3, response, nil) + } + + return loginV3Response, nil +} + func CallVerifyMfaToken(httpClient *resty.Client, request VerifyMfaTokenRequest) (*VerifyMfaTokenResponse, *VerifyMfaTokenErrorResponse, error) { var verifyMfaTokenResponse VerifyMfaTokenResponse var responseError VerifyMfaTokenErrorResponse diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 9bf666e44a..07f59e98fc 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -245,6 +245,15 @@ type GetLoginOneV2Request struct { ClientPublicKey string `json:"clientPublicKey"` } +type GetLoginV3Request struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type GetLoginV3Response struct { + AccessToken string `json:"accessToken"` +} + type GetLoginOneV2Response struct { ServerPublicKey string `json:"serverPublicKey"` Salt string `json:"salt"` diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index fd3ce15698..01c309d270 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -25,7 +25,6 @@ import ( "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/crypto" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/srp" "github.com/Infisical/infisical-merge/packages/util" @@ -280,7 +279,21 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) { util.HandleError(err, "Unable to parse email and password for authentication") } - loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password) + loginV3Response, err := getFreshUserCredentials(email, password) + if err == nil { + userCredentialsToBeStored.Email = email + userCredentialsToBeStored.PrivateKey = "" + userCredentialsToBeStored.JTWToken = loginV3Response.AccessToken + return + } + + if !strings.Contains(err.Error(), "LegacyEncryptionScheme") { + util.HandleError(err) + } + + log.Info().Msg("Unable to authenticate with the provided credentials, falling back to SRP authentication") + + _, loginTwoResponse, err := getFreshUserCredentialsWithSrp(email, password) if err != nil { fmt.Println("Unable to authenticate with the provided credentials, please try again") log.Debug().Err(err) @@ -338,105 +351,12 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) { } } - var decryptedPrivateKey []byte - - if loginTwoResponse.EncryptionVersion == 1 { - log.Debug().Msg("Login version 1") - encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey) - tag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag) - if err != nil { - util.HandleError(err) - } - - IV, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv) - if err != nil { - util.HandleError(err) - } - - paddedPassword := fmt.Sprintf("%032s", password) - key := []byte(paddedPassword) - - computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV) - if err != nil || len(computedDecryptedPrivateKey) == 0 { - util.HandleError(err) - } - - decryptedPrivateKey = computedDecryptedPrivateKey - - } else if loginTwoResponse.EncryptionVersion == 2 { - log.Debug().Msg("Login version 2") - protectedKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKey) - if err != nil { - util.HandleError(err) - } - - protectedKeyTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyTag) - if err != nil { - util.HandleError(err) - } - - protectedKeyIV, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyIV) - if err != nil { - util.HandleError(err) - } - - nonProtectedTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag) - if err != nil { - util.HandleError(err) - } - - nonProtectedIv, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv) - if err != nil { - util.HandleError(err) - } - - parameters := ¶ms{ - memory: 64 * 1024, - iterations: 3, - parallelism: 1, - keyLength: 32, - } - - derivedKey, err := generateFromPassword(password, []byte(loginOneResponse.Salt), parameters) - if err != nil { - util.HandleError(fmt.Errorf("unable to generate argon hash from password [err=%s]", err)) - } - - decryptedProtectedKey, err := crypto.DecryptSymmetric(derivedKey, protectedKey, protectedKeyTag, protectedKeyIV) - if err != nil { - util.HandleError(fmt.Errorf("unable to get decrypted protected key [err=%s]", err)) - } - - encryptedPrivateKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey) - if err != nil { - util.HandleError(err) - } - - decryptedProtectedKeyInHex, err := hex.DecodeString(string(decryptedProtectedKey)) - if err != nil { - util.HandleError(err) - } - - computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv) - if err != nil { - util.HandleError(err) - } - - decryptedPrivateKey = computedDecryptedPrivateKey - } else { - util.PrintErrorMessageAndExit("Insufficient details to decrypt private key") - } - - if string(decryptedPrivateKey) == "" || email == "" || loginTwoResponse.Token == "" { - log.Debug().Msgf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token) - util.PrintErrorMessageAndExit("We were unable to fetch required details to complete your login. Run with -d to see more info") - } // Login is successful so ask user to choose organization newJwtToken := GetJwtTokenWithOrganizationId(loginTwoResponse.Token, email) //updating usercredentials userCredentialsToBeStored.Email = email - userCredentialsToBeStored.PrivateKey = string(decryptedPrivateKey) + userCredentialsToBeStored.PrivateKey = "" userCredentialsToBeStored.JTWToken = newJwtToken } @@ -665,7 +585,27 @@ func askForLoginCredentials() (email string, password string, err error) { return userEmail, userPassword, nil } -func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2Response, *api.GetLoginTwoV2Response, error) { +func getFreshUserCredentials(email string, password string) (*api.GetLoginV3Response, error) { + log.Debug().Msg(fmt.Sprint("getFreshUserCredentials: ", "email", email, "password: ", password)) + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + return nil, err + } + httpClient.SetRetryCount(5) + + loginV3Response, err := api.CallLoginV3(httpClient, api.GetLoginV3Request{ + Email: email, + Password: password, + }) + + if err != nil { + return nil, err + } + + return &loginV3Response, nil +} + +func getFreshUserCredentialsWithSrp(email string, password string) (*api.GetLoginOneV2Response, *api.GetLoginTwoV2Response, error) { log.Debug().Msg(fmt.Sprint("getFreshUserCredentials: ", "email", email, "password: ", password)) httpClient, err := util.GetRestyClientWithCustomHeaders() if err != nil { diff --git a/frontend/src/components/auth/UserInfoStep.tsx b/frontend/src/components/auth/UserInfoStep.tsx index 2a1f8dca1d..11c425ae3e 100644 --- a/frontend/src/components/auth/UserInfoStep.tsx +++ b/frontend/src/components/auth/UserInfoStep.tsx @@ -1,12 +1,8 @@ -import crypto from "crypto"; - import { useState } from "react"; import { useTranslation } from "react-i18next"; import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import jsrp from "jsrp"; -import { useServerConfig } from "@app/context"; import { initProjectHelper } from "@app/helpers/project"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; @@ -14,15 +10,9 @@ import { onRequestError } from "@app/hooks/api/reactQuery"; import InputField from "../basic/InputField"; import checkPassword from "../utilities/checks/password/checkPassword"; -import Aes256Gcm from "../utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey, generateKeyPair } from "../utilities/cryptography/crypto"; -import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage"; import SecurityClient from "../utilities/SecurityClient"; import { Button, Input } from "../v2"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - interface UserInfoStepProps { incrementStep: () => void; email: string; @@ -76,7 +66,6 @@ export default function UserInfoStep({ }: UserInfoStepProps): JSX.Element { const [nameError, setNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false); - const { config } = useServerConfig(); const [errors, setErrors] = useState({}); @@ -108,109 +97,45 @@ export default function UserInfoStep({ }); if (!errorCheck) { - // Generate a random pair of a public and a private key - const pair = await generateKeyPair(config.fipsEnabled); + console.log("signupErrorCheck passed"); - localStorage.setItem("PRIVATE_KEY", pair.privateKey); + try { + const response = await completeAccountSignup({ + email, + password, + firstName: name.split(" ")[0], + lastName: name.split(" ").slice(1).join(" "), + providerAuthToken, + organizationName, + attributionSource + }); - client.init( - { - username: email, - password - }, - async () => { - client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { - try { - // TODO: moduralize into KeyService - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); + console.log("Signed up", JSON.stringify(response, null, 2)); - if (!derivedKey) throw new Error("Failed to derive key from password"); + // unset signup JWT token and set JWT token + SecurityClient.setSignupToken(""); + SecurityClient.setToken(response.token); + SecurityClient.setProviderAuthToken(""); - const key = crypto.randomBytes(32); - - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: pair.privateKey, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - const response = await completeAccountSignup({ - email, - password, - firstName: name.split(" ")[0], - lastName: name.split(" ").slice(1).join(" "), - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey: pair.publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - providerAuthToken, - salt: result.salt, - verifier: result.verifier, - organizationName, - attributionSource - }); - - // unset signup JWT token and set JWT token - SecurityClient.setSignupToken(""); - SecurityClient.setToken(response.token); - SecurityClient.setProviderAuthToken(""); - - if (response.organizationId) { - await selectOrganization({ organizationId: response.organizationId }); - } - - saveTokenToLocalStorage({ - publicKey: pair.publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey: pair.privateKey - }); - - const userOrgs = await fetchOrganizations(); - - const orgId = userOrgs[0]?.id; - await initProjectHelper({ - projectName: "Example Project" - }); - - localStorage.setItem("orgData.id", orgId); - - incrementStep(); - } catch (error) { - onRequestError(error); - setIsLoading(false); - console.error(error); - } - }); + if (response.organizationId) { + await selectOrganization({ organizationId: response.organizationId }); } - ); + + const userOrgs = await fetchOrganizations(); + + const orgId = userOrgs[0]?.id; + await initProjectHelper({ + projectName: "Example Project" + }); + + localStorage.setItem("orgData.id", orgId); + + incrementStep(); + } catch (error) { + onRequestError(error); + setIsLoading(false); + console.error(error); + } } else { setIsLoading(false); } diff --git a/frontend/src/components/utilities/attemptChangePassword.ts b/frontend/src/components/utilities/attemptChangePassword.ts deleted file mode 100644 index e51168525f..0000000000 --- a/frontend/src/components/utilities/attemptChangePassword.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* eslint-disable new-cap */ -import crypto from "crypto"; - -import jsrp from "jsrp"; - -import { changePassword, srp1 } from "@app/hooks/api/auth/queries"; - -import Aes256Gcm from "./cryptography/aes-256-gcm"; -import { deriveArgonKey } from "./cryptography/crypto"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; - -const clientOldPassword = new jsrp.client(); -const clientNewPassword = new jsrp.client(); - -type Params = { - email: string; - currentPassword: string; - newPassword: string; -}; - -const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise => { - return new Promise((resolve, reject) => { - clientOldPassword.init({ username: email, password: currentPassword }, async () => { - let serverPublicKey; - let salt; - - try { - const clientPublicKey = clientOldPassword.getPublicKey(); - - const res = await srp1({ clientPublicKey }); - - serverPublicKey = res.serverPublicKey; - salt = res.salt; - - clientOldPassword.setSalt(salt); - clientOldPassword.setServerPublicKey(serverPublicKey); - - const clientProof = clientOldPassword.getProof(); - - clientNewPassword.init({ username: email, password: newPassword }, async () => { - clientNewPassword.createVerifier(async (_err, result) => { - try { - const derivedKey = await deriveArgonKey({ - password: newPassword, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); - - if (!derivedKey) throw new Error("Failed to derive key from password"); - - const key = crypto.randomBytes(32); - - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: localStorage.getItem("PRIVATE_KEY") as string, - secret: key - }); - - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - await changePassword({ - password: newPassword, - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt: result.salt, - verifier: result.verifier - }); - - saveTokenToLocalStorage({ - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - }); - - resolve(); - } catch (err2) { - console.error(err2); - reject(err2); - } - }); - }); - } catch (err) { - console.error(err); - reject(err); - } - }); - }); -}; - -export default attemptChangePassword; diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index d897ebeaae..dd61dc4664 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -1,18 +1,18 @@ /* eslint-disable prefer-destructuring */ +import axios from "axios"; import jsrp from "jsrp"; -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, login2 } from "@app/hooks/api/auth/queries"; +import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries"; +import { createNotification } from "../notifications"; import Telemetry from "./telemetry/Telemetry"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; +import { LoginMode } from "./attemptLogin"; import SecurityClient from "./SecurityClient"; // eslint-disable-next-line new-cap const client = new jsrp.client(); export interface IsCliLoginSuccessful { - mfaEnabled: boolean; loginResponse?: { email: string; privateKey: string; @@ -31,14 +31,62 @@ const attemptLogin = async ({ email, password, providerAuthToken, - captchaToken + captchaToken, + loginMode = LoginMode.ServerSide }: { email: string; password: string; providerAuthToken?: string; captchaToken?: string; + loginMode?: LoginMode; }): Promise => { const telemetry = new Telemetry().getInstance(); + + if (loginMode === LoginMode.ServerSide) { + console.log("attempting login with server side"); + const data = await loginV3({ + email, + password, + providerAuthToken, + captchaToken + }).catch((err) => { + if (axios.isAxiosError(err) && err.response?.status === 400) { + if (err.response.data.error === "LegacyEncryptionScheme") { + createNotification({ + text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.", + type: "error" + }); + + return null; + } + } + + throw err; + }); + + if (data === null) { + return attemptLogin({ + email, + password, + providerAuthToken, + captchaToken, + loginMode: LoginMode.LegacySrp + }); + } + + SecurityClient.setProviderAuthToken(""); + SecurityClient.setToken(data.accessToken); + + return { + success: true, + loginResponse: { + email, + privateKey: "", + JTWToken: data.accessToken + } + }; + } + return new Promise((resolve, reject) => { client.init( { @@ -58,79 +106,26 @@ const attemptLogin = async ({ client.setServerPublicKey(serverPublicKey); const clientProof = client.getProof(); // called M1 - const { - mfaEnabled, - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await login2({ + const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({ email, password, clientProof, providerAuthToken, captchaToken }); - if (mfaEnabled) { - // case: MFA is enabled - - // set temporary (MFA) JWT token - SecurityClient.setMfaToken(token); - - resolve({ - mfaEnabled, - success: true - }); - } else if ( - !mfaEnabled && - encryptionVersion && - encryptedPrivateKey && - iv && - tag && - token - ) { - // case: MFA is not enabled - - // unset provider auth token in case it was used + if (encryptionVersion && encryptedPrivateKey && iv && tag && token) { SecurityClient.setProviderAuthToken(""); - // set JWT token SecurityClient.setToken(token); - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - if (email) { telemetry.identify(email, email); telemetry.capture("User Logged In"); } resolve({ - mfaEnabled: false, loginResponse: { email, - privateKey, + privateKey: "", JTWToken: token }, success: true diff --git a/frontend/src/components/utilities/attemptCliLoginMfa.ts b/frontend/src/components/utilities/attemptCliLoginMfa.ts deleted file mode 100644 index 14a61d817e..0000000000 --- a/frontend/src/components/utilities/attemptCliLoginMfa.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-disable prefer-destructuring */ -import jsrp from "jsrp"; - -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries"; - -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; -import SecurityClient from "./SecurityClient"; - -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - -interface IsMfaLoginSuccessful { - success: boolean; - loginResponse: { - privateKey: string; - JTWToken: string; - }; -} - -/** - * Return whether or not MFA-login is successful for user with email [email] - * and MFA token [mfaToken] - * @param {Object} obj - * @param {String} obj.email - email of user - * @param {String} obj.mfaToken - MFA code/token - */ -const attemptLoginMfa = async ({ - email, - password, - providerAuthToken, - mfaToken -}: { - email: string; - password: string; - providerAuthToken?: string; - mfaToken: string; -}): Promise => { - return new Promise((resolve, reject) => { - client.init( - { - username: email, - password - }, - async () => { - try { - const clientPublicKey = client.getPublicKey(); - const { salt } = await login1({ - email, - clientPublicKey, - providerAuthToken - }); - - const { - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await verifyMfaToken({ - email, - mfaCode: mfaToken - }); - - // unset temporary (MFA) JWT token and set JWT token - SecurityClient.setMfaToken(""); - SecurityClient.setToken(token); - SecurityClient.setProviderAuthToken(""); - - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - - resolve({ - success: true, - loginResponse: { - privateKey, - JTWToken: token - } - }); - } catch (err) { - reject(err); - } - } - ); - }); -}; - -export default attemptLoginMfa; diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index c371d9c910..1bf673cc61 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -1,15 +1,19 @@ /* eslint-disable prefer-destructuring */ +import axios from "axios"; import jsrp from "jsrp"; -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, login2 } from "@app/hooks/api/auth/queries"; +import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries"; +import { createNotification } from "../notifications"; import Telemetry from "./telemetry/Telemetry"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; import SecurityClient from "./SecurityClient"; +export enum LoginMode { + LegacySrp = "legacy-srp", + ServerSide = "server-side" +} + interface IsLoginSuccessful { - mfaEnabled: boolean; success: boolean; } @@ -23,14 +27,62 @@ const attemptLogin = async ({ email, password, providerAuthToken, - captchaToken + captchaToken, + loginMode = LoginMode.ServerSide }: { email: string; password: string; providerAuthToken?: string; captchaToken?: string; + loginMode?: LoginMode; }): Promise => { const telemetry = new Telemetry().getInstance(); + + if (loginMode === LoginMode.ServerSide) { + console.log("attempting login with server side"); + const data = await loginV3({ + email, + password, + providerAuthToken, + captchaToken + }).catch((err) => { + if (axios.isAxiosError(err) && err.response?.status === 400) { + if (err.response.data.error === "LegacyEncryptionScheme") { + createNotification({ + text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.", + type: "error" + }); + + return null; + } + } + + throw err; + }); + + if (data === null) { + return attemptLogin({ + email, + password, + providerAuthToken, + captchaToken, + loginMode: LoginMode.LegacySrp + }); + } + + SecurityClient.setProviderAuthToken(""); + SecurityClient.setToken(data.accessToken); + + if (email) { + telemetry.identify(email, email); + telemetry.capture("User Logged In"); + } + + return { + success: true + }; + } + // eslint-disable-next-line new-cap const client = new jsrp.client(); await new Promise((resolve) => { @@ -48,18 +100,7 @@ const attemptLogin = async ({ client.setServerPublicKey(serverPublicKey); const clientProof = client.getProof(); // called M1 - const { - mfaEnabled, - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await login2({ + const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({ captchaToken, email, password, @@ -67,56 +108,22 @@ const attemptLogin = async ({ providerAuthToken }); - if (mfaEnabled) { - // case: MFA is enabled - - // set temporary (MFA) JWT token - SecurityClient.setMfaToken(token); - - return { - mfaEnabled, - success: true - }; - } - if (!mfaEnabled && encryptionVersion && encryptedPrivateKey && iv && tag && token) { - // case: MFA is not enabled - + if (encryptionVersion && encryptedPrivateKey && iv && tag && token) { // unset provider auth token in case it was used SecurityClient.setProviderAuthToken(""); // set JWT token SecurityClient.setToken(token); - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - if (email) { telemetry.identify(email, email); telemetry.capture("User Logged In"); } return { - mfaEnabled: false, success: true }; } - return { success: false, mfaEnabled: false }; + return { success: false }; }; export default attemptLogin; diff --git a/frontend/src/components/utilities/attemptLoginMfa.ts b/frontend/src/components/utilities/attemptLoginMfa.ts deleted file mode 100644 index 10f36ec397..0000000000 --- a/frontend/src/components/utilities/attemptLoginMfa.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable prefer-destructuring */ -import jsrp from "jsrp"; - -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries"; - -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; -import SecurityClient from "./SecurityClient"; - -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - -/** - * Return whether or not MFA-login is successful for user with email [email] - * and MFA token [mfaToken] - * @param {Object} obj - * @param {String} obj.email - email of user - * @param {String} obj.mfaToken - MFA code/token - */ -const attemptLoginMfa = async ({ - email, - password, - providerAuthToken, - mfaToken -}: { - email: string; - password: string; - providerAuthToken?: string; - mfaToken: string; -}): Promise => { - return new Promise((resolve, reject) => { - client.init( - { - username: email, - password - }, - async () => { - try { - const clientPublicKey = client.getPublicKey(); - const { salt } = await login1({ - email, - clientPublicKey, - providerAuthToken - }); - - const { - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await verifyMfaToken({ - email, - mfaCode: mfaToken - }); - - // unset temporary (MFA) JWT token and set JWT token - SecurityClient.setMfaToken(""); - SecurityClient.setToken(token); - SecurityClient.setProviderAuthToken(""); - - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - - resolve(true); - } catch (err) { - reject(err); - } - } - ); - }); -}; - -export default attemptLoginMfa; diff --git a/frontend/src/components/utilities/cryptography/issueBackupKey.ts b/frontend/src/components/utilities/cryptography/issueBackupKey.ts deleted file mode 100644 index 52a502e4ea..0000000000 --- a/frontend/src/components/utilities/cryptography/issueBackupKey.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable new-cap */ -import crypto from "crypto"; - -import jsrp from "jsrp"; - -import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries"; - -import generateBackupPDF from "../generateBackupPDF"; -import Aes256Gcm from "./aes-256-gcm"; - -const clientPassword = new jsrp.client(); -const clientKey = new jsrp.client(); - -interface BackupKeyProps { - email: string; - password: string; - personalName: string; - setBackupKeyError: (value: boolean) => void; - setBackupKeyIssued: (value: boolean) => void; -} - -/** - * This function issue a backup key for a user - * @param {obkect} obj - * @param {string} obj.email - email of a user issuing a backup key - * @param {string} obj.password - password of a user issuing a backup key - * @param {string} obj.personalName - name of a user issuing a backup key - * @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key - * @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly - * @returns - */ -const issueBackupKey = async ({ - email, - password, - personalName, - setBackupKeyError, - setBackupKeyIssued -}: BackupKeyProps) => { - try { - setBackupKeyError(false); - setBackupKeyIssued(false); - clientPassword.init( - { - username: email, - password - }, - async () => { - const clientPublicKey = clientPassword.getPublicKey(); - - let serverPublicKey; - let salt; - try { - const res = await srp1({ - clientPublicKey - }); - serverPublicKey = res.serverPublicKey; - salt = res.salt; - } catch (err) { - setBackupKeyError(true); - console.log("Wrong current password", err, 1); - } - - clientPassword.setSalt(salt as string); - clientPassword.setServerPublicKey(serverPublicKey as string); - const clientProof = clientPassword.getProof(); // called M1 - - const generatedKey = crypto.randomBytes(16).toString("hex"); - - clientKey.init( - { - username: email, - password: generatedKey - }, - async () => { - clientKey.createVerifier( - async (_err: any, result: { salt: string; verifier: string }) => { - const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ - text: String(localStorage.getItem("PRIVATE_KEY")), - secret: generatedKey - }); - - try { - await issueBackupPrivateKey({ - encryptedPrivateKey: ciphertext, - iv, - tag, - salt: result.salt, - verifier: result.verifier, - clientProof - }); - - generateBackupPDF({ - personalName, - personalEmail: email, - generatedKey - }); - setBackupKeyIssued(true); - } catch { - setBackupKeyError(true); - } - } - ); - } - ); - } - ); - } catch { - setBackupKeyError(true); - console.log("Failed to issue a backup key"); - } - return true; -}; - -export default issueBackupKey; diff --git a/frontend/src/components/utilities/saveTokenToLocalStorage.ts b/frontend/src/components/utilities/saveTokenToLocalStorage.ts deleted file mode 100644 index 1539438f45..0000000000 --- a/frontend/src/components/utilities/saveTokenToLocalStorage.ts +++ /dev/null @@ -1,67 +0,0 @@ -interface Props { - protectedKey?: string; - protectedKeyIV?: string; - protectedKeyTag?: string; - publicKey?: string; - encryptedPrivateKey?: string; - iv?: string; - tag?: string; - privateKey?: string; -} - -export const saveTokenToLocalStorage = ({ - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey -}: Props) => { - try { - if (protectedKey) { - localStorage.removeItem("protectedKey"); - localStorage.setItem("protectedKey", protectedKey); - } - - if (protectedKeyIV) { - localStorage.removeItem("protectedKeyIV"); - localStorage.setItem("protectedKeyIV", protectedKeyIV); - } - - if (protectedKeyTag) { - localStorage.removeItem("protectedKeyTag"); - localStorage.setItem("protectedKeyTag", protectedKeyTag); - } - - if (publicKey) { - localStorage.removeItem("publicKey"); - localStorage.setItem("publicKey", publicKey); - } - - if (encryptedPrivateKey) { - localStorage.removeItem("encryptedPrivateKey"); - localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey); - } - - if (iv) { - localStorage.removeItem("iv"); - localStorage.setItem("iv", iv); - } - - if (tag) { - localStorage.removeItem("tag"); - localStorage.setItem("tag", tag); - } - - if (privateKey) { - localStorage.removeItem("PRIVATE_KEY"); - localStorage.setItem("PRIVATE_KEY", privateKey); - } - } catch (err) { - if (err instanceof Error) { - throw new Error(`Unable to send the tokens in local storage:${err.message}`); - } - } -}; diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 796fd31529..cb7e3f64c7 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -8,26 +8,24 @@ import { organizationKeys } from "../organization/queries"; import { setAuthToken } from "../reactQuery"; import { workspaceKeys } from "../workspace"; import { - ChangePasswordDTO, CompleteAccountDTO, CompleteAccountSignupDTO, GetAuthTokenAPI, GetBackupEncryptedPrivateKeyDTO, - IssueBackupPrivateKeyDTO, Login1DTO, Login1Res, Login2DTO, Login2Res, LoginLDAPDTO, LoginLDAPRes, + LoginV3DTO, + LoginV3Res, MfaMethod, ResetPasswordDTO, ResetPasswordV2DTO, ResetUserPasswordV2DTO, SendMfaTokenDTO, SetupPasswordDTO, - SRP1DTO, - SRPR1Res, TOauthTokenExchangeDTO, UserAgentType, UserEncryptionVersion, @@ -50,21 +48,14 @@ export const login2 = async (loginDetails: Login2DTO) => { return data; }; -export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => { - const { data } = await apiRequest.post("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token +export const loginV3 = async (loginDetails: LoginV3DTO) => { + const { data } = await apiRequest.post("/api/v3/auth/login", loginDetails); return data; }; -export const useLogin1 = () => { - return useMutation({ - mutationFn: async (details: { - email: string; - clientPublicKey: string; - providerAuthToken?: string; - }) => { - return login1(details); - } - }); +export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => { + const { data } = await apiRequest.post("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token + return data; }; export const selectOrganization = async (data: { @@ -143,11 +134,6 @@ export const useOauthTokenExchange = () => { }); }; -export const srp1 = async (details: SRP1DTO) => { - const { data } = await apiRequest.post("/api/v1/password/srp1", details); - return data; -}; - export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => { const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details); return data; @@ -158,14 +144,6 @@ export const completeAccountSignupInvite = async (details: CompleteAccountDTO) = return data; }; -export const useCompleteAccountSignup = () => { - return useMutation({ - mutationFn: async (details: CompleteAccountSignupDTO) => { - return completeAccountSignup(details); - } - }); -}; - export const useSendMfaToken = () => { return useMutation({ mutationFn: async ({ email }) => { @@ -263,11 +241,6 @@ export const useVerifyPasswordResetCode = () => { }); }; -export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => { - const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details); - return data; -}; - export const getBackupEncryptedPrivateKey = async ({ verificationToken }: GetBackupEncryptedPrivateKeyDTO) => { @@ -328,20 +301,6 @@ export const useResetUserPasswordV2 = () => { }); }; -export const changePassword = async (details: ChangePasswordDTO) => { - const { data } = await apiRequest.post("/api/v1/password/change-password", details); - return data; -}; - -export const useChangePassword = () => { - // note: use after srp1 - return useMutation({ - mutationFn: async (details: ChangePasswordDTO) => { - return changePassword(details); - } - }); -}; - // Refresh token is set as cookie when logged in // Using that we fetch the auth bearer token needed for auth calls export const fetchAuthToken = async () => { diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index cbd20b6432..fd9d6fb5cb 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -49,13 +49,19 @@ export type Login2DTO = { password: string; }; +export type LoginV3DTO = { + email: string; + password: string; + providerAuthToken?: string; + captchaToken?: string; +}; + export type Login1Res = { serverPublicKey: string; salt: string; }; export type Login2Res = { - mfaEnabled: boolean; token: string; encryptionVersion?: number; protectedKey?: string; @@ -67,6 +73,11 @@ export type Login2Res = { tag?: string; }; +export type LoginV3Res = { + accessToken: string; + mfaEnabled: boolean; +}; + export type LoginLDAPDTO = { organizationSlug: string; username: string; @@ -77,28 +88,10 @@ export type LoginLDAPRes = { nextUrl: string; }; -export type SRP1DTO = { - clientPublicKey: string; -}; - -export type SRPR1Res = { - serverPublicKey: string; - salt: string; -}; - export type CompleteAccountDTO = { email: string; firstName: string; lastName: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; password: string; tokenMetadata?: string; }; @@ -116,19 +109,6 @@ export type VerifySignupInviteDTO = { organizationId: string; }; -export type ChangePasswordDTO = { - password: string; - clientProof: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; -}; - export type ResetPasswordDTO = { protectedKey: string; protectedKeyIV: string; @@ -153,27 +133,11 @@ export type ResetUserPasswordV2DTO = { }; export type SetupPasswordDTO = { - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; + email: string; token: string; password: string; }; -export type IssueBackupPrivateKeyDTO = { - encryptedPrivateKey: string; - iv: string; - tag: string; - salt: string; - verifier: string; - clientProof: string; -}; - export type GetBackupEncryptedPrivateKeyDTO = { verificationToken: string; }; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index b740b58ee9..b53b980fd2 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -477,14 +477,6 @@ export const useGetMyOrganizationProjects = (orgId: string) => { }); }; -export const fetchMyPrivateKey = async () => { - const { - data: { privateKey } - } = await apiRequest.get<{ privateKey: string }>("/api/v1/user/private-key"); - - return privateKey; -}; - export const useListUserGroupMemberships = (username: string) => { return useQuery({ queryKey: userKeys.listUserGroupMemberships(username), diff --git a/frontend/src/lib/crypto/index.ts b/frontend/src/lib/crypto/index.ts index d0d6cd5dd2..d722b68eae 100644 --- a/frontend/src/lib/crypto/index.ts +++ b/frontend/src/lib/crypto/index.ts @@ -4,53 +4,6 @@ import jsrp from "jsrp"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto"; -import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries"; - -export const generateUserBackupKey = async (email: string, password: string) => { - // eslint-disable-next-line new-cap - const clientKey = new jsrp.client(); - // eslint-disable-next-line new-cap - const clientPassword = new jsrp.client(); - - await new Promise((resolve) => { - clientPassword.init({ username: email, password }, () => resolve(null)); - }); - const clientPublicKey = clientPassword.getPublicKey(); - const srpKeys = await srp1({ clientPublicKey }); - clientPassword.setSalt(srpKeys.salt); - clientPassword.setServerPublicKey(srpKeys.serverPublicKey); - - const clientProof = clientPassword.getProof(); // called M1 - const generatedKey = crypto.randomBytes(16).toString("hex"); - - await new Promise((resolve) => { - clientKey.init({ username: email, password: generatedKey }, () => resolve(null)); - }); - - const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>( - (resolve, reject) => { - clientKey.createVerifier((err, res) => { - if (err) return reject(err); - return resolve(res); - }); - } - ); - const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ - text: String(localStorage.getItem("PRIVATE_KEY")), - secret: generatedKey - }); - - await issueBackupPrivateKey({ - encryptedPrivateKey: ciphertext, - iv, - tag, - salt, - verifier, - clientProof - }); - - return generatedKey; -}; export const generateUserPassKey = async ( email: string, diff --git a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx index 2d6aeb9d65..19c1537a2f 100644 --- a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx +++ b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx @@ -8,7 +8,6 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; // TODO(akhilmhdh): rewrite this into module functions in lib -import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, ContentLoader, FormControl, Input } from "@app/components/v2"; import { useServerConfig } from "@app/context"; @@ -63,13 +62,6 @@ export const SignUpPage = () => { }); SecurityClient.setToken(res.token); - saveTokenToLocalStorage({ - publicKey: userPass.publicKey, - encryptedPrivateKey: userPass.encryptedPrivateKey, - iv: userPass.encryptedPrivateKeyIV, - tag: userPass.encryptedPrivateKeyTag, - privateKey - }); await selectOrganization({ organizationId: res.organization.id }); // TODO(akhilmhdh): This is such a confusing pattern and too unreliable diff --git a/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx b/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx index 91432f9bee..b347a78d85 100644 --- a/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx @@ -18,7 +18,7 @@ import { useToggle } from "@app/hooks"; import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -import { fetchMyPrivateKey, fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries"; +import { fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries"; import { EmailDuplicationConfirmation } from "@app/pages/auth/SelectOrgPage/EmailDuplicationConfirmation"; import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils"; @@ -70,9 +70,6 @@ export const PasswordStep = ({ // set JWT token SecurityClient.setToken(oauthLogin.token); - const privateKey = await fetchMyPrivateKey(); - localStorage.setItem("PRIVATE_KEY", privateKey); - // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org if (organizationId) { const finishWithOrgWorkflow = async () => { @@ -92,7 +89,7 @@ export const PasswordStep = ({ console.log("organization id was present. new JWT token to be used in CLI:", token); const instance = axios.create(); const payload = { - privateKey, + privateKey: "", // note(daniel): no longer needed by the CLI, because the CLI only uses the private key to create service tokens, and the private key isn't used anymore when creating service tokens. email, JTWToken: token }; diff --git a/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx index 311d613efc..a134220e89 100644 --- a/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx +++ b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx @@ -1,22 +1,14 @@ -import crypto from "crypto"; - import { FormEvent, useState } from "react"; import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate, useSearch } from "@tanstack/react-router"; -import jsrp from "jsrp"; import { createNotification } from "@app/components/notifications"; import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck"; -import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { useSetupPassword } from "@app/hooks/api/auth/queries"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - export const PasswordSetupPage = () => { const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -65,83 +57,31 @@ export const PasswordSetupPage = () => { setPasswordsMatch(true); if (!errorCheck) { - client.init( - { - username: email, + try { + await setupPassword.mutateAsync({ + email, + token, password - }, - async () => { - client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); + }); - if (!derivedKey) throw new Error("Failed to derive key from password"); + setIsRedirecting(true); - const key = crypto.randomBytes(32); + createNotification({ + type: "success", + title: "Password successfully set", + text: "Redirecting to login..." + }); - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: localStorage.getItem("PRIVATE_KEY") as string, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - try { - await setupPassword.mutateAsync({ - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt: result.salt, - verifier: result.verifier, - token, - password - }); - - setIsRedirecting(true); - - createNotification({ - type: "success", - title: "Password successfully set", - text: "Redirecting to login..." - }); - - setTimeout(() => { - window.location.href = "/login"; - }, 3000); - } catch (error) { - createNotification({ - type: "error", - text: (error as Error).message ?? "Error setting password" - }); - navigate({ to: "/personal-settings" }); - } - }); - } - ); + setTimeout(() => { + window.location.href = "/login"; + }, 3000); + } catch (error) { + createNotification({ + type: "error", + text: (error as Error).message ?? "Error setting password" + }); + navigate({ to: "/personal-settings" }); + } } }; diff --git a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx index ac194cd05d..9518b4ae32 100644 --- a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx @@ -132,11 +132,8 @@ export const SelectOrganizationSection = () => { } if (callbackPort) { - const privateKey = localStorage.getItem("PRIVATE_KEY"); - let error: string | null = null; - if (!privateKey) error = "Private key not found"; if (!user?.email) error = "User email not found"; if (!token) error = "No token found"; @@ -151,7 +148,7 @@ export const SelectOrganizationSection = () => { const payload = { JTWToken: token, email: user?.email, - privateKey + privateKey: "" } as IsCliLoginSuccessful["loginResponse"]; // send request to server endpoint diff --git a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx index a4ebe7a11a..3bd8f6bc41 100644 --- a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx +++ b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx @@ -1,23 +1,17 @@ /* eslint-disable no-nested-ternary */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import crypto from "crypto"; import { useState } from "react"; import { Helmet } from "react-helmet"; import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useNavigate, useSearch } from "@tanstack/react-router"; -import jsrp from "jsrp"; import { Mfa } from "@app/components/auth/Mfa"; import InputField from "@app/components/basic/InputField"; import checkPassword from "@app/components/utilities/checks/password/checkPassword"; -import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto"; -import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button } from "@app/components/v2"; -import { useServerConfig } from "@app/context"; import { useToggle } from "@app/hooks"; import { completeAccountSignupInvite, @@ -28,9 +22,6 @@ import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { isLoggedIn } from "@app/hooks/api/reactQuery"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - type Errors = { tooShort?: string; tooLong?: string; @@ -67,7 +58,6 @@ export const SignupInvitePage = () => { const metadata = queryParams.get("metadata") || undefined; const { mutateAsync: selectOrganization } = useSelectOrganization(); - const { config } = useServerConfig(); const loggedIn = isLoggedIn(); @@ -94,123 +84,56 @@ export const SignupInvitePage = () => { } if (!errorCheck) { - // Generate a random pair of a public and a private key - const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled); + try { + const { token: jwtToken } = await completeAccountSignupInvite({ + email, + password, + firstName, + lastName, + tokenMetadata: metadata + }); - localStorage.setItem("PRIVATE_KEY", privateKey); + // unset temporary signup JWT token and set JWT token + SecurityClient.setSignupToken(""); + SecurityClient.setToken(jwtToken); - client.init( - { - username: email, - password - }, - async () => { - client.createVerifier(async (_err, result) => { - try { - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); + const userOrgs = await fetchOrganizations(); - if (!derivedKey) throw new Error("Failed to derive key from password"); + const orgId = userOrgs[0].id; - const key = crypto.randomBytes(32); + if (!orgId) throw new Error("You are not part of any organization"); - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: privateKey, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - const { token: jwtToken } = await completeAccountSignupInvite({ - email, - password, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt: result.salt, - verifier: result.verifier, - tokenMetadata: metadata - }); - - // unset temporary signup JWT token and set JWT token - SecurityClient.setSignupToken(""); - SecurityClient.setToken(jwtToken); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey - }); - - const userOrgs = await fetchOrganizations(); - - const orgId = userOrgs[0].id; - - if (!orgId) throw new Error("You are not part of any organization"); - - const completeSignupFlow = async () => { - const { - token: mfaToken, - isMfaEnabled, - mfaMethod - } = await selectOrganization({ - organizationId: orgId - }); - - if (isMfaEnabled) { - SecurityClient.setMfaToken(mfaToken); - if (mfaMethod) { - setRequiredMfaMethod(mfaMethod); - } - toggleShowMfa.on(); - setMfaSuccessCallback(() => completeSignupFlow); - return; - } - - localStorage.setItem("orgData.id", orgId); - - navigate({ - to: "/organization/projects" - }); - }; - - await completeSignupFlow(); - } catch (error) { - setIsLoading(false); - console.error(error); - } + const completeSignupFlow = async () => { + const { + token: mfaToken, + isMfaEnabled, + mfaMethod + } = await selectOrganization({ + organizationId: orgId }); - } - ); + + if (isMfaEnabled) { + SecurityClient.setMfaToken(mfaToken); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } + toggleShowMfa.on(); + setMfaSuccessCallback(() => completeSignupFlow); + return; + } + + localStorage.setItem("orgData.id", orgId); + + navigate({ + to: "/organization/projects" + }); + }; + + await completeSignupFlow(); + } catch (error) { + setIsLoading(false); + console.error(error); + } } else { setIsLoading(false); } diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 85343e79e3..1b10ed94c6 100644 --- a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -3,24 +3,16 @@ import crypto from "crypto"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "@tanstack/react-router"; -import jsrp from "jsrp"; import { Mfa } from "@app/components/auth/Mfa"; -import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto"; -import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, Input } from "@app/components/v2"; -import { useServerConfig } from "@app/context"; import { initProjectHelper } from "@app/helpers/project"; import { useToggle } from "@app/hooks"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - type Props = { username: string; password: string; @@ -64,7 +56,6 @@ export const UserInfoSSOStep = ({ const { mutateAsync: selectOrganization } = useSelectOrganization(); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const navigate = useNavigate(); - const { config } = useServerConfig(); useEffect(() => { const randomPassword = crypto.randomBytes(32).toString("hex"); @@ -93,131 +84,64 @@ export const UserInfoSSOStep = ({ } if (!errorCheck) { - // Generate a random pair of a public and a private key - const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled); - localStorage.setItem("PRIVATE_KEY", privateKey); + try { + const response = await completeAccountSignup({ + email: username, + password, + firstName: name.split(" ")[0], + lastName: name.split(" ").slice(1).join(" "), + providerAuthToken, + organizationName, + attributionSource, + useDefaultOrg: forceDefaultOrg + }); - client.init( - { - username, - password - }, - async () => { - client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { - try { - // TODO: moduralize into KeyService - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); + // unset signup JWT token and set JWT token + SecurityClient.setSignupToken(""); + SecurityClient.setToken(response.token); + SecurityClient.setProviderAuthToken(""); - if (!derivedKey) throw new Error("Failed to derive key from password"); + const userOrgs = await fetchOrganizations(); + const orgId = userOrgs[0]?.id; - const key = crypto.randomBytes(32); + const completeSignupFlow = async () => { + try { + const { isMfaEnabled, token, mfaMethod } = await selectOrganization({ + organizationId: orgId + }); - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: privateKey, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - const response = await completeAccountSignup({ - email: username, - password, - firstName: name.split(" ")[0], - lastName: name.split(" ").slice(1).join(" "), - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - providerAuthToken, - salt: result.salt, - verifier: result.verifier, - organizationName, - attributionSource, - useDefaultOrg: forceDefaultOrg - }); - - // unset signup JWT token and set JWT token - SecurityClient.setSignupToken(""); - SecurityClient.setToken(response.token); - SecurityClient.setProviderAuthToken(""); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey - }); - - const userOrgs = await fetchOrganizations(); - const orgId = userOrgs[0]?.id; - - const completeSignupFlow = async () => { - try { - const { isMfaEnabled, token, mfaMethod } = await selectOrganization({ - organizationId: orgId - }); - - if (isMfaEnabled) { - SecurityClient.setMfaToken(token); - if (mfaMethod) { - setRequiredMfaMethod(mfaMethod); - } - toggleShowMfa.on(); - setMfaSuccessCallback(() => completeSignupFlow); - return; - } - - // only create example project if not joining existing org - if (!providerOrganizationName) { - await initProjectHelper({ - projectName: "Example Project" - }); - } - - localStorage.setItem("orgData.id", orgId); - navigate({ - to: "/organization/projects" - }); - } catch (error) { - setIsLoading(false); - console.error(error); - } - }; - - await completeSignupFlow(); - } catch (error) { - setIsLoading(false); - console.error(error); + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } + toggleShowMfa.on(); + setMfaSuccessCallback(() => completeSignupFlow); + return; } - }); - } - ); + + // only create example project if not joining existing org + if (!providerOrganizationName) { + await initProjectHelper({ + projectName: "Example Project" + }); + } + + localStorage.setItem("orgData.id", orgId); + navigate({ + to: "/organization/projects" + }); + } catch (error) { + setIsLoading(false); + console.error(error); + } + }; + + await completeSignupFlow(); + } catch (error) { + setIsLoading(false); + console.error(error); + } } else { setIsLoading(false); } diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx index da5e00fbed..5498a29e99 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx @@ -8,7 +8,6 @@ import { useNavigate } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import attemptChangePassword from "@app/components/utilities/attemptChangePassword"; import checkPassword from "@app/components/utilities/checks/password/checkPassword"; import { Button, FormControl, Input } from "@app/components/v2"; import { useUser } from "@app/context"; @@ -68,10 +67,9 @@ export const ChangePasswordSection = () => { newPassword }); } else { - await attemptChangePassword({ - email: user.username, - currentPassword: oldPassword, - newPassword + createNotification({ + text: "Legacy encryption scheme not supported for changing password. Please log out and log back in before changing your password.", + type: "error" }); } diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/EmergencyKitSection/EmergencyKitSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/EmergencyKitSection/EmergencyKitSection.tsx deleted file mode 100644 index 1cf0d336e1..0000000000 --- a/frontend/src/pages/user/PersonalSettingsPage/components/EmergencyKitSection/EmergencyKitSection.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Controller, useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; - -import { createNotification } from "@app/components/notifications"; -import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey"; -import { Button, FormControl, Input } from "@app/components/v2"; -import { useUser } from "@app/context"; - -const schema = z - .object({ - password: z.string().describe("Password is required") - }) - .required(); - -export type FormData = z.infer; - -export const EmergencyKitSection = () => { - const { user } = useUser(); - const { reset, control, handleSubmit } = useForm({ - defaultValues: { - password: "" - }, - resolver: zodResolver(schema) - }); - - const onFormSubmit = ({ password }: FormData) => { - try { - if (!user?.email) return; - - issueBackupKey({ - email: user.email, - password, - personalName: `${user.firstName} ${user.lastName}`, - setBackupKeyError: () => {}, - setBackupKeyIssued: () => {} - }); - - reset(); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to download emergency kit", - type: "error" - }); - } - }; - - return ( -
-

Emergency Kit

-

- The kit contains information you can use to recover your account. -

-
- ( - - - - )} - control={control} - name="password" - /> -
- -
- ); -}; diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/EmergencyKitSection/index.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/EmergencyKitSection/index.tsx deleted file mode 100644 index c817666b1d..0000000000 --- a/frontend/src/pages/user/PersonalSettingsPage/components/EmergencyKitSection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { EmergencyKitSection } from "./EmergencyKitSection"; diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx index dcf35e6726..025c2d0e43 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx @@ -1,20 +1,12 @@ -import { useUser } from "@app/context"; -import { UserEncryptionVersion } from "@app/hooks/api/auth/types"; - import { DeleteAccountSection } from "../DeleteAccountSection"; -import { EmergencyKitSection } from "../EmergencyKitSection"; import { SessionsSection } from "../SessionsSection"; import { UserNameSection } from "../UserNameSection"; export const PersonalGeneralTab = () => { - const { user } = useUser(); - const encryptionVersion = user?.encryptionVersion ?? UserEncryptionVersion.V2; - return (
- {encryptionVersion === UserEncryptionVersion.V1 && }
);