feat: srp removal

This commit is contained in:
Daniel Hougaard
2025-07-30 04:25:27 +04:00
parent 75622ed03e
commit d5dd2e8bfd
68 changed files with 817 additions and 2410 deletions
@@ -0,0 +1,18 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.UserEncryptionKey, (table) => {
table.text("encryptedPrivateKey").nullable().alter();
table.text("publicKey").nullable().alter();
table.text("iv").nullable().alter();
table.text("tag").nullable().alter();
table.text("salt").nullable().alter();
table.text("verifier").nullable().alter();
});
}
export async function down(knex: Knex): Promise<void> {
// do nothing for now to avoid breaking down migrations
}
@@ -0,0 +1,25 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const AccessApprovalPoliciesEnvironmentsSchema = z.object({
id: z.string().uuid(),
policyId: z.string().uuid(),
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TAccessApprovalPoliciesEnvironments = z.infer<typeof AccessApprovalPoliciesEnvironmentsSchema>;
export type TAccessApprovalPoliciesEnvironmentsInsert = Omit<
z.input<typeof AccessApprovalPoliciesEnvironmentsSchema>,
TImmutableDBKeys
>;
export type TAccessApprovalPoliciesEnvironmentsUpdate = Partial<
Omit<z.input<typeof AccessApprovalPoliciesEnvironmentsSchema>, TImmutableDBKeys>
>;
+2 -1
View File
@@ -30,7 +30,8 @@ export const ProjectsSchema = z.object({
hasDeleteProtection: z.boolean().default(false).nullable().optional(), hasDeleteProtection: z.boolean().default(false).nullable().optional(),
secretSharing: z.boolean().default(true), secretSharing: z.boolean().default(true),
showSnapshotsLegacy: z.boolean().default(false), showSnapshotsLegacy: z.boolean().default(false),
defaultProduct: z.string().nullable().optional() defaultProduct: z.string().nullable().optional(),
secretDetectionIgnoreValues: z.string().array().nullable().optional()
}); });
export type TProjects = z.infer<typeof ProjectsSchema>; export type TProjects = z.infer<typeof ProjectsSchema>;
@@ -0,0 +1,25 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesEnvironmentsSchema = z.object({
id: z.string().uuid(),
policyId: z.string().uuid(),
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretApprovalPoliciesEnvironments = z.infer<typeof SecretApprovalPoliciesEnvironmentsSchema>;
export type TSecretApprovalPoliciesEnvironmentsInsert = Omit<
z.input<typeof SecretApprovalPoliciesEnvironmentsSchema>,
TImmutableDBKeys
>;
export type TSecretApprovalPoliciesEnvironmentsUpdate = Partial<
Omit<z.input<typeof SecretApprovalPoliciesEnvironmentsSchema>, TImmutableDBKeys>
>;
@@ -15,12 +15,12 @@ export const UserEncryptionKeysSchema = z.object({
protectedKey: z.string().nullable().optional(), protectedKey: z.string().nullable().optional(),
protectedKeyIV: z.string().nullable().optional(), protectedKeyIV: z.string().nullable().optional(),
protectedKeyTag: z.string().nullable().optional(), protectedKeyTag: z.string().nullable().optional(),
publicKey: z.string(), publicKey: z.string().nullable().optional(),
encryptedPrivateKey: z.string(), encryptedPrivateKey: z.string().nullable().optional(),
iv: z.string(), iv: z.string().nullable().optional(),
tag: z.string(), tag: z.string().nullable().optional(),
salt: z.string(), salt: z.string().nullable().optional(),
verifier: z.string(), verifier: z.string().nullable().optional(),
userId: z.string().uuid(), userId: z.string().uuid(),
hashedPassword: z.string().nullable().optional(), hashedPassword: z.string().nullable().optional(),
serverEncryptedPrivateKey: z.string().nullable().optional(), serverEncryptedPrivateKey: z.string().nullable().optional(),
+4
View File
@@ -115,6 +115,10 @@ export const generateUserSrpKeys = async (password: string) => {
}; };
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => { 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, { const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt), salt: Buffer.from(user.salt),
memoryCost: 65536, memoryCost: 65536,
+4 -3
View File
@@ -1,7 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { crypto } from "@app/lib/crypto"; import { initEnvConfig } from "@app/lib/config/env";
import { initLogger } from "@app/lib/logger"; import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { AuthMethod } from "../../services/auth/auth-type"; import { AuthMethod } from "../../services/auth/auth-type";
@@ -17,7 +17,7 @@ export async function seed(knex: Knex): Promise<void> {
initLogger(); initLogger();
const superAdminDAL = superAdminDALFactory(knex); const superAdminDAL = superAdminDALFactory(knex);
await crypto.initialize(superAdminDAL); await initEnvConfig(superAdminDAL, logger);
await knex(TableName.SuperAdmin).insert([ await knex(TableName.SuperAdmin).insert([
// eslint-disable-next-line // eslint-disable-next-line
@@ -25,6 +25,7 @@ export async function seed(knex: Knex): Promise<void> {
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true } { id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
]); ]);
// Inserts seed entries // Inserts seed entries
const [user] = await knex(TableName.Users) const [user] = await knex(TableName.Users)
.insert([ .insert([
{ {
+12
View File
@@ -1,6 +1,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { initEnvConfig } from "@app/lib/config/env";
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data"; import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
@@ -17,6 +20,11 @@ export async function seed(knex: Knex): Promise<void> {
await knex(TableName.Environment).del(); await knex(TableName.Environment).del();
await knex(TableName.SecretFolder).del(); await knex(TableName.SecretFolder).del();
initLogger();
const superAdminDAL = superAdminDALFactory(knex);
await initEnvConfig(superAdminDAL, logger);
const [project] = await knex(TableName.Project) const [project] = await knex(TableName.Project)
.insert({ .insert({
name: seedData1.project.name, name: seedData1.project.name,
@@ -43,6 +51,10 @@ export async function seed(knex: Knex): Promise<void> {
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first(); const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
if (!user) throw new Error("User not found"); if (!user) throw new Error("User not found");
if (!user.publicKey) {
throw new Error("User public key not found");
}
const userPrivateKey = await getUserPrivateKey(seedData1.password, user); const userPrivateKey = await getUserPrivateKey(seedData1.password, user);
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey); const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey);
await knex(TableName.ProjectKeys).insert({ await knex(TableName.ProjectKeys).insert({
@@ -1,6 +1,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { initEnvConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; 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 { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
import { seedData1 } from "../seed-data"; import { seedData1 } from "../seed-data";
@@ -10,6 +13,11 @@ export async function seed(knex: Knex): Promise<void> {
await knex(TableName.Identity).del(); await knex(TableName.Identity).del();
await knex(TableName.IdentityOrgMembership).del(); await knex(TableName.IdentityOrgMembership).del();
initLogger();
const superAdminDAL = superAdminDALFactory(knex);
await initEnvConfig(superAdminDAL, logger);
// Inserts seed entries // Inserts seed entries
await knex(TableName.Identity).insert([ await knex(TableName.Identity).insert([
{ {
+25 -1
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex"; 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 { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors"; 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}`)); const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
for await (const projectId of projectIds) { 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}`)); const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
if (usersToAddProjectKeyFor.length) { 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); const bot = await projectBotDAL.findOne({ projectId }, tx);
if (!bot) { if (!bot) {
@@ -112,6 +130,12 @@ const addAcceptedUsersToGroup = async ({
}); });
const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => { 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 const { ciphertext: encryptedKey, nonce } = crypto
.encryption() .encryption()
.asymmetric() .asymmetric()
@@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = {
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find" "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
>; >;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
+3 -3
View File
@@ -65,7 +65,7 @@ export type TAddUsersToGroup = {
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">; userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx: Knex; tx: Knex;
}; };
@@ -78,7 +78,7 @@ export type TAddUsersToGroupByUserIds = {
orgDAL: Pick<TOrgDALFactory, "findMembership">; orgDAL: Pick<TOrgDALFactory, "findMembership">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex; tx?: Knex;
}; };
@@ -102,7 +102,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex; tx?: Knex;
}; };
@@ -55,7 +55,7 @@ type TLdapConfigServiceFactoryDep = {
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">; groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
@@ -79,7 +79,7 @@ type TOidcConfigServiceFactoryDep = {
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">; auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
+1 -1
View File
@@ -59,7 +59,7 @@ type TScimServiceFactoryDep = {
TOrgMembershipDALFactory, TOrgMembershipDALFactory,
"find" | "findOne" | "create" | "updateById" | "findById" | "update" "find" | "findOne" | "create" | "updateById" | "findById" | "update"
>; >;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser" | "findById">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
+17 -1
View File
@@ -53,7 +53,7 @@ type DecryptedIntegrationAuths = z.infer<typeof DecryptedIntegrationAuthsSchema>
type TLatestKey = TProjectKeys & { type TLatestKey = TProjectKeys & {
sender: { sender: {
publicKey: string; publicKey?: string;
}; };
}; };
@@ -91,6 +91,10 @@ const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: s
return results; return results;
}; };
export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => { 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({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
@@ -143,6 +147,10 @@ export const decryptSecretVersions = (
privateKey: string, privateKey: string,
latestKey: TLatestKey latestKey: TLatestKey
) => { ) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
@@ -195,6 +203,10 @@ export const decryptSecretApprovals = (
privateKey: string, privateKey: string,
latestKey: TLatestKey latestKey: TLatestKey
) => { ) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
@@ -247,6 +259,10 @@ export const decryptIntegrationAuths = (
privateKey: string, privateKey: string,
latestKey: TLatestKey latestKey: TLatestKey
) => { ) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
+18
View File
@@ -4,6 +4,7 @@ import jsrp from "jsrp";
import { TUserEncryptionKeys } from "@app/db/schemas"; import { TUserEncryptionKeys } from "@app/db/schemas";
import { UserEncryption } from "@app/services/user/user-types"; import { UserEncryption } from "@app/services/user/user-types";
import { BadRequestError } from "../errors";
import { crypto, SymmetricKeySize } from "./cryptography"; import { crypto, SymmetricKeySize } from "./cryptography";
export const generateSrpServerKey = async (salt: string, verifier: string) => { export const generateSrpServerKey = async (salt: string, verifier: string) => {
@@ -127,6 +128,10 @@ export const getUserPrivateKey = async (
> >
) => { ) => {
if (user.encryptionVersion === UserEncryption.V1) { 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 return crypto
.encryption() .encryption()
.symmetric() .symmetric()
@@ -138,12 +143,25 @@ export const getUserPrivateKey = async (
keySize: SymmetricKeySize.Bits128 keySize: SymmetricKeySize.Bits128
}); });
} }
// still used for legacy things
if ( if (
user.encryptionVersion === UserEncryption.V2 && user.encryptionVersion === UserEncryption.V2 &&
user.protectedKey && user.protectedKey &&
user.protectedKeyIV && user.protectedKeyIV &&
user.protectedKeyTag 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, { const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt), salt: Buffer.from(user.salt),
memoryCost: 65536, memoryCost: 65536,
-4
View File
@@ -761,7 +761,6 @@ export const registerRoutes = async (
orgRoleDAL, orgRoleDAL,
permissionService, permissionService,
orgDAL, orgDAL,
projectBotDAL,
incidentContactDAL, incidentContactDAL,
tokenService, tokenService,
projectUserAdditionalPrivilegeDAL, projectUserAdditionalPrivilegeDAL,
@@ -1114,11 +1113,9 @@ export const registerRoutes = async (
projectBotService, projectBotService,
identityProjectDAL, identityProjectDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
projectKeyDAL,
userDAL, userDAL,
projectEnvDAL, projectEnvDAL,
orgDAL, orgDAL,
orgService,
projectMembershipDAL, projectMembershipDAL,
projectRoleDAL, projectRoleDAL,
folderDAL, folderDAL,
@@ -1138,7 +1135,6 @@ export const registerRoutes = async (
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
keyStore, keyStore,
kmsService, kmsService,
projectBotDAL,
certificateTemplateDAL, certificateTemplateDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
@@ -247,7 +247,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
lastName: true, lastName: true,
id: true, id: true,
superAdmin: true superAdmin: true
}).merge(z.object({ publicKey: z.string().nullable() })) }).merge(z.object({ publicKey: z.string().nullable().optional() }))
}) })
) )
.omit({ createdAt: true, updatedAt: true }) .omit({ createdAt: true, updatedAt: true })
@@ -9,73 +9,6 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { UserEncryption } from "@app/services/user/user-types"; import { UserEncryption } from "@app/services/user/user-types";
export const registerPasswordRouter = async (server: FastifyZodProvider) => { 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({ server.route({
method: "POST", method: "POST",
url: "/email/password-reset", 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({ server.route({
method: "GET", method: "GET",
url: "/backup-private-key", url: "/backup-private-key",
@@ -257,14 +155,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ 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(), password: z.string().trim(),
token: z.string().trim() token: z.string().trim()
}), }),
@@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
publicKeys: z publicKeys: z
.object({ .object({
publicKey: z.string().optional(), publicKey: z.string().nullable().optional(),
userId: z.string() userId: z.string()
}) })
.array() .array()
+2 -38
View File
@@ -1,6 +1,6 @@
import { z } from "zod"; 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 { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -19,23 +19,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
user: UsersSchema.merge( user: UsersSchema
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
})
)
}) })
} }
}, },
@@ -94,26 +78,6 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/private-key",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
const privateKey = await server.services.user.getUserPrivateKey(req.permission.id);
return { privateKey };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:userId/unlock", url: "/:userId/unlock",
+7 -7
View File
@@ -97,13 +97,13 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
encryptionVersion: z.number().default(1).nullable().optional(), encryptionVersion: z.number().default(1).nullable().optional(),
protectedKey: z.string().nullable(), protectedKey: z.string().nullish(),
protectedKeyIV: z.string().nullable(), protectedKeyIV: z.string().nullish(),
protectedKeyTag: z.string().nullable(), protectedKeyTag: z.string().nullish(),
publicKey: z.string(), publicKey: z.string().nullish(),
encryptedPrivateKey: z.string(), encryptedPrivateKey: z.string().nullish(),
iv: z.string(), iv: z.string().nullish(),
tag: z.string(), tag: z.string().nullish(),
token: z.string() token: z.string()
}) })
} }
@@ -153,7 +153,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
firstName: true, firstName: true,
lastName: true, lastName: true,
id: true id: true
}).extend({ publicKey: z.string().nullable() }) }).extend({ publicKey: z.string().nullish() })
}).omit({ createdAt: true, updatedAt: true }) }).omit({ createdAt: true, updatedAt: true })
}) })
} }
@@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: ProjectKeysSchema.merge( 200: ProjectKeysSchema.merge(
z.object({ z.object({
sender: z.object({ sender: z.object({
publicKey: z.string() publicKey: z.string().optional()
}) })
}) })
) )
+65 -10
View File
@@ -20,8 +20,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
serverPublicKey: z.string(), serverPublicKey: z.string().nullish(),
salt: z.string() salt: z.string().nullish()
}) })
} }
}, },
@@ -124,14 +124,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
encryptionVersion: z.number().default(1).nullable().optional(), encryptionVersion: z.number().default(1).nullish(),
protectedKey: z.string().nullable(), protectedKey: z.string().nullish(),
protectedKeyIV: z.string().nullable(), protectedKeyIV: z.string().nullish(),
protectedKeyTag: z.string().nullable(), protectedKeyTag: z.string().nullish(),
publicKey: z.string(), publicKey: z.string().nullish(),
encryptedPrivateKey: z.string(), encryptedPrivateKey: z.string().nullish(),
iv: z.string(), iv: z.string().nullish(),
tag: z.string(), tag: z.string().nullish(),
token: z.string() token: z.string()
}) })
} }
@@ -181,4 +181,59 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
} as const; } as const;
} }
}); });
// New login route that doesn't use SRP
server.route({
method: "POST",
url: "/login",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
email: z.string().trim(),
password: z.string().trim(),
providerAuthToken: z.string().trim().optional(),
captchaToken: z.string().trim().optional()
}),
response: {
200: z.object({
accessToken: z.string()
})
}
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
if (!userAgent) throw new Error("user agent header is required");
const { tokens, mfaEnabled } = await server.services.login.login({
email: req.body.email,
password: req.body.password,
ip: req.realIp,
userAgent,
providerAuthToken: req.body.providerAuthToken,
captchaToken: req.body.captchaToken
});
const appCfg = getConfig();
void res.setCookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
return { accessToken: tokens.accessToken, mfaEnabled };
}
});
}; };
@@ -98,15 +98,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
email: z.string().trim(), email: z.string().trim(),
firstName: z.string().trim(), firstName: z.string().trim(),
lastName: z.string().trim().optional(), 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(), providerAuthToken: z.string().trim().optional().nullish(),
attributionSource: z.string().trim().optional(), attributionSource: z.string().trim().optional(),
password: z.string() password: z.string()
@@ -189,15 +180,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
password: z.string(), password: z.string(),
firstName: z.string().trim(), firstName: z.string().trim(),
lastName: z.string().trim().optional(), 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() tokenMetadata: z.string().optional()
}), }),
response: { response: {
+57 -2
View File
@@ -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 { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto } from "@app/lib/crypto"; 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) => { export const validateProviderAuthToken = (providerToken: string, username?: string) => {
if (!providerToken) throw new UnauthorizedError(); 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 };
};
+78 -42
View File
@@ -4,7 +4,6 @@ import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSc
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; 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 { LoginMethod } from "../super-admin/super-admin-types";
import { TTotpServiceFactory } from "../totp/totp-service"; import { TTotpServiceFactory } from "../totp/totp-service";
import { TUserDALFactory } from "../user/user-dal"; 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 { import {
TLoginClientProofDTO, TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO, TLoginGenServerPublicKeyDTO,
@@ -208,6 +208,10 @@ export const authLoginServiceFactory = ({
throw new Error("Failed to find user"); throw new Error("Failed to find user");
} }
if (!userEnc.salt || !userEnc.verifier) {
throw new BadRequestError({ message: "Salt or verifier not found" });
}
if ( if (
serverCfg.enabledLoginMethods && serverCfg.enabledLoginMethods &&
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
@@ -247,8 +251,6 @@ export const authLoginServiceFactory = ({
captchaToken, captchaToken,
password password
}: TLoginClientProofDTO) => { }: TLoginClientProofDTO) => {
const appCfg = getConfig();
// akhilmhdh: case sensitive email resolution // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({ const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email username: email
@@ -259,44 +261,11 @@ export const authLoginServiceFactory = ({
const user = await userDAL.findById(userEnc.userId); const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig(); const cfg = getConfig();
let authMethod = AuthMethod.EMAIL; const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken);
let organizationId: string | undefined; await verifyCaptcha(user, captchaToken);
if (providerAuthToken) { if (!userEnc.salt || !userEnc.verifier) {
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); throw new BadRequestError({ message: "Salt or verifier not found" });
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.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?"); if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
@@ -371,6 +340,72 @@ export const authLoginServiceFactory = ({
return { token, user: userEnc } as const; return { token, user: userEnc } as const;
}; };
const login = async ({
email,
password,
ip,
userAgent,
providerAuthToken,
captchaToken
}: {
email: string;
password: string;
ip: string;
userAgent: string;
providerAuthToken?: string;
captchaToken?: string;
}) => {
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
});
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc) throw new BadRequestError({ message: "User not found" });
if (userEnc.encryptionVersion !== UserEncryption.V2) {
throw new BadRequestError({ message: "Legacy encryption scheme not supported", name: "LegacyEncryptionScheme" });
}
if (!userEnc.hashedPassword) {
if (userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
throw new BadRequestError({
message: "Legacy encryption scheme not supported",
name: "LegacyEncryptionScheme"
});
}
throw new BadRequestError({ message: "No password found" });
}
const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken);
await verifyCaptcha(userEnc, captchaToken);
if (!(await crypto.hashing().compareHash(password, userEnc.hashedPassword))) {
throw new BadRequestError({ message: "Invalid username or email" });
}
const token = await generateUserTokens({
user: {
...userEnc,
id: userEnc.userId
},
ip,
userAgent,
authMethod,
organizationId
});
return {
mfaEnabled: userEnc.isMfaEnabled,
tokens: {
accessToken: token.access,
refreshToken: token.refresh
},
user: userEnc
} as const;
};
const selectOrganization = async ({ const selectOrganization = async ({
userAgent, userAgent,
authJwtToken, authJwtToken,
@@ -862,6 +897,7 @@ export const authLoginServiceFactory = ({
resendMfaToken, resendMfaToken,
verifyMfaToken, verifyMfaToken,
selectOrganization, selectOrganization,
generateUserTokens generateUserTokens,
login
}; };
}; };
@@ -1,8 +1,5 @@
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
@@ -16,8 +13,6 @@ import { UserEncryption } from "../user/user-types";
import { TAuthDALFactory } from "./auth-dal"; import { TAuthDALFactory } from "./auth-dal";
import { import {
ResetPasswordV2Type, ResetPasswordV2Type,
TChangePasswordDTO,
TCreateBackupPrivateKeyDTO,
TResetPasswordV2DTO, TResetPasswordV2DTO,
TResetPasswordViaBackupKeyDTO, TResetPasswordViaBackupKeyDTO,
TSetupPasswordViaBackupKeyDTO TSetupPasswordViaBackupKeyDTO
@@ -40,79 +35,6 @@ export const authPaswordServiceFactory = ({
smtpService, smtpService,
totpConfigDAL totpConfigDAL
}: TAuthPasswordServiceFactoryDep) => { }: 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 * Email password reset flow via email. Step 1 send email
*/ */
@@ -211,58 +133,17 @@ export const authPaswordServiceFactory = ({
} }
} }
const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS); if (user.encryptionVersion !== UserEncryption.V2) {
// 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 {
throw new BadRequestError({ throw new BadRequestError({
message: "Cannot reset password without current credentials or recovery method", message: "Cannot reset password without current credentials or recovery method",
name: "Reset password" name: "Reset password"
}); });
} }
const encKeys = await generateUserSrpKeys(user.username, newPassword, { const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS);
publicKey: user.publicKey,
privateKey
});
const { tag, iv, ciphertext, encoding } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
await userDAL.updateUserEncryptionByUserId(userId, { await userDAL.updateUserEncryptionByUserId(userId, {
hashedPassword: newHashedPassword, 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
}); });
await tokenService.revokeAllMySessions(userId); await tokenService.revokeAllMySessions(userId);
@@ -313,66 +194,6 @@ export const authPaswordServiceFactory = ({
}); });
}; };
/*
* backup key creation to give user's their access back when lost their password
* this also needs to do the generateServerPubKey function to be executed first
* then only client proof can be verified
* */
const createBackupPrivateKey = async ({
clientProof,
encryptedPrivateKey,
salt,
verifier,
iv,
tag,
userId
}: TCreateBackupPrivateKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key");
const isValidClientProff = await srpCheckClientProof(
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProff) throw new Error("failed to create backup key");
const backup = await authDAL.transaction(async (tx) => {
const backupKey = await authDAL.upsertBackupKey(
userEnc.userId,
{
encryptedPrivateKey,
iv,
tag,
salt,
verifier,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
},
tx
);
await userDAL.updateUserEncryptionByUserId(
userEnc.userId,
{
serverPrivateKey: null,
clientPublicKey: null
},
tx
);
return backupKey;
});
return backup;
};
/*
* Return user back up
* */
const getBackupPrivateKeyOfUser = async (userId: string) => { const getBackupPrivateKeyOfUser = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId); const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user || (user && !user.isAccepted)) { if (!user || (user && !user.isAccepted)) {
@@ -416,21 +237,7 @@ export const authPaswordServiceFactory = ({
}); });
}; };
const setupPassword = async ( const setupPassword = async ({ password, token }: TSetupPasswordViaBackupKeyDTO, actor: OrgServiceActor) => {
{
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
password,
token
}: TSetupPasswordViaBackupKeyDTO,
actor: OrgServiceActor
) => {
try { try {
await tokenService.validateTokenForUser({ await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP, type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
@@ -466,15 +273,7 @@ export const authPaswordServiceFactory = ({
await userDAL.updateUserEncryptionByUserId( await userDAL.updateUserEncryptionByUserId(
actor.id, actor.id,
{ {
encryptionVersion: 2, encryptionVersion: UserEncryption.V2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
hashedPassword, hashedPassword,
serverPrivateKey: null, serverPrivateKey: null,
clientPublicKey: null clientPublicKey: null
@@ -487,12 +286,9 @@ export const authPaswordServiceFactory = ({
}; };
return { return {
generateServerPubKey,
changePassword,
resetPasswordByBackupKey, resetPasswordByBackupKey,
sendPasswordResetEmail, sendPasswordResetEmail,
verifyPasswordResetEmail, verifyPasswordResetEmail,
createBackupPrivateKey,
getBackupPrivateKeyOfUser, getBackupPrivateKeyOfUser,
sendPasswordSetupEmail, sendPasswordSetupEmail,
setupPassword, setupPassword,
@@ -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 { export enum ResetPasswordV2Type {
Recovery = "recovery", Recovery = "recovery",
LoggedInReset = "logged-in-reset" LoggedInReset = "logged-in-reset"
@@ -39,14 +24,6 @@ export type TResetPasswordViaBackupKeyDTO = {
}; };
export type TSetupPasswordViaBackupKeyDTO = { export type TSetupPasswordViaBackupKeyDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string; password: string;
token: string; token: string;
}; };
+18 -194
View File
@@ -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 { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { getMinExpiresIn } from "@app/lib/fn"; import { getMinExpiresIn } from "@app/lib/fn";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
@@ -41,7 +40,7 @@ type TAuthSignupDep = {
| "findUserGroupMembershipsInProject" | "findUserGroupMembershipsInProject"
>; >;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">; orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
@@ -147,17 +146,8 @@ export const authSignupServiceFactory = ({
firstName, firstName,
lastName, lastName,
providerAuthToken, providerAuthToken,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
organizationName, organizationName,
// attributionSource, // attributionSource,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
ip, ip,
userAgent, userAgent,
authorization, authorization,
@@ -191,98 +181,18 @@ export const authSignupServiceFactory = ({
} }
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); 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 updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); 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 const userEncKey = await userDAL.upsertUserEncryptionKey(
// the private key is actually system generated password us.id,
// 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 encryptionVersion: UserEncryption.V2,
// without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things hashedPassword
if ( },
systemGeneratedUserEncryptionKey && tx
!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
);
}
// 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 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 ( if (
@@ -400,19 +310,10 @@ export const authSignupServiceFactory = ({
const completeAccountInvite = async ({ const completeAccountInvite = async ({
email, email,
ip, ip,
salt,
password, password,
verifier,
firstName, firstName,
publicKey,
userAgent, userAgent,
lastName, lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
authorization authorization
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
const sanitizedEmail = email.trim().toLowerCase(); const sanitizedEmail = email.trim().toLowerCase();
@@ -437,94 +338,17 @@ export const authSignupServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); 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 updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx); const userEncKey = await userDAL.upsertUserEncryptionKey(
let userEncKey; us.id,
// this means this is system generated credentials {
// now replace the private key encryptionVersion: 2,
if ( hashedPassword
systemGeneratedUserEncryptionKey && },
!systemGeneratedUserEncryptionKey.hashedPassword && tx
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 updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
@@ -3,15 +3,6 @@ export type TCompleteAccountSignupDTO = {
password: string; password: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName?: string; organizationName?: string;
providerAuthToken?: string | null; providerAuthToken?: string | null;
attributionSource?: string | undefined; attributionSource?: string | undefined;
@@ -26,15 +17,6 @@ export type TCompleteAccountInviteDTO = {
password: string; password: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
ip: string; ip: string;
userAgent: string; userAgent: string;
authorization: string; authorization: string;
@@ -1,6 +1,6 @@
import { ForbiddenError } from "@casl/ability"; 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 { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types";
import { import {
constructPermissionErrorMessage, constructPermissionErrorMessage,
@@ -188,7 +188,7 @@ export const groupProjectServiceFactory = ({
// other groups that are in the project // other groups that are in the project
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx); 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); const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx);
if (!ghostUser) { 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); const bot = await projectBotDAL.findOne({ projectId: project.id }, tx);
if (!bot) { if (!bot) {
@@ -231,6 +237,12 @@ export const groupProjectServiceFactory = ({
}); });
const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => { 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 const { ciphertext: encryptedKey, nonce } = crypto
.encryption() .encryption()
.asymmetric() .asymmetric()
@@ -1,13 +1,11 @@
import { ForbiddenError } from "@casl/ability"; 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 { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; 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 { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
@@ -83,7 +81,7 @@ export const orgAdminServiceFactory = ({
actorAuthMethod, actorAuthMethod,
projectId projectId
}: TAccessProjectDTO) => { }: TAccessProjectDTO) => {
const { permission, membership } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
@@ -144,29 +142,9 @@ export const orgAdminServiceFactory = ({
}); });
} }
const botPrivateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId); const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId);
if (!userEncryptionKey) if (!userEncryptionKey)
throw new NotFoundError({ message: `User encryption key for user with ID '${actorId}' not found` }); 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 updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
const newProjectMembership = await projectMembershipDAL.create( const newProjectMembership = await projectMembershipDAL.create(
@@ -181,16 +159,6 @@ export const orgAdminServiceFactory = ({
tx tx
); );
await projectKeyDAL.create(
{
encryptedKey: newWsMember.workspaceEncryptedKey,
nonce: newWsMember.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: actorId,
projectId
},
tx
);
return newProjectMembership; return newProjectMembership;
}); });
+2 -144
View File
@@ -8,7 +8,6 @@ import {
OrgMembershipStatus, OrgMembershipStatus,
ProjectMembershipRole, ProjectMembershipRole,
ProjectVersion, ProjectVersion,
SecretKeyEncoding,
TableName, TableName,
TProjectMemberships, TProjectMemberships,
TProjectUserMembershipRolesInsert, TProjectUserMembershipRolesInsert,
@@ -58,8 +57,6 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
import { TProjectDALFactory } from "../project/project-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 { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
@@ -130,7 +127,6 @@ type TOrgServiceFactoryDep = {
>; >;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
loginService: Pick<TAuthLoginFactory, "generateUserTokens">; loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
@@ -162,7 +158,6 @@ export const orgServiceFactory = ({
projectRoleDAL, projectRoleDAL,
samlConfigDAL, samlConfigDAL,
oidcConfigDAL, oidcConfigDAL,
projectBotDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityMetadataDAL, identityMetadataDAL,
projectBotService, projectBotService,
@@ -280,15 +275,7 @@ export const orgServiceFactory = ({
user.id, user.id,
{ {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey: encKeys.protectedKey, publicKey: encKeys.publicKey
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier
}, },
tx tx
); );
@@ -878,29 +865,10 @@ export const orgServiceFactory = ({
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key // 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 // 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)) { 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( await userDAL.createUserEncryption(
{ {
userId: inviteeUserId, userId: inviteeUserId,
encryptionVersion: 2, 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
}, },
tx tx
); );
@@ -1062,106 +1030,6 @@ export const orgServiceFactory = ({
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); 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( const projectMemberships = await projectMembershipDAL.insertMany(
userWithEncryptionKeyInvitedToProject.map((userEnc) => ({ userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
projectId, projectId,
@@ -1184,16 +1052,6 @@ export const orgServiceFactory = ({
}); });
await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); 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({ mailsForProjectInvitation.push({
email: userWithEncryptionKeyInvitedToProject email: userWithEncryptionKeyInvitedToProject
.filter((el) => !userIdsWithOrgInvitation.has(el.userId)) .filter((el) => !userIdsWithOrgInvitation.has(el.userId))
@@ -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.` 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 = ""; let userPrivateKey = "";
if ( if (
projectV1Keys?.serverEncryptedPrivateKey && projectV1Keys?.serverEncryptedPrivateKey &&
@@ -14,7 +14,7 @@ export const projectKeyDALFactory = (db: TDbClient) => {
userId: string, userId: string,
projectId: string, projectId: string,
tx?: Knex tx?: Knex
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => { ): Promise<(TProjectKeys & { sender: { publicKey?: string } }) | undefined> => {
try { try {
const projectKey = await (tx || db.replicaNode())(TableName.ProjectKeys) const projectKey = await (tx || db.replicaNode())(TableName.ProjectKeys)
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`) .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)) .select(db.ref("publicKey").withSchema(TableName.UserEncryptionKey))
.first(); .first();
if (projectKey) { if (projectKey) {
return { ...projectKey, sender: { publicKey: projectKey.publicKey } }; return { ...projectKey, sender: { publicKey: projectKey.publicKey || undefined } };
} }
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find latest project key" }); throw new DatabaseError({ error, name: "Find latest project key" });
@@ -10,6 +10,10 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types"; import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types";
export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => { 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({ const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({
ciphertext: decryptKey.encryptedKey, ciphertext: decryptKey.encryptedKey,
nonce: decryptKey.nonce, nonce: decryptKey.nonce,
@@ -121,6 +121,10 @@ export const projectQueueFactory = ({
tag: data.encryptedPrivateKey.encryptedKeyTag 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({ const decryptedPlainProjectKey = crypto.encryption().asymmetric().decrypt({
ciphertext: oldProjectKey.encryptedKey, ciphertext: oldProjectKey.encryptedKey,
nonce: oldProjectKey.nonce, nonce: oldProjectKey.nonce,
@@ -187,6 +191,10 @@ export const projectQueueFactory = ({
approvalSecrets.push(...secretApprovals); approvalSecrets.push(...secretApprovals);
} }
if (!oldProjectKey.sender.publicKey) {
throw new Error("Old project key is not valid");
}
const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey); const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey);
const decryptedSecretVersions = decryptSecretVersions(secretVersions, userPrivateKey, oldProjectKey); const decryptedSecretVersions = decryptSecretVersions(secretVersions, userPrivateKey, oldProjectKey);
const decryptedApprovalSecrets = decryptSecretApprovals(approvalSecrets, userPrivateKey, oldProjectKey); const decryptedApprovalSecrets = decryptSecretApprovals(approvalSecrets, userPrivateKey, oldProjectKey);
@@ -290,6 +298,10 @@ export const projectQueueFactory = ({
continue; continue;
} }
if (!user.publicKey) {
throw new Error(`User with ID ${key.receiverId} has no public key during upgrade.`);
}
const [newMember] = assignWorkspaceKeysToMembers({ const [newMember] = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey, decryptKey: ghostUserLatestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey, userPrivateKey: ghostUser.keys.plainPrivateKey,
+5 -100
View File
@@ -55,13 +55,10 @@ import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microso
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal"; import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal"; import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-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 { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; 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 { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "../project-role/project-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 { TUserDALFactory } from "../user/user-dal";
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types"; import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
import { TProjectDALFactory } from "./project-dal"; import { TProjectDALFactory } from "./project-dal";
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns"; import { bootstrapSshProject } from "./project-fns";
import { TProjectQueueFactory } from "./project-queue"; import { TProjectQueueFactory } from "./project-queue";
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal"; import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
import { import {
@@ -123,6 +120,7 @@ export const DEFAULT_PROJECT_ENVS = [
type TProjectServiceFactoryDep = { type TProjectServiceFactoryDep = {
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "create">;
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">; projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">;
projectQueue: TProjectQueueFactory; projectQueue: TProjectQueueFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
@@ -132,9 +130,7 @@ type TProjectServiceFactoryDep = {
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">; secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">; projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
identityOrgMembershipDAL: TIdentityOrgDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: TIdentityProjectDALFactory;
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">; identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick< projectMembershipDAL: Pick<
TProjectMembershipDALFactory, TProjectMembershipDALFactory,
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
@@ -167,12 +163,10 @@ type TProjectServiceFactoryDep = {
sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">; sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">;
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">; sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">; keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">;
kmsService: Pick< kmsService: Pick<
TKmsServiceFactory, TKmsServiceFactory,
@@ -196,27 +190,25 @@ export const projectServiceFactory = ({
secretDAL, secretDAL,
secretV2BridgeDAL, secretV2BridgeDAL,
projectQueue, projectQueue,
projectKeyDAL,
permissionService, permissionService,
projectBotService, projectBotService,
orgDAL, orgDAL,
userDAL, userDAL,
folderDAL, folderDAL,
orgService,
identityProjectDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
projectMembershipDAL, projectMembershipDAL,
projectEnvDAL, projectEnvDAL,
licenseService, licenseService,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
projectRoleDAL, projectRoleDAL,
identityProjectMembershipRoleDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
certificateTemplateDAL, certificateTemplateDAL,
pkiCollectionDAL, pkiCollectionDAL,
pkiAlertDAL, pkiAlertDAL,
pkiSubscriberDAL, pkiSubscriberDAL,
identityProjectDAL,
identityProjectMembershipRoleDAL,
sshCertificateAuthorityDAL, sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL, sshCertificateAuthoritySecretDAL,
sshCertificateDAL, sshCertificateDAL,
@@ -225,7 +217,6 @@ export const projectServiceFactory = ({
sshHostGroupDAL, sshHostGroupDAL,
keyStore, keyStore,
kmsService, kmsService,
projectBotDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
projectMicrosoftTeamsConfigDAL, projectMicrosoftTeamsConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
@@ -253,7 +244,7 @@ export const projectServiceFactory = ({
type = ProjectType.SecretManager type = ProjectType.SecretManager
}: TCreateProjectDTO) => { }: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId }); const organization = await orgDAL.findOne({ id: actorOrgId });
const { permission, membership: orgMembership } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
organization.id, 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." 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) { if (kmsKeyId) {
const kms = await kmsService.getKmsById(kmsKeyId, tx); 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 // set default environments and root folder for provided environments
let envs: TProjectEnvironments[] = []; let envs: TProjectEnvironments[] = [];
if (projectTemplate) { 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 the project is being created by a user, add the user to the project as an admin
if (actor === ActorType.USER) { if (actor === ActorType.USER) {
// Find public key of user // Find public key of user
@@ -432,17 +360,6 @@ export const projectServiceFactory = ({
throw new Error("User not found"); 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 // Create a membership for the user
const userProjectMembership = await projectMembershipDAL.create( const userProjectMembership = await projectMembershipDAL.create(
{ {
@@ -455,18 +372,6 @@ export const projectServiceFactory = ({
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
tx 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 // If the project is being created by an identity, add the identity to the project as an admin
@@ -116,7 +116,7 @@ export type TUpgradeProjectDTO = {
} & TProjectPermission; } & TProjectPermission;
export type AddUserToWsDTO = { export type AddUserToWsDTO = {
decryptKey: TProjectKeys & { sender: { publicKey: string } }; decryptKey: TProjectKeys & { sender: { publicKey?: string } };
userPrivateKey: string; userPrivateKey: string;
members: { members: {
orgMembershipId: string; orgMembershipId: string;
-22
View File
@@ -1,9 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; 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 { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
@@ -212,25 +210,6 @@ export const userServiceFactory = ({
); );
}; };
const getUserPrivateKey = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user?.serverEncryptedPrivateKey || !user.serverEncryptedPrivateKeyIV || !user.serverEncryptedPrivateKeyTag) {
throw new NotFoundError({ message: `Private key for user with ID '${userId}' not found` });
}
const privateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
ciphertext: user.serverEncryptedPrivateKey,
tag: user.serverEncryptedPrivateKeyTag,
iv: user.serverEncryptedPrivateKeyIV,
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
return privateKey;
};
const getUserProjectFavorites = async (userId: string, orgId: string) => { const getUserProjectFavorites = async (userId: string, orgId: string) => {
const orgMembership = await orgMembershipDAL.findOne({ const orgMembership = await orgMembershipDAL.findOne({
userId, userId,
@@ -311,7 +290,6 @@ export const userServiceFactory = ({
listUserGroups, listUserGroups,
getUserAction, getUserAction,
unlockUser, unlockUser,
getUserPrivateKey,
getAllMyAccounts, getAllMyAccounts,
getUserProjectFavorites, getUserProjectFavorites,
removeMyDuplicateAccounts, removeMyDuplicateAccounts,
+21
View File
@@ -17,6 +17,7 @@ const (
operationCallGetEncryptedWorkspaceKey = "CallGetEncryptedWorkspaceKey" operationCallGetEncryptedWorkspaceKey = "CallGetEncryptedWorkspaceKey"
operationCallGetServiceTokenDetails = "CallGetServiceTokenDetails" operationCallGetServiceTokenDetails = "CallGetServiceTokenDetails"
operationCallLogin1V3 = "CallLogin1V3" operationCallLogin1V3 = "CallLogin1V3"
operationCallLoginV3 = "CallLoginV3"
operationCallVerifyMfaToken = "CallVerifyMfaToken" operationCallVerifyMfaToken = "CallVerifyMfaToken"
operationCallLogin2V3 = "CallLogin2V3" operationCallLogin2V3 = "CallLogin2V3"
operationCallGetAllOrganizations = "CallGetAllOrganizations" operationCallGetAllOrganizations = "CallGetAllOrganizations"
@@ -100,6 +101,26 @@ func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLo
return loginOneV2Response, nil return loginOneV2Response, nil
} }
func CallLoginV3(httpClient *resty.Client, request GetLoginV3Request) (GetLoginV3Response, error) {
var loginV3Response GetLoginV3Response
response, err := httpClient.
R().
SetResult(&loginV3Response).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v3/auth/login", config.INFISICAL_URL))
if err != nil {
return GetLoginV3Response{}, NewGenericRequestError(operationCallLoginV3, err)
}
if response.IsError() {
return GetLoginV3Response{}, NewAPIErrorWithResponse(operationCallLoginV3, response, nil)
}
return loginV3Response, nil
}
func CallVerifyMfaToken(httpClient *resty.Client, request VerifyMfaTokenRequest) (*VerifyMfaTokenResponse, *VerifyMfaTokenErrorResponse, error) { func CallVerifyMfaToken(httpClient *resty.Client, request VerifyMfaTokenRequest) (*VerifyMfaTokenResponse, *VerifyMfaTokenErrorResponse, error) {
var verifyMfaTokenResponse VerifyMfaTokenResponse var verifyMfaTokenResponse VerifyMfaTokenResponse
var responseError VerifyMfaTokenErrorResponse var responseError VerifyMfaTokenErrorResponse
+9
View File
@@ -245,6 +245,15 @@ type GetLoginOneV2Request struct {
ClientPublicKey string `json:"clientPublicKey"` ClientPublicKey string `json:"clientPublicKey"`
} }
type GetLoginV3Request struct {
Email string `json:"email"`
Password string `json:"password"`
}
type GetLoginV3Response struct {
AccessToken string `json:"accessToken"`
}
type GetLoginOneV2Response struct { type GetLoginOneV2Response struct {
ServerPublicKey string `json:"serverPublicKey"` ServerPublicKey string `json:"serverPublicKey"`
Salt string `json:"salt"` Salt string `json:"salt"`
+37 -97
View File
@@ -25,7 +25,6 @@ import (
"github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/crypto"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/srp" "github.com/Infisical/infisical-merge/packages/srp"
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
@@ -280,7 +279,21 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
util.HandleError(err, "Unable to parse email and password for authentication") util.HandleError(err, "Unable to parse email and password for authentication")
} }
loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password) loginV3Response, err := getFreshUserCredentials(email, password)
if err == nil {
userCredentialsToBeStored.Email = email
userCredentialsToBeStored.PrivateKey = ""
userCredentialsToBeStored.JTWToken = loginV3Response.AccessToken
return
}
if !strings.Contains(err.Error(), "LegacyEncryptionScheme") {
util.HandleError(err)
}
log.Info().Msg("Unable to authenticate with the provided credentials, falling back to SRP authentication")
_, loginTwoResponse, err := getFreshUserCredentialsWithSrp(email, password)
if err != nil { if err != nil {
fmt.Println("Unable to authenticate with the provided credentials, please try again") fmt.Println("Unable to authenticate with the provided credentials, please try again")
log.Debug().Err(err) log.Debug().Err(err)
@@ -338,105 +351,12 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
} }
} }
var decryptedPrivateKey []byte
if loginTwoResponse.EncryptionVersion == 1 {
log.Debug().Msg("Login version 1")
encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey)
tag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag)
if err != nil {
util.HandleError(err)
}
IV, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv)
if err != nil {
util.HandleError(err)
}
paddedPassword := fmt.Sprintf("%032s", password)
key := []byte(paddedPassword)
computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV)
if err != nil || len(computedDecryptedPrivateKey) == 0 {
util.HandleError(err)
}
decryptedPrivateKey = computedDecryptedPrivateKey
} else if loginTwoResponse.EncryptionVersion == 2 {
log.Debug().Msg("Login version 2")
protectedKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKey)
if err != nil {
util.HandleError(err)
}
protectedKeyTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyTag)
if err != nil {
util.HandleError(err)
}
protectedKeyIV, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyIV)
if err != nil {
util.HandleError(err)
}
nonProtectedTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag)
if err != nil {
util.HandleError(err)
}
nonProtectedIv, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv)
if err != nil {
util.HandleError(err)
}
parameters := &params{
memory: 64 * 1024,
iterations: 3,
parallelism: 1,
keyLength: 32,
}
derivedKey, err := generateFromPassword(password, []byte(loginOneResponse.Salt), parameters)
if err != nil {
util.HandleError(fmt.Errorf("unable to generate argon hash from password [err=%s]", err))
}
decryptedProtectedKey, err := crypto.DecryptSymmetric(derivedKey, protectedKey, protectedKeyTag, protectedKeyIV)
if err != nil {
util.HandleError(fmt.Errorf("unable to get decrypted protected key [err=%s]", err))
}
encryptedPrivateKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey)
if err != nil {
util.HandleError(err)
}
decryptedProtectedKeyInHex, err := hex.DecodeString(string(decryptedProtectedKey))
if err != nil {
util.HandleError(err)
}
computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv)
if err != nil {
util.HandleError(err)
}
decryptedPrivateKey = computedDecryptedPrivateKey
} else {
util.PrintErrorMessageAndExit("Insufficient details to decrypt private key")
}
if string(decryptedPrivateKey) == "" || email == "" || loginTwoResponse.Token == "" {
log.Debug().Msgf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token)
util.PrintErrorMessageAndExit("We were unable to fetch required details to complete your login. Run with -d to see more info")
}
// Login is successful so ask user to choose organization // Login is successful so ask user to choose organization
newJwtToken := GetJwtTokenWithOrganizationId(loginTwoResponse.Token, email) newJwtToken := GetJwtTokenWithOrganizationId(loginTwoResponse.Token, email)
//updating usercredentials //updating usercredentials
userCredentialsToBeStored.Email = email userCredentialsToBeStored.Email = email
userCredentialsToBeStored.PrivateKey = string(decryptedPrivateKey) userCredentialsToBeStored.PrivateKey = ""
userCredentialsToBeStored.JTWToken = newJwtToken userCredentialsToBeStored.JTWToken = newJwtToken
} }
@@ -665,7 +585,27 @@ func askForLoginCredentials() (email string, password string, err error) {
return userEmail, userPassword, nil return userEmail, userPassword, nil
} }
func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2Response, *api.GetLoginTwoV2Response, error) { func getFreshUserCredentials(email string, password string) (*api.GetLoginV3Response, error) {
log.Debug().Msg(fmt.Sprint("getFreshUserCredentials: ", "email", email, "password: ", password))
httpClient, err := util.GetRestyClientWithCustomHeaders()
if err != nil {
return nil, err
}
httpClient.SetRetryCount(5)
loginV3Response, err := api.CallLoginV3(httpClient, api.GetLoginV3Request{
Email: email,
Password: password,
})
if err != nil {
return nil, err
}
return &loginV3Response, nil
}
func getFreshUserCredentialsWithSrp(email string, password string) (*api.GetLoginOneV2Response, *api.GetLoginTwoV2Response, error) {
log.Debug().Msg(fmt.Sprint("getFreshUserCredentials: ", "email", email, "password: ", password)) log.Debug().Msg(fmt.Sprint("getFreshUserCredentials: ", "email", email, "password: ", password))
httpClient, err := util.GetRestyClientWithCustomHeaders() httpClient, err := util.GetRestyClientWithCustomHeaders()
if err != nil { if err != nil {
+34 -109
View File
@@ -1,12 +1,8 @@
import crypto from "crypto";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import jsrp from "jsrp";
import { useServerConfig } from "@app/context";
import { initProjectHelper } from "@app/helpers/project"; import { initProjectHelper } from "@app/helpers/project";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/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 InputField from "../basic/InputField";
import checkPassword from "../utilities/checks/password/checkPassword"; 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 SecurityClient from "../utilities/SecurityClient";
import { Button, Input } from "../v2"; import { Button, Input } from "../v2";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface UserInfoStepProps { interface UserInfoStepProps {
incrementStep: () => void; incrementStep: () => void;
email: string; email: string;
@@ -76,7 +66,6 @@ export default function UserInfoStep({
}: UserInfoStepProps): JSX.Element { }: UserInfoStepProps): JSX.Element {
const [nameError, setNameError] = useState(false); const [nameError, setNameError] = useState(false);
const [organizationNameError, setOrganizationNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false);
const { config } = useServerConfig();
const [errors, setErrors] = useState<Errors>({}); const [errors, setErrors] = useState<Errors>({});
@@ -108,109 +97,45 @@ export default function UserInfoStep({
}); });
if (!errorCheck) { if (!errorCheck) {
// Generate a random pair of a public and a private key console.log("signupErrorCheck passed");
const pair = await generateKeyPair(config.fipsEnabled);
localStorage.setItem("PRIVATE_KEY", pair.privateKey); try {
const response = await completeAccountSignup({
email,
password,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
providerAuthToken,
organizationName,
attributionSource
});
client.init( console.log("Signed up", JSON.stringify(response, null, 2));
{
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"); // unset signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken("");
const key = crypto.randomBytes(32); if (response.organizationId) {
await selectOrganization({ organizationId: response.organizationId });
// 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);
}
});
} }
);
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 { } else {
setIsLoading(false); setIsLoading(false);
} }
@@ -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<void> => {
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;
@@ -1,18 +1,18 @@
/* eslint-disable prefer-destructuring */ /* eslint-disable prefer-destructuring */
import axios from "axios";
import jsrp from "jsrp"; import jsrp from "jsrp";
import { decryptPrivateKeyHelper } from "@app/helpers/key"; import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries";
import { login1, login2 } from "@app/hooks/api/auth/queries";
import { createNotification } from "../notifications";
import Telemetry from "./telemetry/Telemetry"; import Telemetry from "./telemetry/Telemetry";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; import { LoginMode } from "./attemptLogin";
import SecurityClient from "./SecurityClient"; import SecurityClient from "./SecurityClient";
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
export interface IsCliLoginSuccessful { export interface IsCliLoginSuccessful {
mfaEnabled: boolean;
loginResponse?: { loginResponse?: {
email: string; email: string;
privateKey: string; privateKey: string;
@@ -31,14 +31,62 @@ const attemptLogin = async ({
email, email,
password, password,
providerAuthToken, providerAuthToken,
captchaToken captchaToken,
loginMode = LoginMode.ServerSide
}: { }: {
email: string; email: string;
password: string; password: string;
providerAuthToken?: string; providerAuthToken?: string;
captchaToken?: string; captchaToken?: string;
loginMode?: LoginMode;
}): Promise<IsCliLoginSuccessful> => { }): Promise<IsCliLoginSuccessful> => {
const telemetry = new Telemetry().getInstance(); const telemetry = new Telemetry().getInstance();
if (loginMode === LoginMode.ServerSide) {
console.log("attempting login with server side");
const data = await loginV3({
email,
password,
providerAuthToken,
captchaToken
}).catch((err) => {
if (axios.isAxiosError(err) && err.response?.status === 400) {
if (err.response.data.error === "LegacyEncryptionScheme") {
createNotification({
text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.",
type: "error"
});
return null;
}
}
throw err;
});
if (data === null) {
return attemptLogin({
email,
password,
providerAuthToken,
captchaToken,
loginMode: LoginMode.LegacySrp
});
}
SecurityClient.setProviderAuthToken("");
SecurityClient.setToken(data.accessToken);
return {
success: true,
loginResponse: {
email,
privateKey: "",
JTWToken: data.accessToken
}
};
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
client.init( client.init(
{ {
@@ -58,79 +106,26 @@ const attemptLogin = async ({
client.setServerPublicKey(serverPublicKey); client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1 const clientProof = client.getProof(); // called M1
const { const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await login2({
email, email,
password, password,
clientProof, clientProof,
providerAuthToken, providerAuthToken,
captchaToken captchaToken
}); });
if (mfaEnabled) { if (encryptionVersion && encryptedPrivateKey && iv && tag && token) {
// 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
SecurityClient.setProviderAuthToken(""); SecurityClient.setProviderAuthToken("");
// set JWT token
SecurityClient.setToken(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) { if (email) {
telemetry.identify(email, email); telemetry.identify(email, email);
telemetry.capture("User Logged In"); telemetry.capture("User Logged In");
} }
resolve({ resolve({
mfaEnabled: false,
loginResponse: { loginResponse: {
email, email,
privateKey, privateKey: "",
JTWToken: token JTWToken: token
}, },
success: true success: true
@@ -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<IsMfaLoginSuccessful> => {
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;
@@ -1,15 +1,19 @@
/* eslint-disable prefer-destructuring */ /* eslint-disable prefer-destructuring */
import axios from "axios";
import jsrp from "jsrp"; import jsrp from "jsrp";
import { decryptPrivateKeyHelper } from "@app/helpers/key"; import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries";
import { login1, login2 } from "@app/hooks/api/auth/queries";
import { createNotification } from "../notifications";
import Telemetry from "./telemetry/Telemetry"; import Telemetry from "./telemetry/Telemetry";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
import SecurityClient from "./SecurityClient"; import SecurityClient from "./SecurityClient";
export enum LoginMode {
LegacySrp = "legacy-srp",
ServerSide = "server-side"
}
interface IsLoginSuccessful { interface IsLoginSuccessful {
mfaEnabled: boolean;
success: boolean; success: boolean;
} }
@@ -23,14 +27,62 @@ const attemptLogin = async ({
email, email,
password, password,
providerAuthToken, providerAuthToken,
captchaToken captchaToken,
loginMode = LoginMode.ServerSide
}: { }: {
email: string; email: string;
password: string; password: string;
providerAuthToken?: string; providerAuthToken?: string;
captchaToken?: string; captchaToken?: string;
loginMode?: LoginMode;
}): Promise<IsLoginSuccessful> => { }): Promise<IsLoginSuccessful> => {
const telemetry = new Telemetry().getInstance(); const telemetry = new Telemetry().getInstance();
if (loginMode === LoginMode.ServerSide) {
console.log("attempting login with server side");
const data = await loginV3({
email,
password,
providerAuthToken,
captchaToken
}).catch((err) => {
if (axios.isAxiosError(err) && err.response?.status === 400) {
if (err.response.data.error === "LegacyEncryptionScheme") {
createNotification({
text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.",
type: "error"
});
return null;
}
}
throw err;
});
if (data === null) {
return attemptLogin({
email,
password,
providerAuthToken,
captchaToken,
loginMode: LoginMode.LegacySrp
});
}
SecurityClient.setProviderAuthToken("");
SecurityClient.setToken(data.accessToken);
if (email) {
telemetry.identify(email, email);
telemetry.capture("User Logged In");
}
return {
success: true
};
}
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
await new Promise((resolve) => { await new Promise((resolve) => {
@@ -48,18 +100,7 @@ const attemptLogin = async ({
client.setServerPublicKey(serverPublicKey); client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1 const clientProof = client.getProof(); // called M1
const { const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await login2({
captchaToken, captchaToken,
email, email,
password, password,
@@ -67,56 +108,22 @@ const attemptLogin = async ({
providerAuthToken providerAuthToken
}); });
if (mfaEnabled) { if (encryptionVersion && encryptedPrivateKey && iv && tag && token) {
// 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
// unset provider auth token in case it was used // unset provider auth token in case it was used
SecurityClient.setProviderAuthToken(""); SecurityClient.setProviderAuthToken("");
// set JWT token // set JWT token
SecurityClient.setToken(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) { if (email) {
telemetry.identify(email, email); telemetry.identify(email, email);
telemetry.capture("User Logged In"); telemetry.capture("User Logged In");
} }
return { return {
mfaEnabled: false,
success: true success: true
}; };
} }
return { success: false, mfaEnabled: false }; return { success: false };
}; };
export default attemptLogin; export default attemptLogin;
@@ -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<boolean> => {
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;
@@ -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;
@@ -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}`);
}
}
};
+7 -48
View File
@@ -8,26 +8,24 @@ import { organizationKeys } from "../organization/queries";
import { setAuthToken } from "../reactQuery"; import { setAuthToken } from "../reactQuery";
import { workspaceKeys } from "../workspace"; import { workspaceKeys } from "../workspace";
import { import {
ChangePasswordDTO,
CompleteAccountDTO, CompleteAccountDTO,
CompleteAccountSignupDTO, CompleteAccountSignupDTO,
GetAuthTokenAPI, GetAuthTokenAPI,
GetBackupEncryptedPrivateKeyDTO, GetBackupEncryptedPrivateKeyDTO,
IssueBackupPrivateKeyDTO,
Login1DTO, Login1DTO,
Login1Res, Login1Res,
Login2DTO, Login2DTO,
Login2Res, Login2Res,
LoginLDAPDTO, LoginLDAPDTO,
LoginLDAPRes, LoginLDAPRes,
LoginV3DTO,
LoginV3Res,
MfaMethod, MfaMethod,
ResetPasswordDTO, ResetPasswordDTO,
ResetPasswordV2DTO, ResetPasswordV2DTO,
ResetUserPasswordV2DTO, ResetUserPasswordV2DTO,
SendMfaTokenDTO, SendMfaTokenDTO,
SetupPasswordDTO, SetupPasswordDTO,
SRP1DTO,
SRPR1Res,
TOauthTokenExchangeDTO, TOauthTokenExchangeDTO,
UserAgentType, UserAgentType,
UserEncryptionVersion, UserEncryptionVersion,
@@ -50,21 +48,14 @@ export const login2 = async (loginDetails: Login2DTO) => {
return data; return data;
}; };
export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => { export const loginV3 = async (loginDetails: LoginV3DTO) => {
const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token const { data } = await apiRequest.post<LoginV3Res>("/api/v3/auth/login", loginDetails);
return data; return data;
}; };
export const useLogin1 = () => { export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
return useMutation({ const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token
mutationFn: async (details: { return data;
email: string;
clientPublicKey: string;
providerAuthToken?: string;
}) => {
return login1(details);
}
});
}; };
export const selectOrganization = async (data: { export const selectOrganization = async (data: {
@@ -143,11 +134,6 @@ export const useOauthTokenExchange = () => {
}); });
}; };
export const srp1 = async (details: SRP1DTO) => {
const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details);
return data;
};
export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => { export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => {
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details); const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details);
return data; return data;
@@ -158,14 +144,6 @@ export const completeAccountSignupInvite = async (details: CompleteAccountDTO) =
return data; return data;
}; };
export const useCompleteAccountSignup = () => {
return useMutation({
mutationFn: async (details: CompleteAccountSignupDTO) => {
return completeAccountSignup(details);
}
});
};
export const useSendMfaToken = () => { export const useSendMfaToken = () => {
return useMutation<object, object, SendMfaTokenDTO>({ return useMutation<object, object, SendMfaTokenDTO>({
mutationFn: async ({ email }) => { 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 ({ export const getBackupEncryptedPrivateKey = async ({
verificationToken verificationToken
}: GetBackupEncryptedPrivateKeyDTO) => { }: 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 // Refresh token is set as cookie when logged in
// Using that we fetch the auth bearer token needed for auth calls // Using that we fetch the auth bearer token needed for auth calls
export const fetchAuthToken = async () => { export const fetchAuthToken = async () => {
+13 -49
View File
@@ -49,13 +49,19 @@ export type Login2DTO = {
password: string; password: string;
}; };
export type LoginV3DTO = {
email: string;
password: string;
providerAuthToken?: string;
captchaToken?: string;
};
export type Login1Res = { export type Login1Res = {
serverPublicKey: string; serverPublicKey: string;
salt: string; salt: string;
}; };
export type Login2Res = { export type Login2Res = {
mfaEnabled: boolean;
token: string; token: string;
encryptionVersion?: number; encryptionVersion?: number;
protectedKey?: string; protectedKey?: string;
@@ -67,6 +73,11 @@ export type Login2Res = {
tag?: string; tag?: string;
}; };
export type LoginV3Res = {
accessToken: string;
mfaEnabled: boolean;
};
export type LoginLDAPDTO = { export type LoginLDAPDTO = {
organizationSlug: string; organizationSlug: string;
username: string; username: string;
@@ -77,28 +88,10 @@ export type LoginLDAPRes = {
nextUrl: string; nextUrl: string;
}; };
export type SRP1DTO = {
clientPublicKey: string;
};
export type SRPR1Res = {
serverPublicKey: string;
salt: string;
};
export type CompleteAccountDTO = { export type CompleteAccountDTO = {
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string; password: string;
tokenMetadata?: string; tokenMetadata?: string;
}; };
@@ -116,19 +109,6 @@ export type VerifySignupInviteDTO = {
organizationId: string; 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 = { export type ResetPasswordDTO = {
protectedKey: string; protectedKey: string;
protectedKeyIV: string; protectedKeyIV: string;
@@ -153,27 +133,11 @@ export type ResetUserPasswordV2DTO = {
}; };
export type SetupPasswordDTO = { export type SetupPasswordDTO = {
protectedKey: string; email: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
token: string; token: string;
password: string; password: string;
}; };
export type IssueBackupPrivateKeyDTO = {
encryptedPrivateKey: string;
iv: string;
tag: string;
salt: string;
verifier: string;
clientProof: string;
};
export type GetBackupEncryptedPrivateKeyDTO = { export type GetBackupEncryptedPrivateKeyDTO = {
verificationToken: string; verificationToken: string;
}; };
-8
View File
@@ -477,14 +477,6 @@ export const useGetMyOrganizationProjects = (orgId: string) => {
}); });
}; };
export const fetchMyPrivateKey = async () => {
const {
data: { privateKey }
} = await apiRequest.get<{ privateKey: string }>("/api/v1/user/private-key");
return privateKey;
};
export const useListUserGroupMemberships = (username: string) => { export const useListUserGroupMemberships = (username: string) => {
return useQuery({ return useQuery({
queryKey: userKeys.listUserGroupMemberships(username), queryKey: userKeys.listUserGroupMemberships(username),
-47
View File
@@ -4,53 +4,6 @@ import jsrp from "jsrp";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto"; 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 ( export const generateUserPassKey = async (
email: string, email: string,
@@ -8,7 +8,6 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
// TODO(akhilmhdh): rewrite this into module functions in lib // TODO(akhilmhdh): rewrite this into module functions in lib
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, ContentLoader, FormControl, Input } from "@app/components/v2"; import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context"; import { useServerConfig } from "@app/context";
@@ -63,13 +62,6 @@ export const SignUpPage = () => {
}); });
SecurityClient.setToken(res.token); SecurityClient.setToken(res.token);
saveTokenToLocalStorage({
publicKey: userPass.publicKey,
encryptedPrivateKey: userPass.encryptedPrivateKey,
iv: userPass.encryptedPrivateKeyIV,
tag: userPass.encryptedPrivateKeyTag,
privateKey
});
await selectOrganization({ organizationId: res.organization.id }); await selectOrganization({ organizationId: res.organization.id });
// TODO(akhilmhdh): This is such a confusing pattern and too unreliable // TODO(akhilmhdh): This is such a confusing pattern and too unreliable
@@ -18,7 +18,7 @@ import { useToggle } from "@app/hooks";
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
import { MfaMethod } from "@app/hooks/api/auth/types"; import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; 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 { EmailDuplicationConfirmation } from "@app/pages/auth/SelectOrgPage/EmailDuplicationConfirmation";
import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils"; import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils";
@@ -70,9 +70,6 @@ export const PasswordStep = ({
// set JWT token // set JWT token
SecurityClient.setToken(oauthLogin.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 // 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) { if (organizationId) {
const finishWithOrgWorkflow = async () => { 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); console.log("organization id was present. new JWT token to be used in CLI:", token);
const instance = axios.create(); const instance = axios.create();
const payload = { 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, email,
JTWToken: token JTWToken: token
}; };
@@ -1,22 +1,14 @@
import crypto from "crypto";
import { FormEvent, useState } from "react"; import { FormEvent, useState } from "react";
import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck"; 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 { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { useSetupPassword } from "@app/hooks/api/auth/queries"; import { useSetupPassword } from "@app/hooks/api/auth/queries";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
export const PasswordSetupPage = () => { export const PasswordSetupPage = () => {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
@@ -65,83 +57,31 @@ export const PasswordSetupPage = () => {
setPasswordsMatch(true); setPasswordsMatch(true);
if (!errorCheck) { if (!errorCheck) {
client.init( try {
{ await setupPassword.mutateAsync({
username: email, email,
token,
password 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 setTimeout(() => {
// key with the symmetric key [key] window.location.href = "/login";
const { }, 3000);
ciphertext: encryptedPrivateKey, } catch (error) {
iv: encryptedPrivateKeyIV, createNotification({
tag: encryptedPrivateKeyTag type: "error",
} = Aes256Gcm.encrypt({ text: (error as Error).message ?? "Error setting password"
text: localStorage.getItem("PRIVATE_KEY") as string, });
secret: key navigate({ to: "/personal-settings" });
}); }
// 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" });
}
});
}
);
} }
}; };
@@ -132,11 +132,8 @@ export const SelectOrganizationSection = () => {
} }
if (callbackPort) { if (callbackPort) {
const privateKey = localStorage.getItem("PRIVATE_KEY");
let error: string | null = null; let error: string | null = null;
if (!privateKey) error = "Private key not found";
if (!user?.email) error = "User email not found"; if (!user?.email) error = "User email not found";
if (!token) error = "No token found"; if (!token) error = "No token found";
@@ -151,7 +148,7 @@ export const SelectOrganizationSection = () => {
const payload = { const payload = {
JTWToken: token, JTWToken: token,
email: user?.email, email: user?.email,
privateKey privateKey: ""
} as IsCliLoginSuccessful["loginResponse"]; } as IsCliLoginSuccessful["loginResponse"];
// send request to server endpoint // send request to server endpoint
@@ -1,23 +1,17 @@
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import crypto from "crypto";
import { useState } from "react"; import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useSearch } from "@tanstack/react-router"; import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
import { Mfa } from "@app/components/auth/Mfa"; import { Mfa } from "@app/components/auth/Mfa";
import InputField from "@app/components/basic/InputField"; import InputField from "@app/components/basic/InputField";
import checkPassword from "@app/components/utilities/checks/password/checkPassword"; 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 SecurityClient from "@app/components/utilities/SecurityClient";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { import {
completeAccountSignupInvite, completeAccountSignupInvite,
@@ -28,9 +22,6 @@ import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { isLoggedIn } from "@app/hooks/api/reactQuery"; import { isLoggedIn } from "@app/hooks/api/reactQuery";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Errors = { type Errors = {
tooShort?: string; tooShort?: string;
tooLong?: string; tooLong?: string;
@@ -67,7 +58,6 @@ export const SignupInvitePage = () => {
const metadata = queryParams.get("metadata") || undefined; const metadata = queryParams.get("metadata") || undefined;
const { mutateAsync: selectOrganization } = useSelectOrganization(); const { mutateAsync: selectOrganization } = useSelectOrganization();
const { config } = useServerConfig();
const loggedIn = isLoggedIn(); const loggedIn = isLoggedIn();
@@ -94,123 +84,56 @@ export const SignupInvitePage = () => {
} }
if (!errorCheck) { if (!errorCheck) {
// Generate a random pair of a public and a private key try {
const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled); 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( const userOrgs = await fetchOrganizations();
{
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
});
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 const completeSignupFlow = async () => {
// key with the symmetric key [key] const {
const { token: mfaToken,
ciphertext: encryptedPrivateKey, isMfaEnabled,
iv: encryptedPrivateKeyIV, mfaMethod
tag: encryptedPrivateKeyTag } = await selectOrganization({
} = Aes256Gcm.encrypt({ organizationId: orgId
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);
}
}); });
}
); 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 { } else {
setIsLoading(false); setIsLoading(false);
} }
@@ -3,24 +3,16 @@ import crypto from "crypto";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import jsrp from "jsrp";
import { Mfa } from "@app/components/auth/Mfa"; 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 SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2"; import { Button, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { initProjectHelper } from "@app/helpers/project"; import { initProjectHelper } from "@app/helpers/project";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types"; import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Props = { type Props = {
username: string; username: string;
password: string; password: string;
@@ -64,7 +56,6 @@ export const UserInfoSSOStep = ({
const { mutateAsync: selectOrganization } = useSelectOrganization(); const { mutateAsync: selectOrganization } = useSelectOrganization();
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const navigate = useNavigate(); const navigate = useNavigate();
const { config } = useServerConfig();
useEffect(() => { useEffect(() => {
const randomPassword = crypto.randomBytes(32).toString("hex"); const randomPassword = crypto.randomBytes(32).toString("hex");
@@ -93,131 +84,64 @@ export const UserInfoSSOStep = ({
} }
if (!errorCheck) { if (!errorCheck) {
// Generate a random pair of a public and a private key try {
const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled); const response = await completeAccountSignup({
localStorage.setItem("PRIVATE_KEY", privateKey); email: username,
password,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
providerAuthToken,
organizationName,
attributionSource,
useDefaultOrg: forceDefaultOrg
});
client.init( // unset signup JWT token and set JWT token
{ SecurityClient.setSignupToken("");
username, SecurityClient.setToken(response.token);
password SecurityClient.setProviderAuthToken("");
},
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 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 if (isMfaEnabled) {
// key with the symmetric key [key] SecurityClient.setMfaToken(token);
const { if (mfaMethod) {
ciphertext: encryptedPrivateKey, setRequiredMfaMethod(mfaMethod);
iv: encryptedPrivateKeyIV, }
tag: encryptedPrivateKeyTag toggleShowMfa.on();
} = Aes256Gcm.encrypt({ setMfaSuccessCallback(() => completeSignupFlow);
text: privateKey, return;
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);
} }
});
} // 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 { } else {
setIsLoading(false); setIsLoading(false);
} }
@@ -8,7 +8,6 @@ import { useNavigate } from "@tanstack/react-router";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import attemptChangePassword from "@app/components/utilities/attemptChangePassword";
import checkPassword from "@app/components/utilities/checks/password/checkPassword"; import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { useUser } from "@app/context"; import { useUser } from "@app/context";
@@ -68,10 +67,9 @@ export const ChangePasswordSection = () => {
newPassword newPassword
}); });
} else { } else {
await attemptChangePassword({ createNotification({
email: user.username, text: "Legacy encryption scheme not supported for changing password. Please log out and log back in before changing your password.",
currentPassword: oldPassword, type: "error"
newPassword
}); });
} }
@@ -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<typeof schema>;
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 (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<h2 className="flex-1 text-xl font-semibold text-mineshaft-100">Emergency Kit</h2>
<p className="mb-8 text-gray-400">
The kit contains information you can use to recover your account.
</p>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="Password"
type="password"
{...field}
className="bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="password"
/>
</div>
<Button type="submit" colorSchema="secondary" isLoading={false}>
Save
</Button>
</form>
);
};
@@ -1 +0,0 @@
export { EmergencyKitSection } from "./EmergencyKitSection";
@@ -1,20 +1,12 @@
import { useUser } from "@app/context";
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
import { DeleteAccountSection } from "../DeleteAccountSection"; import { DeleteAccountSection } from "../DeleteAccountSection";
import { EmergencyKitSection } from "../EmergencyKitSection";
import { SessionsSection } from "../SessionsSection"; import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection"; import { UserNameSection } from "../UserNameSection";
export const PersonalGeneralTab = () => { export const PersonalGeneralTab = () => {
const { user } = useUser();
const encryptionVersion = user?.encryptionVersion ?? UserEncryptionVersion.V2;
return ( return (
<div> <div>
<UserNameSection /> <UserNameSection />
<SessionsSection /> <SessionsSection />
{encryptionVersion === UserEncryptionVersion.V1 && <EmergencyKitSection />}
<DeleteAccountSection /> <DeleteAccountSection />
</div> </div>
); );