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 000000000..4eb93e86a --- /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(): Promise { + // do nothing for now to avoid breaking down migrations +} diff --git a/backend/src/db/schemas/user-encryption-keys.ts b/backend/src/db/schemas/user-encryption-keys.ts index fd9d21a9d..cfe61fdc5 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 2aee85fb1..f1a70ca05 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 5c6245382..43ce4dadf 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 26f96eafb..47a41a95c 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -1,9 +1,28 @@ import { Knex } from "knex"; +import { initEnvConfig } from "@app/lib/config/env"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; +import { generateUserSrpKeys } from "@app/lib/crypto/srp"; +import { initLogger, logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { AuthMethod } from "@app/services/auth/auth-type"; +import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns"; +import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal"; +import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; +import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; +import { userDALFactory } from "@app/services/user/user-dal"; -import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; -import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data"; +import { + OrgMembershipRole, + OrgMembershipStatus, + ProjectMembershipRole, + ProjectType, + SecretEncryptionAlgo, + SecretKeyEncoding, + TableName +} from "../schemas"; +import { seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ { name: "Development", slug: "dev" }, @@ -11,12 +30,159 @@ export const DEFAULT_PROJECT_ENVS = [ { name: "Production", slug: "prod" } ]; +const createUserWithGhostUser = async ( + orgId: string, + projectId: string, + userId: string, + userOrgMembershipId: string, + knex: Knex +) => { + const projectKeyDAL = projectKeyDALFactory(knex); + const userDAL = userDALFactory(knex); + const projectMembershipDAL = projectMembershipDALFactory(knex); + const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex); + + const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key. + + const password = crypto.randomBytes(128).toString("hex"); + + const [ghostUser] = await knex(TableName.Users) + .insert({ + isGhost: true, + authMethods: [AuthMethod.EMAIL], + username: email, + email, + isAccepted: true + }) + .returning("*"); + + const encKeys = await generateUserSrpKeys(email, password); + + await knex(TableName.UserEncryptionKey) + .insert({ userId: ghostUser.id, encryptionVersion: 2, publicKey: encKeys.publicKey }) + .onConflict("userId") + .merge(); + + await knex(TableName.OrgMembership) + .insert({ + orgId, + userId: ghostUser.id, + role: OrgMembershipRole.Admin, + status: OrgMembershipStatus.Accepted, + isActive: true + }) + .returning("*"); + + const [projectMembership] = await knex(TableName.ProjectMembership) + .insert({ + userId: ghostUser.id, + projectId + }) + .returning("*"); + + await knex(TableName.ProjectUserMembershipRole).insert({ + projectMembershipId: projectMembership.id, + role: ProjectMembershipRole.Admin + }); + + const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ + publicKey: encKeys.publicKey, + privateKey: encKeys.plainPrivateKey + }); + + await knex(TableName.ProjectKeys).insert({ + projectId, + receiverId: ghostUser.id, + encryptedKey: encryptedProjectKey, + nonce: encryptedProjectKeyIv, + senderId: ghostUser.id + }); + + const { iv, tag, ciphertext, encoding, algorithm } = crypto + .encryption() + .symmetric() + .encryptWithRootEncryptionKey(encKeys.plainPrivateKey); + + await knex(TableName.ProjectBot).insert({ + name: "Infisical Bot (Ghost)", + projectId, + tag, + iv, + encryptedProjectKey, + encryptedProjectKeyNonce: encryptedProjectKeyIv, + encryptedPrivateKey: ciphertext, + isActive: true, + publicKey: encKeys.publicKey, + senderId: ghostUser.id, + algorithm, + keyEncoding: encoding + }); + + const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, knex); + + if (!latestKey) { + throw new Error("Latest key not found for user"); + } + + const user = await userDAL.findUserEncKeyByUserId(userId, knex); + + if (!user || !user.publicKey) { + throw new Error("User not found"); + } + + const [projectAdmin] = assignWorkspaceKeysToMembers({ + decryptKey: latestKey, + userPrivateKey: encKeys.plainPrivateKey, + members: [ + { + userPublicKey: user.publicKey, + orgMembershipId: userOrgMembershipId + } + ] + }); + + // Create a membership for the user + const userProjectMembership = await projectMembershipDAL.create( + { + projectId, + userId: user.id + }, + knex + ); + await projectUserMembershipRoleDAL.create( + { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, + knex + ); + + // Create a project key for the user + await projectKeyDAL.create( + { + encryptedKey: projectAdmin.workspaceEncryptedKey, + nonce: projectAdmin.workspaceEncryptedNonce, + senderId: ghostUser.id, + receiverId: user.id, + projectId + }, + knex + ); + + return { + user: ghostUser, + keys: encKeys + }; +}; + export async function seed(knex: Knex): Promise { // Deletes ALL existing entries await knex(TableName.Project).del(); 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, @@ -29,29 +195,24 @@ export async function seed(knex: Knex): Promise { }) .returning("*"); - const projectMembership = await knex(TableName.ProjectMembership) - .insert({ - projectId: project.id, + const userOrgMembership = await knex(TableName.OrgMembership) + .where({ + orgId: seedData1.organization.id, userId: seedData1.id }) - .returning("*"); - await knex(TableName.ProjectUserMembershipRole).insert({ - role: ProjectMembershipRole.Admin, - projectMembershipId: projectMembership[0].id - }); + .first(); + if (!userOrgMembership) { + throw new Error("User org membership not found"); + } const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first(); if (!user) throw new Error("User not found"); - const userPrivateKey = await getUserPrivateKey(seedData1.password, user); - const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey); - await knex(TableName.ProjectKeys).insert({ - projectId: project.id, - nonce: projectKey.nonce, - encryptedKey: projectKey.ciphertext, - receiverId: seedData1.id, - senderId: seedData1.id - }); + if (!user.publicKey) { + throw new Error("User public key not found"); + } + + await createUserWithGhostUser(seedData1.organization.id, project.id, seedData1.id, userOrgMembership.id, knex); // create default environments and default folders const envs = await knex(TableName.Environment) diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index 391f785ec..ae3d04514 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/event/event-sse-service.ts b/backend/src/ee/services/event/event-sse-service.ts index f8c025e77..bb85f667c 100644 --- a/backend/src/ee/services/event/event-sse-service.ts +++ b/backend/src/ee/services/event/event-sse-service.ts @@ -13,11 +13,9 @@ const AUTH_REFRESH_INTERVAL = 60 * 1000; const HEART_BEAT_INTERVAL = 15 * 1000; export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => { - let heartbeatInterval: NodeJS.Timeout | null = null; - const clients = new Set(); - heartbeatInterval = setInterval(() => { + const heartbeatInterval = setInterval(() => { for (const client of clients) { if (client.stream.closed) continue; void client.ping(); diff --git a/backend/src/ee/services/event/event-sse-stream.ts b/backend/src/ee/services/event/event-sse-stream.ts index d73eccb42..258783c2e 100644 --- a/backend/src/ee/services/event/event-sse-stream.ts +++ b/backend/src/ee/services/event/event-sse-stream.ts @@ -66,15 +66,24 @@ export type EventStreamClient = { }; export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient { - const rules = options.registered.map((r) => ({ - subject: options.type, - action: "subscribe", - conditions: { - eventType: r.event, - secretPath: r.conditions?.secretPath ?? "/", - environment: r.conditions?.environmentSlug - } - })); + const rules = options.registered.map((r) => { + const secretPath = r.conditions?.secretPath; + const hasConditions = r.conditions?.environmentSlug || r.conditions?.secretPath; + + return { + subject: options.type, + action: "subscribe", + conditions: { + eventType: r.event, + ...(hasConditions + ? { + environment: r.conditions?.environmentSlug ?? "", + secretPath: { $glob: secretPath } + } + : {}) + } + }; + }); const id = `sse-${nanoid()}`; const control = new AbortController(); diff --git a/backend/src/ee/services/group/group-fns.ts b/backend/src/ee/services/group/group-fns.ts index 436b0b79e..56f8df6c0 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 55665c146..46c9831b0 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 1d7c5fc71..e91f7a47b 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 f3aac5b92..b1a10056c 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 ca6f67235..101f744c3 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 a87d67b94..9cc6e134d 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/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b4bdcb4fa..0cc272f15 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -70,7 +70,8 @@ export enum ApiDocsTags { SecretScanning = "Secret Scanning", OidcSso = "OIDC SSO", SamlSso = "SAML SSO", - LdapSso = "LDAP SSO" + LdapSso = "LDAP SSO", + Events = "Event Subscriptions" } export const GROUPS = { @@ -2872,3 +2873,10 @@ export const LdapSso = { caCert: "The CA certificate to use when verifying the LDAP server certificate." } }; + +export const EventSubscriptions = { + SUBSCRIBE_PROJECT_EVENTS: { + projectId: "The ID of the project to subscribe to events for.", + register: "List of events you want to subscribe to" + } +}; diff --git a/backend/src/lib/crypto/secret-encryption.ts b/backend/src/lib/crypto/secret-encryption.ts index 7355d4bd6..f22c918ac 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 3f403405e..2623e5756 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 5dabdac6b..64cfb140e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -775,7 +775,6 @@ export const registerRoutes = async ( orgRoleDAL, permissionService, orgDAL, - projectBotDAL, incidentContactDAL, tokenService, projectUserAdditionalPrivilegeDAL, @@ -850,7 +849,6 @@ export const registerRoutes = async ( projectDAL, permissionService, projectUserMembershipRoleDAL, - userDAL, projectBotDAL, projectKeyDAL, projectMembershipDAL @@ -1138,11 +1136,9 @@ export const registerRoutes = async ( projectBotService, identityProjectDAL, identityOrgMembershipDAL, - projectKeyDAL, userDAL, projectEnvDAL, orgDAL, - orgService, projectMembershipDAL, projectRoleDAL, folderDAL, @@ -1162,7 +1158,6 @@ export const registerRoutes = async ( identityProjectMembershipRoleDAL, keyStore, kmsService, - projectBotDAL, certificateTemplateDAL, projectSlackConfigDAL, slackIntegrationDAL, diff --git a/backend/src/server/routes/v1/event-router.ts b/backend/src/server/routes/v1/event-router.ts index b8a043db5..19dc296bf 100644 --- a/backend/src/server/routes/v1/event-router.ts +++ b/backend/src/server/routes/v1/event-router.ts @@ -7,6 +7,7 @@ import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream"; import { EventRegisterSchema } from "@app/ee/services/event/types"; import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ApiDocsTags, EventSubscriptions } from "@app/lib/api-docs"; import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -20,10 +21,14 @@ export const registerEventRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Events], + description: "Subscribe to project events", body: z.object({ - projectId: z.string().trim(), - register: z.array(EventRegisterSchema).max(10) - }) + projectId: z.string().trim().describe(EventSubscriptions.SUBSCRIBE_PROJECT_EVENTS.projectId), + register: z.array(EventRegisterSchema).min(1).max(10) + }), + produces: ["text/event-stream"] }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, reply) => { @@ -75,13 +80,15 @@ export const registerEventRouter = async (server: FastifyZodProvider) => { } req.body.register.forEach((r) => { + const fields = { + environment: r.conditions?.environmentSlug ?? "", + secretPath: r.conditions?.secretPath ?? "/", + eventType: r.event + }; + const allowed = info.permission.can( ProjectPermissionSecretActions.Subscribe, - subject(ProjectPermissionSub.Secrets, { - environment: r.conditions?.environmentSlug ?? "", - secretPath: r.conditions?.secretPath ?? "/", - eventType: r.event - }) + subject(ProjectPermissionSub.Secrets, fields) ); if (!allowed) { @@ -89,9 +96,9 @@ export const registerEventRouter = async (server: FastifyZodProvider) => { name: "PermissionDenied", message: `You are not allowed to subscribe on secrets`, details: { - event: r.event, - environmentSlug: r.conditions?.environmentSlug, - secretPath: r.conditions?.secretPath ?? "/" + event: fields.eventType, + environmentSlug: fields.environment, + secretPath: fields.secretPath } }); } diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index e1669c784..323354bc1 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 3396cebe9..72d20db57 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 6f981f61f..7a5a9341f 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 a0c3592f7..7ef2e0d33 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,9 @@ 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.extend({ + encryptionVersion: z.number() + }) }) } }, @@ -94,26 +80,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 59a3943f7..d8a57d29a 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 87e1d1789..9135d2356 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/password-router.ts b/backend/src/server/routes/v2/password-router.ts index 63b6d8aac..0f0747c3f 100644 --- a/backend/src/server/routes/v2/password-router.ts +++ b/backend/src/server/routes/v2/password-router.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { getConfig } from "@app/lib/config/env"; import { authRateLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns"; @@ -41,13 +42,38 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { rateLimit: authRateLimit }, onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), - handler: async (req) => { + handler: async (req, res) => { + const appCfg = getConfig(); + await server.services.password.resetPasswordV2({ type: ResetPasswordV2Type.LoggedInReset, userId: req.permission.id, newPassword: req.body.newPassword, oldPassword: req.body.oldPassword }); + + void res.cookie("jid", "", { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + void res.cookie("infisical-project-assume-privileges", "", { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED, + maxAge: 0 + }); + + void res.cookie("aod", "", { + httpOnly: false, + path: "/", + sameSite: "lax", + secure: appCfg.HTTPS_ENABLED, + maxAge: 0 + }); } }); }; diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 2a883dcb6..b71a744fd 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 3a8510f34..07923fd6c 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 } = 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 }; + } + }); }; diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 391c459a2..736bc13aa 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 b38275c8b..d490486cb 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 aea90fe11..7dd1d3aa4 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,80 @@ 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))) { + await userDAL.update( + { id: userEnc.userId }, + { + $incr: { + consecutiveFailedPasswordAttempts: 1 + } + } + ); + + throw new BadRequestError({ message: "Invalid username or email" }); + } + + const token = await generateUserTokens({ + user: { + ...userEnc, + id: userEnc.userId + }, + ip, + userAgent, + authMethod, + organizationId + }); + + return { + tokens: { + accessToken: token.access, + refreshToken: token.refresh + }, + user: userEnc + } as const; + }; + const selectOrganization = async ({ userAgent, authJwtToken, @@ -862,6 +905,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 c0629f561..efc8b3cc0 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 */ @@ -215,58 +137,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); @@ -317,66 +198,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)) { @@ -420,21 +241,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, @@ -470,15 +277,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 @@ -491,12 +290,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 b3b14c3b4..ceb70d411 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 96cf4121a..7bc3a5ef6 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 8bbf302c5..5c4fdc3c9 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/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index 3fa7e8d5e..908741bde 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { isValidIp } from "@app/lib/ip"; import { isFQDN } from "@app/lib/validator/validate-url"; +import { TAltNameMapping, TAltNameType } from "@app/services/certificate/certificate-types"; const isValidDate = (dateString: string) => { const date = new Date(dateString); @@ -56,3 +57,19 @@ export const validateAltNamesField = z message: "Each alt name must be a valid hostname, email address, IP address or URL" } ); + +export const validateAndMapAltNameType = (name: string): TAltNameMapping | null => { + if (isFQDN(name, { allow_wildcard: true, require_tld: false })) { + return { type: TAltNameType.DNS, value: name }; + } + if (z.string().url().safeParse(name).success) { + return { type: TAltNameType.URL, value: name }; + } + if (z.string().email().safeParse(name).success) { + return { type: TAltNameType.EMAIL, value: name }; + } + if (isValidIp(name)) { + return { type: TAltNameType.IP, value: name }; + } + return null; +}; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts index eace4e47a..6811476f8 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts @@ -1,7 +1,6 @@ /* eslint-disable no-bitwise */ import * as x509 from "@peculiar/x509"; import RE2 from "re2"; -import { z } from "zod"; import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; @@ -9,7 +8,6 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; -import { isFQDN } from "@app/lib/validator/validate-url"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; @@ -17,7 +15,8 @@ import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage, - CertStatus + CertStatus, + TAltNameMapping } from "@app/services/certificate/certificate-types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -34,6 +33,7 @@ import { } from "../certificate-authority-fns"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types"; +import { validateAndMapAltNameType } from "../certificate-authority-validators"; type TInternalCertificateAuthorityFnsDeps = { certificateAuthorityDAL: Pick; @@ -152,27 +152,15 @@ export const InternalCertificateAuthorityFns = ({ extensions.push(extendedKeyUsagesExtension); } - let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = []; + let altNamesArray: TAltNameMapping[] = []; if (subscriber.subjectAlternativeNames?.length) { altNamesArray = subscriber.subjectAlternativeNames.map((altName) => { - if (z.string().email().safeParse(altName).success) { - return { type: "email", value: altName }; + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); } - - if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) { - return { type: "dns", value: altName }; - } - - if (z.string().url().safeParse(altName).success) { - return { type: "url", value: altName }; - } - - if (z.string().ip().safeParse(altName).success) { - return { type: "ip", value: altName }; - } - - throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); + return altNameType; }); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); @@ -426,27 +414,15 @@ export const InternalCertificateAuthorityFns = ({ ); } - let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = []; + let altNamesArray: TAltNameMapping[] = []; if (altNames) { altNamesArray = altNames.split(",").map((altName) => { - if (z.string().email().safeParse(altName).success) { - return { type: "email", value: altName }; + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); } - - if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) { - return { type: "dns", value: altName }; - } - - if (z.string().url().safeParse(altName).success) { - return { type: "url", value: altName }; - } - - if (z.string().ip().safeParse(altName).success) { - return { type: "ip", value: altName }; - } - - throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); + return altNameType; }); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index dd30cc62e..510a160e9 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -2,7 +2,6 @@ import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import slugify from "@sindresorhus/slugify"; -import { z } from "zod"; import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -18,7 +17,6 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { isFQDN } from "@app/lib/validator/validate-url"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -34,7 +32,8 @@ import { CertExtendedKeyUsageOIDToName, CertKeyAlgorithm, CertKeyUsage, - CertStatus + CertStatus, + TAltNameMapping } from "../../certificate/certificate-types"; import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal"; import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns"; @@ -69,6 +68,7 @@ import { TSignIntermediateDTO, TUpdateCaDTO } from "./internal-certificate-authority-types"; +import { validateAndMapAltNameType } from "../certificate-authority-validators"; type TInternalCertificateAuthorityServiceFactoryDep = { certificateAuthorityDAL: Pick< @@ -1364,34 +1364,18 @@ export const internalCertificateAuthorityServiceFactory = ({ ); } - let altNamesArray: { - type: "email" | "dns"; - value: string; - }[] = []; + let altNamesArray: TAltNameMapping[] = []; if (altNames) { altNamesArray = altNames .split(",") .map((name) => name.trim()) - .map((altName) => { - // check if the altName is a valid email - if (z.string().email().safeParse(altName).success) { - return { - type: "email", - value: altName - }; + .map((altName): TAltNameMapping => { + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new Error(`Invalid altName: ${altName}`); } - - // check if the altName is a valid hostname - if (isFQDN(altName, { allow_wildcard: true })) { - return { - type: "dns", - value: altName - }; - } - - // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly - throw new Error(`Invalid altName: ${altName}`); + return altNameType; }); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); @@ -1766,34 +1750,22 @@ export const internalCertificateAuthorityServiceFactory = ({ } let altNamesFromCsr: string = ""; - let altNamesArray: { - type: "email" | "dns"; - value: string; - }[] = []; + let altNamesArray: TAltNameMapping[] = []; + if (altNames) { altNamesArray = altNames .split(",") .map((name) => name.trim()) - .map((altName) => { - // check if the altName is a valid email - if (z.string().email().safeParse(altName).success) { - return { - type: "email", - value: altName - }; + .map((altName): TAltNameMapping => { + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new Error(`Invalid altName: ${altName}`); } - - // check if the altName is a valid hostname - if (isFQDN(altName, { allow_wildcard: true })) { - return { - type: "dns", - value: altName - }; - } - - // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly - throw new Error(`Invalid altName: ${altName}`); + return altNameType; }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); } else { // attempt to read from CSR if altNames is not explicitly provided const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17"); @@ -1801,11 +1773,16 @@ export const internalCertificateAuthorityServiceFactory = ({ const sanNames = new x509.GeneralNames(sanExtension.value); altNamesArray = sanNames.items - .filter((value) => value.type === "email" || value.type === "dns") - .map((name) => ({ - type: name.type as "email" | "dns", - value: name.value - })); + .filter( + (value) => value.type === "email" || value.type === "dns" || value.type === "url" || value.type === "ip" + ) + .map((name): TAltNameMapping => { + const altNameType = validateAndMapAltNameType(name.value); + if (!altNameType) { + throw new Error(`Invalid altName from CSR: ${name.value}`); + } + return altNameType; + }); altNamesFromCsr = sanNames.items.map((item) => item.value).join(","); } diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index f1c79a36f..527df2a39 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -104,3 +104,14 @@ export type TGetCertificateCredentialsDTO = { projectDAL: Pick; kmsService: Pick; }; + +export enum TAltNameType { + EMAIL = "email", + DNS = "dns", + IP = "ip", + URL = "url" +} +export type TAltNameMapping = { + type: TAltNameType; + value: string; +}; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 50a08e94f..838d89437 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 6640412c3..3dec0c7f8 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -1,19 +1,16 @@ 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"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; -import { TUserDALFactory } from "../user/user-dal"; import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types"; type TOrgAdminServiceFactoryDep = { @@ -25,7 +22,6 @@ type TOrgAdminServiceFactoryDep = { >; projectKeyDAL: Pick; projectBotDAL: Pick; - userDAL: Pick; projectUserMembershipRoleDAL: Pick; smtpService: Pick; }; @@ -38,7 +34,6 @@ export const orgAdminServiceFactory = ({ projectMembershipDAL, projectKeyDAL, projectBotDAL, - userDAL, projectUserMembershipRoleDAL, smtpService }: TOrgAdminServiceFactoryDep) => { @@ -83,7 +78,7 @@ export const orgAdminServiceFactory = ({ actorAuthMethod, projectId }: TAccessProjectDTO) => { - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -98,8 +93,10 @@ export const orgAdminServiceFactory = ({ const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId }); if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); - if (project.version === ProjectVersion.V1) { - throw new BadRequestError({ message: "Please upgrade your project on your dashboard" }); + if (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2) { + throw new BadRequestError({ + message: `Project '${project.name}' is a legacy project and must be upgraded before accessing it through the admin console.` + }); } // check already there exist a membership if there return it @@ -144,30 +141,6 @@ 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 +154,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 ca51ff57a..5779fa7d3 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"; @@ -137,7 +134,6 @@ type TOrgServiceFactoryDep = { >; projectUserAdditionalPrivilegeDAL: Pick; projectRoleDAL: Pick; - projectBotDAL: Pick; projectUserMembershipRoleDAL: Pick; projectBotService: Pick; loginService: Pick; @@ -169,7 +165,6 @@ export const orgServiceFactory = ({ projectRoleDAL, samlConfigDAL, oidcConfigDAL, - projectBotDAL, projectUserMembershipRoleDAL, identityMetadataDAL, projectBotService, @@ -287,15 +282,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 ); @@ -885,29 +872,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 ); @@ -1069,106 +1037,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, @@ -1191,16 +1059,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 d26669b86..029f320df 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 ea4ed813c..bb91b9c85 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 f166ec04f..fca0074d7 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 0d7c7dd55..e557b7c96 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, @@ -290,6 +294,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 153f627fc..4261870b1 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 02f89bc38..ceef78f6a 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -117,7 +117,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/reminder/reminder-service.ts b/backend/src/services/reminder/reminder-service.ts index 3c4276b56..181f0cdb9 100644 --- a/backend/src/services/reminder/reminder-service.ts +++ b/backend/src/services/reminder/reminder-service.ts @@ -42,7 +42,7 @@ export const reminderServiceFactory = ({ const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise => { if (!newRecipients || newRecipients.length === 0) { // If no recipients provided, remove all existing recipients - await reminderRecipientDAL.deleteById(reminderId); + await reminderRecipientDAL.delete({ reminderId }); return; } diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b2258447e..30bf750c3 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"; @@ -175,7 +173,11 @@ export const userServiceFactory = ({ const getMe = async (userId: string) => { const user = await userDAL.findUserEncKeyByUserId(userId); if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" }); - return user; + + return { + ...user, + encryptionVersion: user.encryptionVersion! + }; }; const deleteUser = async (userId: string) => { @@ -212,25 +214,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 +294,6 @@ export const userServiceFactory = ({ listUserGroups, getUserAction, unlockUser, - getUserPrivateKey, getAllMyAccounts, getUserProjectFavorites, removeMyDuplicateAccounts, diff --git a/docs/api-reference/endpoints/events/project-events.mdx b/docs/api-reference/endpoints/events/project-events.mdx new file mode 100644 index 000000000..9ceb0439e --- /dev/null +++ b/docs/api-reference/endpoints/events/project-events.mdx @@ -0,0 +1,4 @@ +--- +title: "Project Events" +openapi: "POST /api/v1/events/subscribe/project-events" +--- \ No newline at end of file diff --git a/docs/docs.json b/docs/docs.json index 938d86030..5656b9c41 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -41,6 +41,8 @@ "group": "Platform Reference", "pages": [ "documentation/platform/organization", + "documentation/platform/event-subscriptions", + "documentation/platform/folder", { "group": "Projects", "pages": [ @@ -777,6 +779,10 @@ "group": "Admin", "pages": ["api-reference/endpoints/admin/bootstrap-instance"] }, + { + "group": "Events", + "pages": ["api-reference/endpoints/events/project-events"] + }, { "group": "Identities", "pages": [ diff --git a/docs/documentation/platform/event-subscriptions.mdx b/docs/documentation/platform/event-subscriptions.mdx new file mode 100644 index 000000000..ca6ceebd1 --- /dev/null +++ b/docs/documentation/platform/event-subscriptions.mdx @@ -0,0 +1,118 @@ +--- +title: "Event Subscriptions" +sidebarTitle: "Events" +description: "Subscribe to events in Infisical for real-time updates" +--- + + + **Note:** Event Subscriptions is a paid feature. - **Infisical Cloud users:** Event Subscriptions is available under + the **Enterprise Tier**. - **Self-Hosted Infisical:** Please contact [sales@infisical.com](mailto:sales@infisical.com) + to purchase an enterprise license. + + +Event Subscriptions in Infisical allow you to receive real-time notifications when specific actions occur within your account or organization. These notifications include changes to secrets, users, teams, and many more **coming soon**. + +## How It Works + +- Server receives message over pubsub connection indicating changes have occurred +- Server processes the change notification +- Updated data is synchronized across all connected Infisical instances +- Client applications receive real-time updates through [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) +- All servers maintain consistent state without manual intervention + +This ensures your infrastructure stays up-to-date automatically, without requiring restarts or manual synchronization. + + + Event Subscriptions are designed for real-time communication and do not include persistence or replay + capabilities—events are delivered once and are not stored for future retrieval. + + +## Supported Resources + +You can currently subscribe to notifications for the following resources and event types: + +- **Secrets** + - `secret:created`: Triggered when a secret is created + - `secret:updated`: Triggered when a secret is updated + - `secret:deleted`: Triggered when a secret is deleted + +## Permissions Setup + +To receive events on a supported resource, the identity must have `Subscribe` action permission on that resource. + +Follow these steps to set up the necessary permissions: + + + + ![Select Project](/images/platform/events/select-project.png) + +On your project page, open **Project Settings** from the sidebar. + +In the Project name section, click **Copy Project ID** to copy your Project ID, or extract it from the URL: +`https://app.infisical.com/project//settings` + + + + + ![Project Detail](/images/platform/events/project-detail.png) ![Project + Access](/images/platform/events/project-access.png) Navigate to **Access Management**, then select **Project Roles**. + + + + ![Project Role](/images/platform/events/project-role.png) You can either edit an existing role or create a new role + for event subscriptions. + + + + ![Role Detail](/images/platform/events/role-detail.png) Select the specific resources that the role should have access + to. ![Add policy](/images/platform/events/add-policy.png) + + + + ![Policy setting](/images/platform/events/policy-setting.png) + + Ensure the **Subscribe** action is selected for the relevant resources and events. + + ## Conditions + + By default, the role will have access to all events for the selected resources in this project. + + + + ![Policy setting](/images/platform/events/access-full.png) + + + ![Policy setting](/images/platform/events/access-path.png) + + + ![Policy setting](/images/platform/events/access-dev.png) + + + + + +## Getting Started + +Currently, events are only available via [API](/api-reference/endpoints/events) but will soon be available in our SDKs, Kubernetes Operator, and more. + +### API Usage + +You need an auth token to use this API. To get an authentication token, follow the authentication guide for one of our supported auth methods from the [machine identities documentation](/documentation/platform/identities/machine-identities#authentication-methods). + +#### Creating a Subscription + +![Postman Subscription](/images/platform/events/postman-subscribe.png) + +**Request Parameters:** + +- `projectId`: Project whose events you want to subscribe to +- `register`: List of event filters + - `conditions`: Conditions to filter events on + - `environmentSlug`: Project environment + - `secretPath`: Path of the secrets + +![Postman Subscription Response](/images/platform/events/postman-sse-response.png) + +The subscribe endpoint responds with a `text/event-stream` content type to initiate SSE streaming. + +For more specific details, please refer to our [API Reference](/api-reference/endpoints/events). diff --git a/docs/documentation/platform/secret-rotation/azure-client-secret.mdx b/docs/documentation/platform/secret-rotation/azure-client-secret.mdx index 046b772f6..c2da0a6a1 100644 --- a/docs/documentation/platform/secret-rotation/azure-client-secret.mdx +++ b/docs/documentation/platform/secret-rotation/azure-client-secret.mdx @@ -6,6 +6,7 @@ description: "Learn how to automatically rotate Azure Client Secrets." ## Prerequisites - Create an [Azure Client Secret Connection](/integrations/app-connections/azure-client-secrets). +- Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create an Azure Client Secret Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/ldap-password.mdx b/docs/documentation/platform/secret-rotation/ldap-password.mdx index feb3a664d..ba75e0af7 100644 --- a/docs/documentation/platform/secret-rotation/ldap-password.mdx +++ b/docs/documentation/platform/secret-rotation/ldap-password.mdx @@ -14,6 +14,7 @@ description: "Learn how to automatically rotate LDAP passwords." ## Prerequisites - Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements +- Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create an LDAP Password Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx index c20622f26..f609d11c5 100644 --- a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx @@ -30,6 +30,7 @@ An example creation statement might look like: To learn more about Microsoft SQL Server's permission system, please visit their [documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver16). +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create a Microsoft SQL Server Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/mysql-credentials.mdx b/docs/documentation/platform/secret-rotation/mysql-credentials.mdx index d0088a29e..349b5b893 100644 --- a/docs/documentation/platform/secret-rotation/mysql-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/mysql-credentials.mdx @@ -25,7 +25,7 @@ description: "Learn how to automatically rotate MySQL credentials." To learn more about the MySQL permission system, please visit their [documentation](https://dev.mysql.com/doc/refman/8.4/en/grant.html). - +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create a MySQL Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx b/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx index fb0887d33..0f72d62fa 100644 --- a/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx @@ -31,6 +31,7 @@ description: "Learn how to automatically rotate Oracle Database credentials." To learn more about the Oracle Database permission system, please visit their [documentation](https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-privilege-and-role-authorization.html). +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create an Oracle Database Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx index 55175d967..e8cddb0f4 100644 --- a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx @@ -27,6 +27,7 @@ description: "Learn how to automatically rotate PostgreSQL credentials." To learn more about PostgreSQL's permission system, please visit their [documentation](https://www.postgresql.org/docs/current/sql-grant.html). +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create a PostgreSQL Credentials Rotation in Infisical diff --git a/docs/images/platform/events/access-dev.png b/docs/images/platform/events/access-dev.png new file mode 100644 index 000000000..3f44fc51b Binary files /dev/null and b/docs/images/platform/events/access-dev.png differ diff --git a/docs/images/platform/events/access-full.png b/docs/images/platform/events/access-full.png new file mode 100644 index 000000000..d41260c17 Binary files /dev/null and b/docs/images/platform/events/access-full.png differ diff --git a/docs/images/platform/events/access-path.png b/docs/images/platform/events/access-path.png new file mode 100644 index 000000000..3bfa9b28b Binary files /dev/null and b/docs/images/platform/events/access-path.png differ diff --git a/docs/images/platform/events/add-auth-method-form.png b/docs/images/platform/events/add-auth-method-form.png new file mode 100644 index 000000000..ba181ff48 Binary files /dev/null and b/docs/images/platform/events/add-auth-method-form.png differ diff --git a/docs/images/platform/events/add-auth-method.png b/docs/images/platform/events/add-auth-method.png new file mode 100644 index 000000000..583f13d09 Binary files /dev/null and b/docs/images/platform/events/add-auth-method.png differ diff --git a/docs/images/platform/events/add-conditions.png b/docs/images/platform/events/add-conditions.png new file mode 100644 index 000000000..bf0580b22 Binary files /dev/null and b/docs/images/platform/events/add-conditions.png differ diff --git a/docs/images/platform/events/add-policy.png b/docs/images/platform/events/add-policy.png new file mode 100644 index 000000000..00e6b76f0 Binary files /dev/null and b/docs/images/platform/events/add-policy.png differ diff --git a/docs/images/platform/events/copy-generated-token.png b/docs/images/platform/events/copy-generated-token.png new file mode 100644 index 000000000..1fb061017 Binary files /dev/null and b/docs/images/platform/events/copy-generated-token.png differ diff --git a/docs/images/platform/events/generate-auth-token-add.png b/docs/images/platform/events/generate-auth-token-add.png new file mode 100644 index 000000000..2057e2509 Binary files /dev/null and b/docs/images/platform/events/generate-auth-token-add.png differ diff --git a/docs/images/platform/events/generate-auth-token-create.png b/docs/images/platform/events/generate-auth-token-create.png new file mode 100644 index 000000000..9bccbb16f Binary files /dev/null and b/docs/images/platform/events/generate-auth-token-create.png differ diff --git a/docs/images/platform/events/generate-auth-token.png b/docs/images/platform/events/generate-auth-token.png new file mode 100644 index 000000000..40d6336a6 Binary files /dev/null and b/docs/images/platform/events/generate-auth-token.png differ diff --git a/docs/images/platform/events/identity-add-project.png b/docs/images/platform/events/identity-add-project.png new file mode 100644 index 000000000..5fdaa488e Binary files /dev/null and b/docs/images/platform/events/identity-add-project.png differ diff --git a/docs/images/platform/events/identity-add-role.png b/docs/images/platform/events/identity-add-role.png new file mode 100644 index 000000000..c20296cf8 Binary files /dev/null and b/docs/images/platform/events/identity-add-role.png differ diff --git a/docs/images/platform/events/identity-selector.png b/docs/images/platform/events/identity-selector.png new file mode 100644 index 000000000..2aaf7e56b Binary files /dev/null and b/docs/images/platform/events/identity-selector.png differ diff --git a/docs/images/platform/events/org-access-control.png b/docs/images/platform/events/org-access-control.png new file mode 100644 index 000000000..334d6dbc3 Binary files /dev/null and b/docs/images/platform/events/org-access-control.png differ diff --git a/docs/images/platform/events/policy-setting.png b/docs/images/platform/events/policy-setting.png new file mode 100644 index 000000000..cadab9b82 Binary files /dev/null and b/docs/images/platform/events/policy-setting.png differ diff --git a/docs/images/platform/events/postman-sse-response.png b/docs/images/platform/events/postman-sse-response.png new file mode 100644 index 000000000..fea1054b4 Binary files /dev/null and b/docs/images/platform/events/postman-sse-response.png differ diff --git a/docs/images/platform/events/postman-subscribe.png b/docs/images/platform/events/postman-subscribe.png new file mode 100644 index 000000000..308d257c0 Binary files /dev/null and b/docs/images/platform/events/postman-subscribe.png differ diff --git a/docs/images/platform/events/project-access.png b/docs/images/platform/events/project-access.png new file mode 100644 index 000000000..61593befd Binary files /dev/null and b/docs/images/platform/events/project-access.png differ diff --git a/docs/images/platform/events/project-detail.png b/docs/images/platform/events/project-detail.png new file mode 100644 index 000000000..65ff15642 Binary files /dev/null and b/docs/images/platform/events/project-detail.png differ diff --git a/docs/images/platform/events/project-role.png b/docs/images/platform/events/project-role.png new file mode 100644 index 000000000..b6178939f Binary files /dev/null and b/docs/images/platform/events/project-role.png differ diff --git a/docs/images/platform/events/role-detail.png b/docs/images/platform/events/role-detail.png new file mode 100644 index 000000000..7b4d6cea6 Binary files /dev/null and b/docs/images/platform/events/role-detail.png differ diff --git a/docs/images/platform/events/select-project.png b/docs/images/platform/events/select-project.png new file mode 100644 index 000000000..f964bf443 Binary files /dev/null and b/docs/images/platform/events/select-project.png differ diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 32fd0c12f..0a2d52ce1 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx index 91c606b0a..241c0350a 100644 --- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx index f4aaa7edd..34a8eb9a8 100644 --- a/docs/integrations/secret-syncs/azure-app-configuration.mdx +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal diff --git a/docs/integrations/secret-syncs/azure-devops.mdx b/docs/integrations/secret-syncs/azure-devops.mdx index 2f99fe128..b8c0331f8 100644 --- a/docs/integrations/secret-syncs/azure-devops.mdx +++ b/docs/integrations/secret-syncs/azure-devops.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a Azure DevOps Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [Azure DevOps Connection](/integrations/app-connections/azure-devops) +- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx index d19a0162e..e4cb82a04 100644 --- a/docs/integrations/secret-syncs/azure-key-vault.mdx +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx index afce51a0c..1ef31c997 100644 --- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -11,6 +11,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical." ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png) ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png) ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-service-usage-api.png) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index 14b2d9a7f..ed76c5edb 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create a [GitHub Connection](/integrations/app-connections/github) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/gitlab.mdx b/docs/integrations/secret-syncs/gitlab.mdx index 5e6cacffa..3f468ac0e 100644 --- a/docs/integrations/secret-syncs/gitlab.mdx +++ b/docs/integrations/secret-syncs/gitlab.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a GitLab Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create a [GitLab Connection](/integrations/app-connections/gitlab) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx index 396b4d13f..105d42395 100644 --- a/docs/integrations/secret-syncs/oci-vault.mdx +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -14,6 +14,7 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f - Create an [OCI Connection](/integrations/app-connections/oci) with the required **Secret Sync** permissions - [Create](https://docs.oracle.com/en-us/iaas/Content/Identity/compartments/To_create_a_compartment.htm) or use an existing OCI Compartment (which the OCI Connection is authorized to access) - [Create](https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/managingvaults_topic-To_create_a_new_vault.htm#createnewvault) or use an existing OCI Vault +- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/frontend/src/components/auth/UserInfoStep.tsx b/frontend/src/components/auth/UserInfoStep.tsx index 2a1f8dca1..2eb6c65f6 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,41 @@ export default function UserInfoStep({ }); if (!errorCheck) { - // Generate a random pair of a public and a private key - const pair = await generateKeyPair(config.fipsEnabled); + try { + const response = await completeAccountSignup({ + email, + password, + firstName: name.split(" ")[0], + lastName: name.split(" ").slice(1).join(" "), + providerAuthToken, + organizationName, + attributionSource + }); - localStorage.setItem("PRIVATE_KEY", pair.privateKey); + // unset signup JWT token and set JWT token + SecurityClient.setSignupToken(""); + SecurityClient.setToken(response.token); + SecurityClient.setProviderAuthToken(""); - 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 - }); - - if (!derivedKey) throw new Error("Failed to derive key from password"); - - 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 e51168525..000000000 --- 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 d897ebeaa..f8339aecf 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: "info" + }); + + 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 14a61d817..000000000 --- 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 c371d9c91..bdaf79042 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: "info" + }); + + 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 10f36ec39..000000000 --- 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 52a502e4e..000000000 --- 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 1539438f4..000000000 --- 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 796fd3152..cb7e3f64c 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 cbd20b643..fd9d6fb5c 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 75edc0907..77289fee6 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -495,14 +495,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 d0d6cd5dd..d722b68ea 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 2d6aeb9d6..19c1537a2 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 86dff5288..9666a1af4 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 311d613ef..a134220e8 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 3e3a5909f..f331e4bda 100644 --- a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx @@ -123,11 +123,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"; @@ -142,7 +139,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 a4ebe7a11..e05e0f2ad 100644 --- a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx +++ b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx @@ -1,23 +1,14 @@ -/* 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 +19,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 +55,6 @@ export const SignupInvitePage = () => { const metadata = queryParams.get("metadata") || undefined; const { mutateAsync: selectOrganization } = useSelectOrganization(); - const { config } = useServerConfig(); const loggedIn = isLoggedIn(); @@ -94,123 +81,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 85343e79e..1b10ed94c 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/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 5e9832763..910e07eac 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -1207,13 +1207,16 @@ export const OverviewPage = () => { secretsToDeleteKeys={secretsToDeleteKeys} usedBySecretSyncs={usedBySecretSyncs} /> -
+
setScrollOffset(e.currentTarget.scrollLeft)} - className="thin-scrollbar rounded-b-none" + className="thin-scrollbar max-h-[66vh] overflow-y-auto rounded-b-none" > - + { })} onClick={() => handleExploreEnvClick(slug)} > -

