From 777dfd5f58db183410696bec7507b0683097d1b0 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 10 Jun 2024 23:41:35 +0530 Subject: [PATCH] feat: api changes for srp handover --- .../20240609133400_private-key-handoff.ts | 61 +++++++++++ .../src/db/schemas/user-encryption-keys.ts | 7 +- .../ldap-config/ldap-config-service.ts | 10 +- .../saml-config/saml-config-service.ts | 7 +- backend/src/lib/config/env.ts | 2 +- backend/src/lib/crypto/srp.ts | 25 +++-- backend/src/server/routes/v1/sso-router.ts | 46 ++++++++ backend/src/server/routes/v1/user-router.ts | 29 ++++- backend/src/server/routes/v3/login-router.ts | 6 +- backend/src/server/routes/v3/signup-router.ts | 4 +- backend/src/services/auth/auth-fns.ts | 4 +- .../src/services/auth/auth-login-service.ts | 102 ++++++++++++++++-- backend/src/services/auth/auth-login-type.ts | 8 ++ .../src/services/auth/auth-signup-service.ts | 45 +++++++- backend/src/services/auth/auth-signup-type.ts | 2 + backend/src/services/user/user-service.ts | 20 +++- 16 files changed, 344 insertions(+), 34 deletions(-) create mode 100644 backend/src/db/migrations/20240609133400_private-key-handoff.ts diff --git a/backend/src/db/migrations/20240609133400_private-key-handoff.ts b/backend/src/db/migrations/20240609133400_private-key-handoff.ts new file mode 100644 index 000000000..8c71eab80 --- /dev/null +++ b/backend/src/db/migrations/20240609133400_private-key-handoff.ts @@ -0,0 +1,61 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "password"); + const doesPrivateKeyFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKey" + ); + const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyIV" + ); + const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyTag" + ); + const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyEncoding" + ); + if (await knex.schema.hasTable(TableName.UserEncryptionKey)) { + await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => { + if (!doesPasswordFieldExist) t.string("password"); + if (!doesPrivateKeyFieldExist) t.text("serverEncryptedPrivateKey"); + if (!doesPrivateKeyIVFieldExist) t.text("serverEncryptedPrivateKeyIV"); + if (!doesPrivateKeyTagFieldExist) t.text("serverEncryptedPrivateKeyTag"); + if (!doesPrivateKeyEncodingFieldExist) t.text("serverEncryptedPrivateKeyEncoding"); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "password"); + const doesPrivateKeyFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKey" + ); + const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyIV" + ); + const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyTag" + ); + const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyEncoding" + ); + if (await knex.schema.hasTable(TableName.UserEncryptionKey)) { + await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => { + if (doesPasswordFieldExist) t.dropColumn("password"); + if (doesPrivateKeyFieldExist) t.dropColumn("serverEncryptedPrivateKey"); + if (doesPrivateKeyIVFieldExist) t.dropColumn("serverEncryptedPrivateKeyIV"); + if (doesPrivateKeyTagFieldExist) t.dropColumn("serverEncryptedPrivateKeyTag"); + if (doesPrivateKeyEncodingFieldExist) t.dropColumn("serverEncryptedPrivateKeyEncoding"); + }); + } +} diff --git a/backend/src/db/schemas/user-encryption-keys.ts b/backend/src/db/schemas/user-encryption-keys.ts index 693b73b4c..6afab529d 100644 --- a/backend/src/db/schemas/user-encryption-keys.ts +++ b/backend/src/db/schemas/user-encryption-keys.ts @@ -21,7 +21,12 @@ export const UserEncryptionKeysSchema = z.object({ tag: z.string(), salt: z.string(), verifier: z.string(), - userId: z.string().uuid() + userId: z.string().uuid(), + password: z.string().nullable().optional(), + serverEncryptedPrivateKey: z.string().nullable().optional(), + serverEncryptedPrivateKeyIV: z.string().nullable().optional(), + serverEncryptedPrivateKeyTag: z.string().nullable().optional(), + serverEncryptedPrivateKeyEncoding: z.string().nullable().optional() }); export type TUserEncryptionKeys = z.infer; 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 dd49bd0ae..8027f5907 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -73,7 +73,13 @@ type TLdapConfigServiceFactoryDep = { >; userDAL: Pick< TUserDALFactory, - "create" | "findOne" | "transaction" | "updateById" | "findUserEncKeyByUserIdsBatch" | "find" + | "create" + | "findOne" + | "transaction" + | "updateById" + | "findUserEncKeyByUserIdsBatch" + | "find" + | "findUserEncKeyByUserId" >; userAliasDAL: Pick; permissionService: Pick; @@ -592,12 +598,14 @@ export const ldapConfigServiceFactory = ({ }); const isUserCompleted = Boolean(user.isAccepted); + const userEnc = await userDAL.findUserEncKeyByUserId(user.id); const providerAuthToken = jwt.sign( { authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user.id, username: user.username, + hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey), ...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }), firstName, lastName, diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 5d7b7ec3b..3cc51e1c2 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -41,7 +41,10 @@ import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } f type TSamlConfigServiceFactoryDep = { samlConfigDAL: Pick; - userDAL: Pick; + userDAL: Pick< + TUserDALFactory, + "create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId" + >; userAliasDAL: Pick; orgDAL: Pick< TOrgDALFactory, @@ -452,6 +455,7 @@ export const samlConfigServiceFactory = ({ await licenseService.updateSubscriptionOrgMemberCount(organization.id); const isUserCompleted = Boolean(user.isAccepted); + const userEnc = await userDAL.findUserEncKeyByUserId(user.id); const providerAuthToken = jwt.sign( { authTokenType: AuthTokenType.PROVIDER_TOKEN, @@ -464,6 +468,7 @@ export const samlConfigServiceFactory = ({ organizationId: organization.id, organizationSlug: organization.slug, authMethod: authProvider, + hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey), authType: UserAliasType.SAML, isUserCompleted, ...(relayState diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 80a2111fc..f4da71293 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -29,7 +29,7 @@ const envSchema = z DB_USER: zpStr(z.string().describe("Postgres database username").optional()), DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()), DB_NAME: zpStr(z.string().describe("Postgres database name").optional()), - + BCRYPT_SALT_ROUND: z.number().default(12), NODE_ENV: z.enum(["development", "test", "production"]).default("production"), SALT_ROUNDS: z.coerce.number().default(10), INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()), diff --git a/backend/src/lib/crypto/srp.ts b/backend/src/lib/crypto/srp.ts index bc29cdb3f..8d7ea656a 100644 --- a/backend/src/lib/crypto/srp.ts +++ b/backend/src/lib/crypto/srp.ts @@ -6,7 +6,7 @@ import tweetnacl from "tweetnacl-util"; import { TUserEncryptionKeys } from "@app/db/schemas"; -import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption"; +import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption"; export const generateSrpServerKey = async (salt: string, verifier: string) => { // eslint-disable-next-line new-cap @@ -97,7 +97,13 @@ export const generateUserSrpKeys = async (email: string, password: string) => { }; }; -export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => { +export const getUserPrivateKey = async ( + password: string, + user: Pick< + TUserEncryptionKeys, + "protectedKeyTag" | "protectedKey" | "protectedKeyIV" | "encryptedPrivateKey" | "iv" | "salt" | "tag" + > +) => { const derivedKey = await argon2.hash(password, { salt: Buffer.from(user.salt), memoryCost: 65536, @@ -108,17 +114,18 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK raw: true }); if (!derivedKey) throw new Error("Failed to derive key from password"); - const key = decryptSymmetric({ - ciphertext: user.protectedKey!, - iv: user.protectedKeyIV!, - tag: user.protectedKeyTag!, - key: derivedKey.toString("base64") + const key = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: user.protectedKey as string, + iv: user.protectedKeyIV as string, + tag: user.protectedKeyTag as string, + key: derivedKey }); - const privateKey = decryptSymmetric({ + + const privateKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: user.encryptedPrivateKey, iv: user.iv, tag: user.tag, - key + key: Buffer.from(key, "hex") }); return privateKey; }; diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts index 60bbec7db..3d8d02e6f 100644 --- a/backend/src/server/routes/v1/sso-router.ts +++ b/backend/src/server/routes/v1/sso-router.ts @@ -259,4 +259,50 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { ); } }); + + server.route({ + url: "/token-exchange", + method: "POST", + schema: { + body: z.object({ + providerAuthToken: z.string(), + email: z.string() + }) + }, + handler: async (req, res) => { + const userAgent = req.headers["user-agent"]; + if (!userAgent) throw new Error("user agent header is required"); + + const data = await server.services.login.oauth2TokenExchange({ + email: req.body.email, + ip: req.realIp, + userAgent, + providerAuthToken: req.body.providerAuthToken + }); + + if (data.isMfaEnabled) { + return { mfaEnabled: true, token: data.token } as const; // for discriminated union + } + + void res.setCookie("jid", data.token.refresh, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + return { + mfaEnabled: false, + encryptionVersion: data.user.encryptionVersion, + token: data.token.access, + publicKey: data.user.publicKey, + encryptedPrivateKey: data.user.encryptedPrivateKey, + iv: data.user.iv, + tag: data.user.tag, + protectedKey: data.user.protectedKey || null, + protectedKeyIV: data.user.protectedKeyIV || null, + protectedKeyTag: data.user.protectedKeyTag || null + } as const; + } + }); }; diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index 3d9f531b9..eb2c1004b 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -19,7 +19,14 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true })) + user: UsersSchema.merge( + UserEncryptionKeysSchema.omit({ + verifier: true, + serverEncryptedPrivateKey: true, + serverEncryptedPrivateKeyIV: true, + serverEncryptedPrivateKeyTag: true + }) + ) }) } }, @@ -30,6 +37,26 @@ 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/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 4c7df5612..61a0c74e5 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -81,7 +81,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { email: z.string().trim(), providerAuthToken: z.string().trim().optional(), clientProof: z.string().trim(), - captchaToken: z.string().trim().optional() + captchaToken: z.string().trim().optional(), + password: z.string().optional() }), response: { 200: z.discriminatedUnion("mfaEnabled", [ @@ -112,7 +113,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { ip: req.realIp, userAgent, providerAuthToken: req.body.providerAuthToken, - clientProof: req.body.clientProof + clientProof: req.body.clientProof, + password: req.body.password }); if (data.isMfaEnabled) { diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index ac43df36d..59131464a 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -102,7 +102,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { verifier: z.string().trim(), organizationName: z.string().trim().min(1), providerAuthToken: z.string().trim().optional().nullish(), - attributionSource: z.string().trim().optional() + attributionSource: z.string().trim().optional(), + password: z.string() }), response: { 200: z.object({ @@ -167,6 +168,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { schema: { body: z.object({ email: z.string().email().trim(), + password: z.string(), firstName: z.string().trim(), lastName: z.string().trim().optional(), protectedKey: z.string().trim(), diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index ecbf73a48..e8574a8b9 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -15,10 +15,10 @@ export const validateProviderAuthToken = (providerToken: string, username?: stri if (decodedToken.username !== username) throw new Error("Invalid auth credentials"); if (decodedToken.organizationId) { - return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod }; + return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod, userName: decodedToken.username }; } - return { authMethod: decodedToken.authMethod, orgId: null }; + return { authMethod: decodedToken.authMethod, orgId: null, userName: decodedToken.username }; }; export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => { diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index a136508e7..2fecb5857 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -1,3 +1,4 @@ +import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { TUsers, UserDeviceSchema } from "@app/db/schemas"; @@ -5,6 +6,8 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; @@ -19,6 +22,7 @@ import { TLoginClientProofDTO, TLoginGenServerPublicKeyDTO, TOauthLoginDTO, + TOauthTokenExchangeDTO, TVerifyMfaTokenDTO } from "./auth-login-type"; import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type"; @@ -101,7 +105,7 @@ export const authLoginServiceFactory = ({ user: TUsers; ip: string; userAgent: string; - organizationId: string | undefined; + organizationId?: string; authMethod: AuthMethod; }) => { const cfg = getConfig(); @@ -178,7 +182,8 @@ export const authLoginServiceFactory = ({ ip, userAgent, providerAuthToken, - captchaToken + captchaToken, + password }: TLoginClientProofDTO) => { const appCfg = getConfig(); @@ -248,14 +253,29 @@ export const authLoginServiceFactory = ({ throw new Error("Failed to authenticate. Try again?"); } - await userDAL.updateUserEncryptionByUserId(userEnc.userId, { - serverPrivateKey: null, - clientPublicKey: null - }); - await userDAL.updateById(userEnc.userId, { consecutiveFailedPasswordAttempts: 0 }); + // from password decrypt the private key + if (password) { + const privateKey = await getUserPrivateKey(password, userEnc); + const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND); + const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey); + await userDAL.updateUserEncryptionByUserId(userEnc.userId, { + serverPrivateKey: null, + clientPublicKey: null, + password: hashedPassword, + serverEncryptedPrivateKey: ciphertext, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyEncoding: encoding + }); + } else { + await userDAL.updateUserEncryptionByUserId(userEnc.userId, { + serverPrivateKey: null, + clientPublicKey: null + }); + } // send multi factor auth token if they it enabled if (userEnc.isMfaEnabled && userEnc.email) { @@ -499,8 +519,14 @@ export const authLoginServiceFactory = ({ authMethods: [authMethod], isGhost: false }); + } else { + const isLinkingRequired = !user?.authMethods?.includes(authMethod); + if (isLinkingRequired) { + user = await userDAL.updateById(user.id, { authMethods: [...(user.authMethods || []), authMethod] }); + } } - const isLinkingRequired = !user?.authMethods?.includes(authMethod); + + const userEnc = await userDAL.findUserEncKeyByUserId(user.id); const isUserCompleted = user.isAccepted; const providerAuthToken = jwt.sign( { @@ -511,9 +537,9 @@ export const authLoginServiceFactory = ({ isEmailVerified: user.isEmailVerified, firstName: user.firstName, lastName: user.lastName, + hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey), authMethod, isUserCompleted, - isLinkingRequired, ...(callbackPort ? { callbackPort @@ -525,10 +551,65 @@ export const authLoginServiceFactory = ({ expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME } ); - return { isUserCompleted, providerAuthToken }; }; + // to login users with oauth2 token used for private key handoff + // The provider token will be given back to client to send back infisical access token + // why not directly sending access token? + // 1. To keep the logic change easier from SRP oauth to simple oauth + // 2. I don't want to attach access token to url as it may get logged the provider token has very short life span + const oauth2TokenExchange = async ({ userAgent, ip, providerAuthToken, email }: TOauthTokenExchangeDTO) => { + const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); + + const appCfg = getConfig(); + const { authMethod, userName } = decodedProviderToken; + if (!userName) throw new BadRequestError({ message: "Missing user name" }); + const organizationId = + (isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId + ? decodedProviderToken.orgId + : undefined; + + const user = await userDAL.findUserEncKeyByUsername({ + username: email + }); + if (!user) throw new BadRequestError({ message: "Invalid token" }); + if (!user.serverEncryptedPrivateKey) throw new BadRequestError({ message: "Private key handoff needs to be done" }); + // send multi factor auth token if they it enabled + if (user.isMfaEnabled && user.email) { + enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd); + + const mfaToken = jwt.sign( + { + authMethod, + authTokenType: AuthTokenType.MFA_TOKEN, + userId: user.userId + }, + appCfg.AUTH_SECRET, + { + expiresIn: appCfg.JWT_MFA_LIFETIME + } + ); + + await sendUserMfaCode({ + userId: user.id, + email: user.email + }); + + return { isMfaEnabled: true, token: mfaToken } as const; + } + + const token = await generateUserTokens({ + user: { ...user, id: user.userId }, + ip, + userAgent, + authMethod, + organizationId + }); + + return { token, isMfaEnabled: false, user } as const; + }; + /* * logout user by incrementing the version by 1 meaning any old session will become invalid * as there number is behind @@ -542,6 +623,7 @@ export const authLoginServiceFactory = ({ loginExchangeClientProof, logout, oauth2Login, + oauth2TokenExchange, resendMfaToken, verifyMfaToken, selectOrganization, diff --git a/backend/src/services/auth/auth-login-type.ts b/backend/src/services/auth/auth-login-type.ts index 4f73ec996..db57d730e 100644 --- a/backend/src/services/auth/auth-login-type.ts +++ b/backend/src/services/auth/auth-login-type.ts @@ -13,6 +13,7 @@ export type TLoginClientProofDTO = { ip: string; userAgent: string; captchaToken?: string; + password?: string; }; export type TVerifyMfaTokenDTO = { @@ -31,3 +32,10 @@ export type TOauthLoginDTO = { authMethod: AuthMethod; callbackPort?: string; }; + +export type TOauthTokenExchangeDTO = { + providerAuthToken: string; + ip: string; + userAgent: string; + email: string; +}; diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 528cb44fa..a574c6050 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -1,3 +1,4 @@ +import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { OrgMembershipStatus, TableName } from "@app/db/schemas"; @@ -6,6 +7,8 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou 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 { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError } from "@app/lib/errors"; import { isDisposableEmail } from "@app/lib/validator"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; @@ -119,6 +122,7 @@ export const authSignupServiceFactory = ({ const completeEmailAccountSignup = async ({ email, + password, firstName, lastName, providerAuthToken, @@ -137,6 +141,7 @@ export const authSignupServiceFactory = ({ userAgent, authorization }: TCompleteAccountSignupDTO) => { + const appCfg = getConfig(); const user = await userDAL.findOne({ username: email }); if (!user || (user && user.isAccepted)) { throw new Error("Failed to complete account for complete user"); @@ -152,6 +157,17 @@ export const authSignupServiceFactory = ({ validateSignUpAuthorization(authorization, user.id); } + const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND); + const privateKey = await getUserPrivateKey(password, { + salt, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + }); + const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(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"); @@ -166,7 +182,12 @@ export const authSignupServiceFactory = ({ protectedKeyTag, encryptedPrivateKey, iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag + tag: encryptedPrivateKeyTag, + password: hashedPassword, + serverEncryptedPrivateKeyEncoding: encoding, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKey: ciphertext }, tx ); @@ -227,7 +248,6 @@ export const authSignupServiceFactory = ({ userId: updateduser.info.id }); if (!tokenSession) throw new Error("Failed to create token"); - const appCfg = getConfig(); const accessToken = jwt.sign( { @@ -265,6 +285,7 @@ export const authSignupServiceFactory = ({ ip, salt, email, + password, verifier, firstName, publicKey, @@ -295,6 +316,18 @@ export const authSignupServiceFactory = ({ name: "complete account invite" }); + const appCfg = getConfig(); + const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND); + const privateKey = await getUserPrivateKey(password, { + salt, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + }); + const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(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"); @@ -310,7 +343,12 @@ export const authSignupServiceFactory = ({ protectedKeyTag, encryptedPrivateKey, iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag + tag: encryptedPrivateKeyTag, + password: hashedPassword, + serverEncryptedPrivateKeyEncoding: encoding, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKey: ciphertext }, tx ); @@ -343,7 +381,6 @@ export const authSignupServiceFactory = ({ userId: updateduser.info.id }); if (!tokenSession) throw new Error("Failed to create token"); - const appCfg = getConfig(); const accessToken = jwt.sign( { diff --git a/backend/src/services/auth/auth-signup-type.ts b/backend/src/services/auth/auth-signup-type.ts index a37a1cd96..9cd70f8c7 100644 --- a/backend/src/services/auth/auth-signup-type.ts +++ b/backend/src/services/auth/auth-signup-type.ts @@ -1,5 +1,6 @@ export type TCompleteAccountSignupDTO = { email: string; + password: string; firstName: string; lastName?: string; protectedKey: string; @@ -21,6 +22,7 @@ export type TCompleteAccountSignupDTO = { export type TCompleteAccountInviteDTO = { email: string; + password: string; firstName: string; lastName?: string; protectedKey: string; diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index 693078fbd..4ee8bdc1f 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,3 +1,5 @@ +import { SecretKeyEncoding } from "@app/db/schemas"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; @@ -230,6 +232,21 @@ export const userServiceFactory = ({ ); }; + const getUserPrivateKey = async (userId: string) => { + const user = await userDAL.findUserEncKeyByUserId(userId); + if (!user?.serverEncryptedPrivateKey || !user.serverEncryptedPrivateKeyIV || !user.serverEncryptedPrivateKeyTag) { + throw new BadRequestError({ message: "Private key not found. Please login again" }); + } + const privateKey = infisicalSymmetricDecrypt({ + ciphertext: user.serverEncryptedPrivateKey, + tag: user.serverEncryptedPrivateKeyTag, + iv: user.serverEncryptedPrivateKeyIV, + keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding + }); + + return privateKey; + }; + return { sendEmailVerificationCode, verifyEmailVerificationCode, @@ -240,6 +257,7 @@ export const userServiceFactory = ({ getMe, createUserAction, getUserAction, - unlockUser + unlockUser, + getUserPrivateKey }; };