mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 15:26:06 +00:00
feat: srp removal
This commit is contained in:
@@ -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>
|
||||||
|
>;
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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">;
|
||||||
|
|||||||
@@ -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">;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 := ¶ms{
|
|
||||||
memory: 64 * 1024,
|
|
||||||
iterations: 3,
|
|
||||||
parallelism: 1,
|
|
||||||
keyLength: 32,
|
|
||||||
}
|
|
||||||
|
|
||||||
derivedKey, err := generateFromPassword(password, []byte(loginOneResponse.Salt), parameters)
|
|
||||||
if err != nil {
|
|
||||||
util.HandleError(fmt.Errorf("unable to generate argon hash from password [err=%s]", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
decryptedProtectedKey, err := crypto.DecryptSymmetric(derivedKey, protectedKey, protectedKeyTag, protectedKeyIV)
|
|
||||||
if err != nil {
|
|
||||||
util.HandleError(fmt.Errorf("unable to get decrypted protected key [err=%s]", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptedPrivateKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
util.HandleError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
decryptedProtectedKeyInHex, err := hex.DecodeString(string(decryptedProtectedKey))
|
|
||||||
if err != nil {
|
|
||||||
util.HandleError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv)
|
|
||||||
if err != nil {
|
|
||||||
util.HandleError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
decryptedPrivateKey = computedDecryptedPrivateKey
|
|
||||||
} else {
|
|
||||||
util.PrintErrorMessageAndExit("Insufficient details to decrypt private key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(decryptedPrivateKey) == "" || email == "" || loginTwoResponse.Token == "" {
|
|
||||||
log.Debug().Msgf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token)
|
|
||||||
util.PrintErrorMessageAndExit("We were unable to fetch required details to complete your login. Run with -d to see more info")
|
|
||||||
}
|
|
||||||
// Login is successful so ask user to choose organization
|
// 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 {
|
||||||
|
|||||||
@@ -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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-129
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-80
@@ -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";
|
|
||||||
-8
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user