{name}

+

{name}

{!collapseEnvironments && missingKeyCount > 0 && ( { - if (secret.secretValueHidden) { + if (secret.secretValueHidden && !isPending) { return canEditSecretValue ? HIDDEN_SECRET_VALUE : ""; } return secret.valueOverride || secret.value || ""; @@ -413,7 +413,7 @@ export const SecretItem = memo( tabIndex={0} role="button" > - {secretValueHidden && !isOverriden && ( + {secretValueHidden && !isOverriden && !isPending && ( @@ -444,12 +444,14 @@ export const SecretItem = memo( )} diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx index da5e00fbe..32f697a40 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx @@ -8,12 +8,12 @@ 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"; import { useResetUserPasswordV2, useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries"; import { UserEncryptionVersion } from "@app/hooks/api/auth/types"; +import { clearSession } from "@app/hooks/api/users/queries"; type Errors = { tooShort?: string; @@ -62,19 +62,22 @@ export const ChangePasswordSection = () => { if (errorCheck) return; setIsLoading(true); - if (user.encryptionVersion === UserEncryptionVersion.V2) { - await resetPasswordV2({ - oldPassword, - newPassword - }); - } else { - await attemptChangePassword({ - email: user.username, - currentPassword: oldPassword, - newPassword + if (user.encryptionVersion !== UserEncryptionVersion.V2) { + createNotification({ + text: "Legacy encryption scheme not supported for changing password. Please contact support.", + type: "error" }); + + setIsLoading(false); + return; } + await resetPasswordV2({ + oldPassword, + newPassword + }); + clearSession(); + setIsLoading(false); createNotification({ text: "Successfully changed password", 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 1cf0d336e..000000000 --- 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 c817666b1..000000000 --- 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 dcf35e672..025c2d0e4 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 && }
);