Merge remote-tracking branch 'origin' into secrets-mgmt-docs

This commit is contained in:
Tuan Dang
2025-08-06 19:26:59 +07:00
115 changed files with 1191 additions and 2478 deletions
@@ -0,0 +1,18 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.UserEncryptionKey, (table) => {
table.text("encryptedPrivateKey").nullable().alter();
table.text("publicKey").nullable().alter();
table.text("iv").nullable().alter();
table.text("tag").nullable().alter();
table.text("salt").nullable().alter();
table.text("verifier").nullable().alter();
});
}
export async function down(): Promise<void> {
// do nothing for now to avoid breaking down migrations
}
@@ -15,12 +15,12 @@ export const UserEncryptionKeysSchema = z.object({
protectedKey: z.string().nullable().optional(), protectedKey: z.string().nullable().optional(),
protectedKeyIV: z.string().nullable().optional(), protectedKeyIV: z.string().nullable().optional(),
protectedKeyTag: z.string().nullable().optional(), protectedKeyTag: z.string().nullable().optional(),
publicKey: z.string(), publicKey: z.string().nullable().optional(),
encryptedPrivateKey: z.string(), encryptedPrivateKey: z.string().nullable().optional(),
iv: z.string(), iv: z.string().nullable().optional(),
tag: z.string(), tag: z.string().nullable().optional(),
salt: z.string(), salt: z.string().nullable().optional(),
verifier: z.string(), verifier: z.string().nullable().optional(),
userId: z.string().uuid(), userId: z.string().uuid(),
hashedPassword: z.string().nullable().optional(), hashedPassword: z.string().nullable().optional(),
serverEncryptedPrivateKey: z.string().nullable().optional(), serverEncryptedPrivateKey: z.string().nullable().optional(),
+4
View File
@@ -115,6 +115,10 @@ export const generateUserSrpKeys = async (password: string) => {
}; };
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => { export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
throw new Error("User encrypted private key not found");
}
const derivedKey = await argon2.hash(password, { const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt), salt: Buffer.from(user.salt),
memoryCost: 65536, memoryCost: 65536,
+4 -3
View File
@@ -1,7 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { crypto } from "@app/lib/crypto"; import { initEnvConfig } from "@app/lib/config/env";
import { initLogger } from "@app/lib/logger"; import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { AuthMethod } from "../../services/auth/auth-type"; import { AuthMethod } from "../../services/auth/auth-type";
@@ -17,7 +17,7 @@ export async function seed(knex: Knex): Promise<void> {
initLogger(); initLogger();
const superAdminDAL = superAdminDALFactory(knex); const superAdminDAL = superAdminDALFactory(knex);
await crypto.initialize(superAdminDAL); await initEnvConfig(superAdminDAL, logger);
await knex(TableName.SuperAdmin).insert([ await knex(TableName.SuperAdmin).insert([
// eslint-disable-next-line // eslint-disable-next-line
@@ -25,6 +25,7 @@ export async function seed(knex: Knex): Promise<void> {
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true } { id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
]); ]);
// Inserts seed entries // Inserts seed entries
const [user] = await knex(TableName.Users) const [user] = await knex(TableName.Users)
.insert([ .insert([
{ {
+180 -19
View File
@@ -1,9 +1,28 @@
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 { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { initLogger, logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AuthMethod } from "@app/services/auth/auth-type";
import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns";
import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { userDALFactory } from "@app/services/user/user-dal";
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; import {
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data"; OrgMembershipRole,
OrgMembershipStatus,
ProjectMembershipRole,
ProjectType,
SecretEncryptionAlgo,
SecretKeyEncoding,
TableName
} from "../schemas";
import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [ export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" }, { name: "Development", slug: "dev" },
@@ -11,12 +30,159 @@ export const DEFAULT_PROJECT_ENVS = [
{ name: "Production", slug: "prod" } { name: "Production", slug: "prod" }
]; ];
const createUserWithGhostUser = async (
orgId: string,
projectId: string,
userId: string,
userOrgMembershipId: string,
knex: Knex
) => {
const projectKeyDAL = projectKeyDALFactory(knex);
const userDAL = userDALFactory(knex);
const projectMembershipDAL = projectMembershipDALFactory(knex);
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex);
const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
const password = crypto.randomBytes(128).toString("hex");
const [ghostUser] = await knex(TableName.Users)
.insert({
isGhost: true,
authMethods: [AuthMethod.EMAIL],
username: email,
email,
isAccepted: true
})
.returning("*");
const encKeys = await generateUserSrpKeys(email, password);
await knex(TableName.UserEncryptionKey)
.insert({ userId: ghostUser.id, encryptionVersion: 2, publicKey: encKeys.publicKey })
.onConflict("userId")
.merge();
await knex(TableName.OrgMembership)
.insert({
orgId,
userId: ghostUser.id,
role: OrgMembershipRole.Admin,
status: OrgMembershipStatus.Accepted,
isActive: true
})
.returning("*");
const [projectMembership] = await knex(TableName.ProjectMembership)
.insert({
userId: ghostUser.id,
projectId
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
projectMembershipId: projectMembership.id,
role: ProjectMembershipRole.Admin
});
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: encKeys.publicKey,
privateKey: encKeys.plainPrivateKey
});
await knex(TableName.ProjectKeys).insert({
projectId,
receiverId: ghostUser.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: ghostUser.id
});
const { iv, tag, ciphertext, encoding, algorithm } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(encKeys.plainPrivateKey);
await knex(TableName.ProjectBot).insert({
name: "Infisical Bot (Ghost)",
projectId,
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: encKeys.publicKey,
senderId: ghostUser.id,
algorithm,
keyEncoding: encoding
});
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, knex);
if (!latestKey) {
throw new Error("Latest key not found for user");
}
const user = await userDAL.findUserEncKeyByUserId(userId, knex);
if (!user || !user.publicKey) {
throw new Error("User not found");
}
const [projectAdmin] = assignWorkspaceKeysToMembers({
decryptKey: latestKey,
userPrivateKey: encKeys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: userOrgMembershipId
}
]
});
// Create a membership for the user
const userProjectMembership = await projectMembershipDAL.create(
{
projectId,
userId: user.id
},
knex
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
knex
);
// Create a project key for the user
await projectKeyDAL.create(
{
encryptedKey: projectAdmin.workspaceEncryptedKey,
nonce: projectAdmin.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
},
knex
);
return {
user: ghostUser,
keys: encKeys
};
};
export async function seed(knex: Knex): Promise<void> { export async function seed(knex: Knex): Promise<void> {
// Deletes ALL existing entries // Deletes ALL existing entries
await knex(TableName.Project).del(); await knex(TableName.Project).del();
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,
@@ -29,29 +195,24 @@ export async function seed(knex: Knex): Promise<void> {
}) })
.returning("*"); .returning("*");
const projectMembership = await knex(TableName.ProjectMembership) const userOrgMembership = await knex(TableName.OrgMembership)
.insert({ .where({
projectId: project.id, orgId: seedData1.organization.id,
userId: seedData1.id userId: seedData1.id
}) })
.returning("*"); .first();
await knex(TableName.ProjectUserMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectMembershipId: projectMembership[0].id
});
if (!userOrgMembership) {
throw new Error("User org membership not found");
}
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");
const userPrivateKey = await getUserPrivateKey(seedData1.password, user); if (!user.publicKey) {
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey); throw new Error("User public key not found");
await knex(TableName.ProjectKeys).insert({ }
projectId: project.id,
nonce: projectKey.nonce, await createUserWithGhostUser(seedData1.organization.id, project.id, seedData1.id, userOrgMembership.id, knex);
encryptedKey: projectKey.ciphertext,
receiverId: seedData1.id,
senderId: seedData1.id
});
// create default environments and default folders // create default environments and default folders
const envs = await knex(TableName.Environment) const envs = await knex(TableName.Environment)
@@ -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([
{ {
@@ -13,11 +13,9 @@ const AUTH_REFRESH_INTERVAL = 60 * 1000;
const HEART_BEAT_INTERVAL = 15 * 1000; const HEART_BEAT_INTERVAL = 15 * 1000;
export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => { export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
let heartbeatInterval: NodeJS.Timeout | null = null;
const clients = new Set<EventStreamClient>(); const clients = new Set<EventStreamClient>();
heartbeatInterval = setInterval(() => { const heartbeatInterval = setInterval(() => {
for (const client of clients) { for (const client of clients) {
if (client.stream.closed) continue; if (client.stream.closed) continue;
void client.ping(); void client.ping();
@@ -66,15 +66,24 @@ export type EventStreamClient = {
}; };
export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient { export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient {
const rules = options.registered.map((r) => ({ const rules = options.registered.map((r) => {
subject: options.type, const secretPath = r.conditions?.secretPath;
action: "subscribe", const hasConditions = r.conditions?.environmentSlug || r.conditions?.secretPath;
conditions: {
eventType: r.event, return {
secretPath: r.conditions?.secretPath ?? "/", subject: options.type,
environment: r.conditions?.environmentSlug action: "subscribe",
} conditions: {
})); eventType: r.event,
...(hasConditions
? {
environment: r.conditions?.environmentSlug ?? "",
secretPath: { $glob: secretPath }
}
: {})
}
};
});
const id = `sse-${nanoid()}`; const id = `sse-${nanoid()}`;
const control = new AbortController(); const control = new AbortController();
+25 -1
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
@@ -65,6 +65,18 @@ const addAcceptedUsersToGroup = async ({
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`)); const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
for await (const projectId of projectIds) { for await (const projectId of projectIds) {
const project = await projectDAL.findById(projectId, tx);
if (!project) {
throw new NotFoundError({
message: `Failed to find project with ID '${projectId}'`
});
}
if (project.version !== ProjectVersion.V1 && project.version !== ProjectVersion.V2) {
// eslint-disable-next-line no-continue
continue;
}
const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`)); const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
if (usersToAddProjectKeyFor.length) { if (usersToAddProjectKeyFor.length) {
@@ -86,6 +98,12 @@ const addAcceptedUsersToGroup = async ({
}); });
} }
if (!ghostUserLatestKey.sender.publicKey) {
throw new NotFoundError({
message: `Failed to find project owner's public key in project with ID '${projectId}'`
});
}
const bot = await projectBotDAL.findOne({ projectId }, tx); const bot = await projectBotDAL.findOne({ projectId }, tx);
if (!bot) { if (!bot) {
@@ -112,6 +130,12 @@ const addAcceptedUsersToGroup = async ({
}); });
const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => { const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
if (!user.publicKey) {
throw new NotFoundError({
message: `Failed to find user's public key in project with ID '${projectId}'`
});
}
const { ciphertext: encryptedKey, nonce } = crypto const { ciphertext: encryptedKey, nonce } = crypto
.encryption() .encryption()
.asymmetric() .asymmetric()
@@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = {
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find" "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
>; >;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
+3 -3
View File
@@ -65,7 +65,7 @@ export type TAddUsersToGroup = {
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">; userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx: Knex; tx: Knex;
}; };
@@ -78,7 +78,7 @@ export type TAddUsersToGroupByUserIds = {
orgDAL: Pick<TOrgDALFactory, "findMembership">; orgDAL: Pick<TOrgDALFactory, "findMembership">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex; tx?: Knex;
}; };
@@ -102,7 +102,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex; tx?: Knex;
}; };
@@ -55,7 +55,7 @@ type TLdapConfigServiceFactoryDep = {
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">; groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
@@ -79,7 +79,7 @@ type TOidcConfigServiceFactoryDep = {
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">; auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
+1 -1
View File
@@ -59,7 +59,7 @@ type TScimServiceFactoryDep = {
TOrgMembershipDALFactory, TOrgMembershipDALFactory,
"find" | "findOne" | "create" | "updateById" | "findById" | "update" "find" | "findOne" | "create" | "updateById" | "findById" | "update"
>; >;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser" | "findById">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
+9 -1
View File
@@ -70,7 +70,8 @@ export enum ApiDocsTags {
SecretScanning = "Secret Scanning", SecretScanning = "Secret Scanning",
OidcSso = "OIDC SSO", OidcSso = "OIDC SSO",
SamlSso = "SAML SSO", SamlSso = "SAML SSO",
LdapSso = "LDAP SSO" LdapSso = "LDAP SSO",
Events = "Event Subscriptions"
} }
export const GROUPS = { export const GROUPS = {
@@ -2872,3 +2873,10 @@ export const LdapSso = {
caCert: "The CA certificate to use when verifying the LDAP server certificate." caCert: "The CA certificate to use when verifying the LDAP server certificate."
} }
}; };
export const EventSubscriptions = {
SUBSCRIBE_PROJECT_EVENTS: {
projectId: "The ID of the project to subscribe to events for.",
register: "List of events you want to subscribe to"
}
};
+17 -1
View File
@@ -53,7 +53,7 @@ type DecryptedIntegrationAuths = z.infer<typeof DecryptedIntegrationAuthsSchema>
type TLatestKey = TProjectKeys & { type TLatestKey = TProjectKeys & {
sender: { sender: {
publicKey: string; publicKey?: string;
}; };
}; };
@@ -91,6 +91,10 @@ const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: s
return results; return results;
}; };
export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => { export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
@@ -143,6 +147,10 @@ export const decryptSecretVersions = (
privateKey: string, privateKey: string,
latestKey: TLatestKey latestKey: TLatestKey
) => { ) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
@@ -195,6 +203,10 @@ export const decryptSecretApprovals = (
privateKey: string, privateKey: string,
latestKey: TLatestKey latestKey: TLatestKey
) => { ) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
@@ -247,6 +259,10 @@ export const decryptIntegrationAuths = (
privateKey: string, privateKey: string,
latestKey: TLatestKey latestKey: TLatestKey
) => { ) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
+18
View File
@@ -4,6 +4,7 @@ import jsrp from "jsrp";
import { TUserEncryptionKeys } from "@app/db/schemas"; import { TUserEncryptionKeys } from "@app/db/schemas";
import { UserEncryption } from "@app/services/user/user-types"; import { UserEncryption } from "@app/services/user/user-types";
import { BadRequestError } from "../errors";
import { crypto, SymmetricKeySize } from "./cryptography"; import { crypto, SymmetricKeySize } from "./cryptography";
export const generateSrpServerKey = async (salt: string, verifier: string) => { export const generateSrpServerKey = async (salt: string, verifier: string) => {
@@ -127,6 +128,10 @@ export const getUserPrivateKey = async (
> >
) => { ) => {
if (user.encryptionVersion === UserEncryption.V1) { if (user.encryptionVersion === UserEncryption.V1) {
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
throw new BadRequestError({ message: "User encrypted private key not found" });
}
return crypto return crypto
.encryption() .encryption()
.symmetric() .symmetric()
@@ -138,12 +143,25 @@ export const getUserPrivateKey = async (
keySize: SymmetricKeySize.Bits128 keySize: SymmetricKeySize.Bits128
}); });
} }
// still used for legacy things
if ( if (
user.encryptionVersion === UserEncryption.V2 && user.encryptionVersion === UserEncryption.V2 &&
user.protectedKey && user.protectedKey &&
user.protectedKeyIV && user.protectedKeyIV &&
user.protectedKeyTag user.protectedKeyTag
) { ) {
if (
!user.salt ||
!user.protectedKey ||
!user.protectedKeyIV ||
!user.protectedKeyTag ||
!user.encryptedPrivateKey ||
!user.iv ||
!user.tag
) {
throw new BadRequestError({ message: "User encrypted private key not found" });
}
const derivedKey = await argon2.hash(password, { const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt), salt: Buffer.from(user.salt),
memoryCost: 65536, memoryCost: 65536,
-5
View File
@@ -775,7 +775,6 @@ export const registerRoutes = async (
orgRoleDAL, orgRoleDAL,
permissionService, permissionService,
orgDAL, orgDAL,
projectBotDAL,
incidentContactDAL, incidentContactDAL,
tokenService, tokenService,
projectUserAdditionalPrivilegeDAL, projectUserAdditionalPrivilegeDAL,
@@ -850,7 +849,6 @@ export const registerRoutes = async (
projectDAL, projectDAL,
permissionService, permissionService,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
userDAL,
projectBotDAL, projectBotDAL,
projectKeyDAL, projectKeyDAL,
projectMembershipDAL projectMembershipDAL
@@ -1138,11 +1136,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,
@@ -1162,7 +1158,6 @@ export const registerRoutes = async (
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
keyStore, keyStore,
kmsService, kmsService,
projectBotDAL,
certificateTemplateDAL, certificateTemplateDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
+18 -11
View File
@@ -7,6 +7,7 @@ import { ActionProjectType, ProjectType } from "@app/db/schemas";
import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream"; import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream";
import { EventRegisterSchema } from "@app/ee/services/event/types"; import { EventRegisterSchema } from "@app/ee/services/event/types";
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { ApiDocsTags, EventSubscriptions } from "@app/lib/api-docs";
import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -20,10 +21,14 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.Events],
description: "Subscribe to project events",
body: z.object({ body: z.object({
projectId: z.string().trim(), projectId: z.string().trim().describe(EventSubscriptions.SUBSCRIBE_PROJECT_EVENTS.projectId),
register: z.array(EventRegisterSchema).max(10) register: z.array(EventRegisterSchema).min(1).max(10)
}) }),
produces: ["text/event-stream"]
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, reply) => { handler: async (req, reply) => {
@@ -75,13 +80,15 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
} }
req.body.register.forEach((r) => { req.body.register.forEach((r) => {
const fields = {
environment: r.conditions?.environmentSlug ?? "",
secretPath: r.conditions?.secretPath ?? "/",
eventType: r.event
};
const allowed = info.permission.can( const allowed = info.permission.can(
ProjectPermissionSecretActions.Subscribe, ProjectPermissionSecretActions.Subscribe,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, fields)
environment: r.conditions?.environmentSlug ?? "",
secretPath: r.conditions?.secretPath ?? "/",
eventType: r.event
})
); );
if (!allowed) { if (!allowed) {
@@ -89,9 +96,9 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
name: "PermissionDenied", name: "PermissionDenied",
message: `You are not allowed to subscribe on secrets`, message: `You are not allowed to subscribe on secrets`,
details: { details: {
event: r.event, event: fields.eventType,
environmentSlug: r.conditions?.environmentSlug, environmentSlug: fields.environment,
secretPath: r.conditions?.secretPath ?? "/" secretPath: fields.secretPath
} }
}); });
} }
@@ -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()
+4 -38
View File
@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -19,23 +19,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
user: UsersSchema.merge( user: UsersSchema.extend({
UserEncryptionKeysSchema.pick({ encryptionVersion: z.number()
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 +80,6 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/private-key",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
const privateKey = await server.services.user.getUserPrivateKey(req.permission.id);
return { privateKey };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:userId/unlock", url: "/:userId/unlock",
+7 -7
View File
@@ -97,13 +97,13 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
encryptionVersion: z.number().default(1).nullable().optional(), encryptionVersion: z.number().default(1).nullable().optional(),
protectedKey: z.string().nullable(), protectedKey: z.string().nullish(),
protectedKeyIV: z.string().nullable(), protectedKeyIV: z.string().nullish(),
protectedKeyTag: z.string().nullable(), protectedKeyTag: z.string().nullish(),
publicKey: z.string(), publicKey: z.string().nullish(),
encryptedPrivateKey: z.string(), encryptedPrivateKey: z.string().nullish(),
iv: z.string(), iv: z.string().nullish(),
tag: z.string(), tag: z.string().nullish(),
token: z.string() token: z.string()
}) })
} }
@@ -153,7 +153,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
firstName: true, firstName: true,
lastName: true, lastName: true,
id: true id: true
}).extend({ publicKey: z.string().nullable() }) }).extend({ publicKey: z.string().nullish() })
}).omit({ createdAt: true, updatedAt: true }) }).omit({ createdAt: true, updatedAt: true })
}) })
} }
@@ -1,5 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit } from "@app/server/config/rateLimiter"; import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns"; import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
@@ -41,13 +42,38 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
rateLimit: authRateLimit rateLimit: authRateLimit
}, },
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => { handler: async (req, res) => {
const appCfg = getConfig();
await server.services.password.resetPasswordV2({ await server.services.password.resetPasswordV2({
type: ResetPasswordV2Type.LoggedInReset, type: ResetPasswordV2Type.LoggedInReset,
userId: req.permission.id, userId: req.permission.id,
newPassword: req.body.newPassword, newPassword: req.body.newPassword,
oldPassword: req.body.oldPassword oldPassword: req.body.oldPassword
}); });
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
void res.cookie("aod", "", {
httpOnly: false,
path: "/",
sameSite: "lax",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
} }
}); });
}; };
@@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: ProjectKeysSchema.merge( 200: ProjectKeysSchema.merge(
z.object({ z.object({
sender: z.object({ sender: z.object({
publicKey: z.string() publicKey: z.string().optional()
}) })
}) })
) )
+65 -10
View File
@@ -20,8 +20,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
serverPublicKey: z.string(), serverPublicKey: z.string().nullish(),
salt: z.string() salt: z.string().nullish()
}) })
} }
}, },
@@ -124,14 +124,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
encryptionVersion: z.number().default(1).nullable().optional(), encryptionVersion: z.number().default(1).nullish(),
protectedKey: z.string().nullable(), protectedKey: z.string().nullish(),
protectedKeyIV: z.string().nullable(), protectedKeyIV: z.string().nullish(),
protectedKeyTag: z.string().nullable(), protectedKeyTag: z.string().nullish(),
publicKey: z.string(), publicKey: z.string().nullish(),
encryptedPrivateKey: z.string(), encryptedPrivateKey: z.string().nullish(),
iv: z.string(), iv: z.string().nullish(),
tag: z.string(), tag: z.string().nullish(),
token: z.string() token: z.string()
}) })
} }
@@ -181,4 +181,59 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
} as const; } as const;
} }
}); });
// New login route that doesn't use SRP
server.route({
method: "POST",
url: "/login",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
email: z.string().trim(),
password: z.string().trim(),
providerAuthToken: z.string().trim().optional(),
captchaToken: z.string().trim().optional()
}),
response: {
200: z.object({
accessToken: z.string()
})
}
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
if (!userAgent) throw new Error("user agent header is required");
const { tokens } = 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 };
}
});
}; };
@@ -98,15 +98,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
email: z.string().trim(), email: z.string().trim(),
firstName: z.string().trim(), firstName: z.string().trim(),
lastName: z.string().trim().optional(), lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
publicKey: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
providerAuthToken: z.string().trim().optional().nullish(), providerAuthToken: z.string().trim().optional().nullish(),
attributionSource: z.string().trim().optional(), attributionSource: z.string().trim().optional(),
password: z.string() password: z.string()
@@ -189,15 +180,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
password: z.string(), password: z.string(),
firstName: z.string().trim(), firstName: z.string().trim(),
lastName: z.string().trim().optional(), lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
publicKey: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
tokenMetadata: z.string().optional() tokenMetadata: z.string().optional()
}), }),
response: { response: {
+57 -2
View File
@@ -1,8 +1,16 @@
import { TUsers } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type"; import {
AuthMethod,
AuthModeProviderJwtTokenPayload,
AuthModeProviderSignUpTokenPayload,
AuthTokenType
} from "./auth-type";
export const validateProviderAuthToken = (providerToken: string, username?: string) => { export const validateProviderAuthToken = (providerToken: string, username?: string) => {
if (!providerToken) throw new UnauthorizedError(); if (!providerToken) throw new UnauthorizedError();
@@ -97,3 +105,50 @@ export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?:
} }
} }
}; };
export const verifyCaptcha = async (user: TUsers, captchaToken?: string) => {
const appCfg = getConfig();
if (
user.consecutiveFailedPasswordAttempts &&
user.consecutiveFailedPasswordAttempts >= 10 &&
Boolean(appCfg.CAPTCHA_SECRET)
) {
if (!captchaToken) {
throw new BadRequestError({
name: "Captcha Required",
message: "Accomplish the required captcha by logging in via Web"
});
}
// validate captcha token
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
response: captchaToken,
secret: appCfg.CAPTCHA_SECRET
});
if (!response.data.success) {
throw new BadRequestError({
name: "Invalid Captcha"
});
}
}
};
export const getAuthMethodAndOrgId = (email: string, providerAuthToken?: string) => {
let authMethod = AuthMethod.EMAIL;
let organizationId: string | undefined;
if (providerAuthToken) {
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
authMethod = decodedProviderToken.authMethod;
if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
decodedProviderToken.orgId
) {
organizationId = decodedProviderToken.orgId;
}
}
return { authMethod, organizationId };
};
+86 -42
View File
@@ -4,7 +4,6 @@ import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSc
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
@@ -22,7 +21,8 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { LoginMethod } from "../super-admin/super-admin-types"; import { LoginMethod } from "../super-admin/super-admin-types";
import { TTotpServiceFactory } from "../totp/totp-service"; import { TTotpServiceFactory } from "../totp/totp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns"; import { UserEncryption } from "../user/user-types";
import { enforceUserLockStatus, getAuthMethodAndOrgId, validateProviderAuthToken, verifyCaptcha } from "./auth-fns";
import { import {
TLoginClientProofDTO, TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO, TLoginGenServerPublicKeyDTO,
@@ -208,6 +208,10 @@ export const authLoginServiceFactory = ({
throw new Error("Failed to find user"); throw new Error("Failed to find user");
} }
if (!userEnc.salt || !userEnc.verifier) {
throw new BadRequestError({ message: "Salt or verifier not found" });
}
if ( if (
serverCfg.enabledLoginMethods && serverCfg.enabledLoginMethods &&
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
@@ -247,8 +251,6 @@ export const authLoginServiceFactory = ({
captchaToken, captchaToken,
password password
}: TLoginClientProofDTO) => { }: TLoginClientProofDTO) => {
const appCfg = getConfig();
// akhilmhdh: case sensitive email resolution // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({ const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email username: email
@@ -259,44 +261,11 @@ export const authLoginServiceFactory = ({
const user = await userDAL.findById(userEnc.userId); const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig(); const cfg = getConfig();
let authMethod = AuthMethod.EMAIL; const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken);
let organizationId: string | undefined; await verifyCaptcha(user, captchaToken);
if (providerAuthToken) { if (!userEnc.salt || !userEnc.verifier) {
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); throw new BadRequestError({ message: "Salt or verifier not found" });
authMethod = decodedProviderToken.authMethod;
if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
decodedProviderToken.orgId
) {
organizationId = decodedProviderToken.orgId;
}
}
if (
user.consecutiveFailedPasswordAttempts &&
user.consecutiveFailedPasswordAttempts >= 10 &&
Boolean(appCfg.CAPTCHA_SECRET)
) {
if (!captchaToken) {
throw new BadRequestError({
name: "Captcha Required",
message: "Accomplish the required captcha by logging in via Web"
});
}
// validate captcha token
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
response: captchaToken,
secret: appCfg.CAPTCHA_SECRET
});
if (!response.data.success) {
throw new BadRequestError({
name: "Invalid Captcha"
});
}
} }
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?"); if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
@@ -371,6 +340,80 @@ 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))) {
await userDAL.update(
{ id: userEnc.userId },
{
$incr: {
consecutiveFailedPasswordAttempts: 1
}
}
);
throw new BadRequestError({ message: "Invalid username or email" });
}
const token = await generateUserTokens({
user: {
...userEnc,
id: userEnc.userId
},
ip,
userAgent,
authMethod,
organizationId
});
return {
tokens: {
accessToken: token.access,
refreshToken: token.refresh
},
user: userEnc
} as const;
};
const selectOrganization = async ({ const selectOrganization = async ({
userAgent, userAgent,
authJwtToken, authJwtToken,
@@ -862,6 +905,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
*/ */
@@ -215,58 +137,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);
@@ -317,66 +198,6 @@ export const authPaswordServiceFactory = ({
}); });
}; };
/*
* backup key creation to give user's their access back when lost their password
* this also needs to do the generateServerPubKey function to be executed first
* then only client proof can be verified
* */
const createBackupPrivateKey = async ({
clientProof,
encryptedPrivateKey,
salt,
verifier,
iv,
tag,
userId
}: TCreateBackupPrivateKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key");
const isValidClientProff = await srpCheckClientProof(
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProff) throw new Error("failed to create backup key");
const backup = await authDAL.transaction(async (tx) => {
const backupKey = await authDAL.upsertBackupKey(
userEnc.userId,
{
encryptedPrivateKey,
iv,
tag,
salt,
verifier,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
},
tx
);
await userDAL.updateUserEncryptionByUserId(
userEnc.userId,
{
serverPrivateKey: null,
clientPublicKey: null
},
tx
);
return backupKey;
});
return backup;
};
/*
* Return user back up
* */
const getBackupPrivateKeyOfUser = async (userId: string) => { const 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)) {
@@ -420,21 +241,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,
@@ -470,15 +277,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
@@ -491,12 +290,9 @@ export const authPaswordServiceFactory = ({
}; };
return { return {
generateServerPubKey,
changePassword,
resetPasswordByBackupKey, resetPasswordByBackupKey,
sendPasswordResetEmail, sendPasswordResetEmail,
verifyPasswordResetEmail, verifyPasswordResetEmail,
createBackupPrivateKey,
getBackupPrivateKeyOfUser, getBackupPrivateKeyOfUser,
sendPasswordSetupEmail, sendPasswordSetupEmail,
setupPassword, setupPassword,
@@ -1,18 +1,3 @@
export type TChangePasswordDTO = {
userId: string;
clientProof: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
tokenVersionId?: string;
password: string;
};
export enum ResetPasswordV2Type { export enum ResetPasswordV2Type {
Recovery = "recovery", Recovery = "recovery",
LoggedInReset = "logged-in-reset" LoggedInReset = "logged-in-reset"
@@ -39,14 +24,6 @@ export type TResetPasswordViaBackupKeyDTO = {
}; };
export type TSetupPasswordViaBackupKeyDTO = { export type TSetupPasswordViaBackupKeyDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string; password: string;
token: string; token: string;
}; };
+18 -194
View File
@@ -1,11 +1,10 @@
import { OrgMembershipStatus, SecretKeyEncoding, TableName } from "@app/db/schemas"; import { OrgMembershipStatus, TableName } from "@app/db/schemas";
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { getMinExpiresIn } from "@app/lib/fn"; import { getMinExpiresIn } from "@app/lib/fn";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
@@ -41,7 +40,7 @@ type TAuthSignupDep = {
| "findUserGroupMembershipsInProject" | "findUserGroupMembershipsInProject"
>; >;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">; orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
@@ -147,17 +146,8 @@ export const authSignupServiceFactory = ({
firstName, firstName,
lastName, lastName,
providerAuthToken, providerAuthToken,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
organizationName, organizationName,
// attributionSource, // attributionSource,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
ip, ip,
userAgent, userAgent,
authorization, authorization,
@@ -191,98 +181,18 @@ export const authSignupServiceFactory = ({
} }
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
const privateKey = await getUserPrivateKey(password, {
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
encryptionVersion: UserEncryption.V2
});
const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
const updateduser = await authDAL.transaction(async (tx) => { const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
let userEncKey;
// below condition is true means this is system generated credentials const userEncKey = await userDAL.upsertUserEncryptionKey(
// the private key is actually system generated password us.id,
// thus we will re-encrypt the system generated private key with the new password {
// akhilmhdh: you may find this like why? The reason is simple we are moving away from e2ee and these are pieces of it encryptionVersion: UserEncryption.V2,
// without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things hashedPassword
if ( },
systemGeneratedUserEncryptionKey && tx
!systemGeneratedUserEncryptionKey.hashedPassword && );
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
) {
// get server generated password
const serverGeneratedPassword = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey
});
const encKeys = await generateUserSrpKeys(email, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey
});
// now reencrypt server generated key with user provided password
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
} else {
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
}
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it // If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
if ( if (
@@ -400,19 +310,10 @@ export const authSignupServiceFactory = ({
const completeAccountInvite = async ({ const completeAccountInvite = async ({
email, email,
ip, ip,
salt,
password, password,
verifier,
firstName, firstName,
publicKey,
userAgent, userAgent,
lastName, lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
authorization authorization
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
const sanitizedEmail = email.trim().toLowerCase(); const sanitizedEmail = email.trim().toLowerCase();
@@ -437,94 +338,17 @@ export const authSignupServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
const privateKey = await getUserPrivateKey(password, {
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
encryptionVersion: 2
});
const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
const updateduser = await authDAL.transaction(async (tx) => { const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx); const userEncKey = await userDAL.upsertUserEncryptionKey(
let userEncKey; us.id,
// this means this is system generated credentials {
// now replace the private key encryptionVersion: 2,
if ( hashedPassword
systemGeneratedUserEncryptionKey && },
!systemGeneratedUserEncryptionKey.hashedPassword && tx
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey && );
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
) {
// get server generated password
const serverGeneratedPassword = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey
});
const encKeys = await generateUserSrpKeys(sanitizedEmail, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey
});
// now reencrypt server generated key with user provided password
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: 2,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
} else {
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
}
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
@@ -3,15 +3,6 @@ export type TCompleteAccountSignupDTO = {
password: string; password: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName?: string; organizationName?: string;
providerAuthToken?: string | null; providerAuthToken?: string | null;
attributionSource?: string | undefined; attributionSource?: string | undefined;
@@ -26,15 +17,6 @@ export type TCompleteAccountInviteDTO = {
password: string; password: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
ip: string; ip: string;
userAgent: string; userAgent: string;
authorization: string; authorization: string;
@@ -2,6 +2,7 @@ import { z } from "zod";
import { isValidIp } from "@app/lib/ip"; import { isValidIp } from "@app/lib/ip";
import { isFQDN } from "@app/lib/validator/validate-url"; import { isFQDN } from "@app/lib/validator/validate-url";
import { TAltNameMapping, TAltNameType } from "@app/services/certificate/certificate-types";
const isValidDate = (dateString: string) => { const isValidDate = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
@@ -56,3 +57,19 @@ export const validateAltNamesField = z
message: "Each alt name must be a valid hostname, email address, IP address or URL" message: "Each alt name must be a valid hostname, email address, IP address or URL"
} }
); );
export const validateAndMapAltNameType = (name: string): TAltNameMapping | null => {
if (isFQDN(name, { allow_wildcard: true, require_tld: false })) {
return { type: TAltNameType.DNS, value: name };
}
if (z.string().url().safeParse(name).success) {
return { type: TAltNameType.URL, value: name };
}
if (z.string().email().safeParse(name).success) {
return { type: TAltNameType.EMAIL, value: name };
}
if (isValidIp(name)) {
return { type: TAltNameType.IP, value: name };
}
return null;
};
@@ -1,7 +1,6 @@
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import RE2 from "re2"; import RE2 from "re2";
import { z } from "zod";
import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas"; import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
@@ -9,7 +8,6 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
@@ -17,7 +15,8 @@ import {
CertExtendedKeyUsage, CertExtendedKeyUsage,
CertKeyAlgorithm, CertKeyAlgorithm,
CertKeyUsage, CertKeyUsage,
CertStatus CertStatus,
TAltNameMapping
} from "@app/services/certificate/certificate-types"; } from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -34,6 +33,7 @@ import {
} from "../certificate-authority-fns"; } from "../certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types"; import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types";
import { validateAndMapAltNameType } from "../certificate-authority-validators";
type TInternalCertificateAuthorityFnsDeps = { type TInternalCertificateAuthorityFnsDeps = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
@@ -152,27 +152,15 @@ export const InternalCertificateAuthorityFns = ({
extensions.push(extendedKeyUsagesExtension); extensions.push(extendedKeyUsagesExtension);
} }
let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = []; let altNamesArray: TAltNameMapping[] = [];
if (subscriber.subjectAlternativeNames?.length) { if (subscriber.subjectAlternativeNames?.length) {
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => { altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
if (z.string().email().safeParse(altName).success) { const altNameType = validateAndMapAltNameType(altName);
return { type: "email", value: altName }; if (!altNameType) {
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
} }
return altNameType;
if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) {
return { type: "dns", value: altName };
}
if (z.string().url().safeParse(altName).success) {
return { type: "url", value: altName };
}
if (z.string().ip().safeParse(altName).success) {
return { type: "ip", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
}); });
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -426,27 +414,15 @@ export const InternalCertificateAuthorityFns = ({
); );
} }
let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = []; let altNamesArray: TAltNameMapping[] = [];
if (altNames) { if (altNames) {
altNamesArray = altNames.split(",").map((altName) => { altNamesArray = altNames.split(",").map((altName) => {
if (z.string().email().safeParse(altName).success) { const altNameType = validateAndMapAltNameType(altName);
return { type: "email", value: altName }; if (!altNameType) {
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
} }
return altNameType;
if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) {
return { type: "dns", value: altName };
}
if (z.string().url().safeParse(altName).success) {
return { type: "url", value: altName };
}
if (z.string().ip().safeParse(altName).success) {
return { type: "ip", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
}); });
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -2,7 +2,6 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
@@ -18,7 +17,6 @@ import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -34,7 +32,8 @@ import {
CertExtendedKeyUsageOIDToName, CertExtendedKeyUsageOIDToName,
CertKeyAlgorithm, CertKeyAlgorithm,
CertKeyUsage, CertKeyUsage,
CertStatus CertStatus,
TAltNameMapping
} from "../../certificate/certificate-types"; } from "../../certificate/certificate-types";
import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal"; import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal";
import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns"; import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns";
@@ -69,6 +68,7 @@ import {
TSignIntermediateDTO, TSignIntermediateDTO,
TUpdateCaDTO TUpdateCaDTO
} from "./internal-certificate-authority-types"; } from "./internal-certificate-authority-types";
import { validateAndMapAltNameType } from "../certificate-authority-validators";
type TInternalCertificateAuthorityServiceFactoryDep = { type TInternalCertificateAuthorityServiceFactoryDep = {
certificateAuthorityDAL: Pick< certificateAuthorityDAL: Pick<
@@ -1364,34 +1364,18 @@ export const internalCertificateAuthorityServiceFactory = ({
); );
} }
let altNamesArray: { let altNamesArray: TAltNameMapping[] = [];
type: "email" | "dns";
value: string;
}[] = [];
if (altNames) { if (altNames) {
altNamesArray = altNames altNamesArray = altNames
.split(",") .split(",")
.map((name) => name.trim()) .map((name) => name.trim())
.map((altName) => { .map((altName): TAltNameMapping => {
// check if the altName is a valid email const altNameType = validateAndMapAltNameType(altName);
if (z.string().email().safeParse(altName).success) { if (!altNameType) {
return { throw new Error(`Invalid altName: ${altName}`);
type: "email",
value: altName
};
} }
return altNameType;
// check if the altName is a valid hostname
if (isFQDN(altName, { allow_wildcard: true })) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
}); });
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -1766,34 +1750,22 @@ export const internalCertificateAuthorityServiceFactory = ({
} }
let altNamesFromCsr: string = ""; let altNamesFromCsr: string = "";
let altNamesArray: { let altNamesArray: TAltNameMapping[] = [];
type: "email" | "dns";
value: string;
}[] = [];
if (altNames) { if (altNames) {
altNamesArray = altNames altNamesArray = altNames
.split(",") .split(",")
.map((name) => name.trim()) .map((name) => name.trim())
.map((altName) => { .map((altName): TAltNameMapping => {
// check if the altName is a valid email const altNameType = validateAndMapAltNameType(altName);
if (z.string().email().safeParse(altName).success) { if (!altNameType) {
return { throw new Error(`Invalid altName: ${altName}`);
type: "email",
value: altName
};
} }
return altNameType;
// check if the altName is a valid hostname
if (isFQDN(altName, { allow_wildcard: true })) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
}); });
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
} else { } else {
// attempt to read from CSR if altNames is not explicitly provided // attempt to read from CSR if altNames is not explicitly provided
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17"); const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
@@ -1801,11 +1773,16 @@ export const internalCertificateAuthorityServiceFactory = ({
const sanNames = new x509.GeneralNames(sanExtension.value); const sanNames = new x509.GeneralNames(sanExtension.value);
altNamesArray = sanNames.items altNamesArray = sanNames.items
.filter((value) => value.type === "email" || value.type === "dns") .filter(
.map((name) => ({ (value) => value.type === "email" || value.type === "dns" || value.type === "url" || value.type === "ip"
type: name.type as "email" | "dns", )
value: name.value .map((name): TAltNameMapping => {
})); const altNameType = validateAndMapAltNameType(name.value);
if (!altNameType) {
throw new Error(`Invalid altName from CSR: ${name.value}`);
}
return altNameType;
});
altNamesFromCsr = sanNames.items.map((item) => item.value).join(","); altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
} }
@@ -104,3 +104,14 @@ export type TGetCertificateCredentialsDTO = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
}; };
export enum TAltNameType {
EMAIL = "email",
DNS = "dns",
IP = "ip",
URL = "url"
}
export type TAltNameMapping = {
type: TAltNameType;
value: 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,19 +1,16 @@
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";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types"; import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
type TOrgAdminServiceFactoryDep = { type TOrgAdminServiceFactoryDep = {
@@ -25,7 +22,6 @@ type TOrgAdminServiceFactoryDep = {
>; >;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">; projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
}; };
@@ -38,7 +34,6 @@ export const orgAdminServiceFactory = ({
projectMembershipDAL, projectMembershipDAL,
projectKeyDAL, projectKeyDAL,
projectBotDAL, projectBotDAL,
userDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
smtpService smtpService
}: TOrgAdminServiceFactoryDep) => { }: TOrgAdminServiceFactoryDep) => {
@@ -83,7 +78,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,
@@ -98,8 +93,10 @@ export const orgAdminServiceFactory = ({
const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId }); const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId });
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
if (project.version === ProjectVersion.V1) { if (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2) {
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" }); throw new BadRequestError({
message: `Project '${project.name}' is a legacy project and must be upgraded before accessing it through the admin console.`
});
} }
// check already there exist a membership if there return it // check already there exist a membership if there return it
@@ -144,30 +141,6 @@ export const orgAdminServiceFactory = ({
}); });
} }
const botPrivateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId);
if (!userEncryptionKey)
throw new NotFoundError({ message: `User encryption key for user with ID '${actorId}' not found` });
const [newWsMember] = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: [
{
orgMembershipId: membership.id,
userPublicKey: userEncryptionKey.publicKey
}
]
});
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => { const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
const newProjectMembership = await projectMembershipDAL.create( const newProjectMembership = await projectMembershipDAL.create(
{ {
@@ -181,16 +154,6 @@ export const orgAdminServiceFactory = ({
tx tx
); );
await projectKeyDAL.create(
{
encryptedKey: newWsMember.workspaceEncryptedKey,
nonce: newWsMember.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: actorId,
projectId
},
tx
);
return newProjectMembership; return newProjectMembership;
}); });
+2 -144
View File
@@ -8,7 +8,6 @@ import {
OrgMembershipStatus, OrgMembershipStatus,
ProjectMembershipRole, ProjectMembershipRole,
ProjectVersion, ProjectVersion,
SecretKeyEncoding,
TableName, TableName,
TProjectMemberships, TProjectMemberships,
TProjectUserMembershipRolesInsert, TProjectUserMembershipRolesInsert,
@@ -58,8 +57,6 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers, createProjectKey } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
@@ -137,7 +134,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">;
@@ -169,7 +165,6 @@ export const orgServiceFactory = ({
projectRoleDAL, projectRoleDAL,
samlConfigDAL, samlConfigDAL,
oidcConfigDAL, oidcConfigDAL,
projectBotDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityMetadataDAL, identityMetadataDAL,
projectBotService, projectBotService,
@@ -287,15 +282,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
); );
@@ -885,29 +872,10 @@ export const orgServiceFactory = ({
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key // 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
); );
@@ -1069,106 +1037,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,
@@ -1191,16 +1059,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,
@@ -290,6 +294,10 @@ export const projectQueueFactory = ({
continue; continue;
} }
if (!user.publicKey) {
throw new Error(`User with ID ${key.receiverId} has no public key during upgrade.`);
}
const [newMember] = assignWorkspaceKeysToMembers({ const [newMember] = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey, decryptKey: ghostUserLatestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey, userPrivateKey: ghostUser.keys.plainPrivateKey,
+5 -100
View File
@@ -55,13 +55,10 @@ import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microso
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal"; import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal"; import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal"; import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
@@ -78,7 +75,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types"; import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
import { TProjectDALFactory } from "./project-dal"; import { TProjectDALFactory } from "./project-dal";
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns"; import { bootstrapSshProject } from "./project-fns";
import { TProjectQueueFactory } from "./project-queue"; import { TProjectQueueFactory } from "./project-queue";
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal"; import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
import { import {
@@ -123,6 +120,7 @@ export const DEFAULT_PROJECT_ENVS = [
type TProjectServiceFactoryDep = { type TProjectServiceFactoryDep = {
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "create">;
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">; projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">;
projectQueue: TProjectQueueFactory; projectQueue: TProjectQueueFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
@@ -132,9 +130,7 @@ type TProjectServiceFactoryDep = {
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">; secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">; projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
identityOrgMembershipDAL: TIdentityOrgDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: TIdentityProjectDALFactory;
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">; identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick< projectMembershipDAL: Pick<
TProjectMembershipDALFactory, TProjectMembershipDALFactory,
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
@@ -167,12 +163,10 @@ type TProjectServiceFactoryDep = {
sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">; sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">;
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">; sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">; keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">;
kmsService: Pick< kmsService: Pick<
TKmsServiceFactory, TKmsServiceFactory,
@@ -196,27 +190,25 @@ export const projectServiceFactory = ({
secretDAL, secretDAL,
secretV2BridgeDAL, secretV2BridgeDAL,
projectQueue, projectQueue,
projectKeyDAL,
permissionService, permissionService,
projectBotService, projectBotService,
orgDAL, orgDAL,
userDAL, userDAL,
folderDAL, folderDAL,
orgService,
identityProjectDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
projectMembershipDAL, projectMembershipDAL,
projectEnvDAL, projectEnvDAL,
licenseService, licenseService,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
projectRoleDAL, projectRoleDAL,
identityProjectMembershipRoleDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
certificateTemplateDAL, certificateTemplateDAL,
pkiCollectionDAL, pkiCollectionDAL,
pkiAlertDAL, pkiAlertDAL,
pkiSubscriberDAL, pkiSubscriberDAL,
identityProjectDAL,
identityProjectMembershipRoleDAL,
sshCertificateAuthorityDAL, sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL, sshCertificateAuthoritySecretDAL,
sshCertificateDAL, sshCertificateDAL,
@@ -225,7 +217,6 @@ export const projectServiceFactory = ({
sshHostGroupDAL, sshHostGroupDAL,
keyStore, keyStore,
kmsService, kmsService,
projectBotDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
projectMicrosoftTeamsConfigDAL, projectMicrosoftTeamsConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
@@ -253,7 +244,7 @@ export const projectServiceFactory = ({
type = ProjectType.SecretManager type = ProjectType.SecretManager
}: TCreateProjectDTO) => { }: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId }); const organization = await orgDAL.findOne({ id: actorOrgId });
const { permission, membership: orgMembership } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
organization.id, organization.id,
@@ -277,7 +268,6 @@ export const projectServiceFactory = ({
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces." message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
}); });
} }
const ghostUser = await orgService.addGhostUser(organization.id, tx);
if (kmsKeyId) { if (kmsKeyId) {
const kms = await kmsService.getKmsById(kmsKeyId, tx); const kms = await kmsService.getKmsById(kmsKeyId, tx);
@@ -329,19 +319,6 @@ export const projectServiceFactory = ({
}); });
} }
// set ghost user as admin of project
const projectMembership = await projectMembershipDAL.create(
{
userId: ghostUser.user.id,
projectId: project.id
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
tx
);
// set default environments and root folder for provided environments // set default environments and root folder for provided environments
let envs: TProjectEnvironments[] = []; let envs: TProjectEnvironments[] = [];
if (projectTemplate) { if (projectTemplate) {
@@ -374,55 +351,6 @@ export const projectServiceFactory = ({
); );
} }
// 3. Create a random key that we'll use as the project key.
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: ghostUser.keys.publicKey,
privateKey: ghostUser.keys.plainPrivateKey
});
// 4. Save the project key for the ghost user.
await projectKeyDAL.create(
{
projectId: project.id,
receiverId: ghostUser.user.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: ghostUser.user.id
},
tx
);
const { iv, tag, ciphertext, encoding, algorithm } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(ghostUser.keys.plainPrivateKey);
// 5. Create & a bot for the project
await projectBotDAL.create(
{
name: "Infisical Bot (Ghost)",
projectId: project.id,
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: ghostUser.keys.publicKey,
senderId: ghostUser.user.id,
algorithm,
keyEncoding: encoding
},
tx
);
// Find the ghost users latest key
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
if (!latestKey) {
throw new Error("Latest key not found for user");
}
// If the project is being created by a user, add the user to the project as an admin // If the project is being created by a user, add the user to the project as an admin
if (actor === ActorType.USER) { if (actor === ActorType.USER) {
// Find public key of user // Find public key of user
@@ -432,17 +360,6 @@ export const projectServiceFactory = ({
throw new Error("User not found"); throw new Error("User not found");
} }
const [projectAdmin] = assignWorkspaceKeysToMembers({
decryptKey: latestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: orgMembership.id
}
]
});
// Create a membership for the user // Create a membership for the user
const userProjectMembership = await projectMembershipDAL.create( const userProjectMembership = await projectMembershipDAL.create(
{ {
@@ -455,18 +372,6 @@ export const projectServiceFactory = ({
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
tx tx
); );
// Create a project key for the user
await projectKeyDAL.create(
{
encryptedKey: projectAdmin.workspaceEncryptedKey,
nonce: projectAdmin.workspaceEncryptedNonce,
senderId: ghostUser.user.id,
receiverId: user.id,
projectId: project.id
},
tx
);
} }
// If the project is being created by an identity, add the identity to the project as an admin // If the project is being created by an identity, add the identity to the project as an admin
@@ -117,7 +117,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;
@@ -42,7 +42,7 @@ export const reminderServiceFactory = ({
const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise<void> => { const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise<void> => {
if (!newRecipients || newRecipients.length === 0) { if (!newRecipients || newRecipients.length === 0) {
// If no recipients provided, remove all existing recipients // If no recipients provided, remove all existing recipients
await reminderRecipientDAL.deleteById(reminderId); await reminderRecipientDAL.delete({ reminderId });
return; return;
} }
+5 -23
View File
@@ -1,9 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
@@ -175,7 +173,11 @@ export const userServiceFactory = ({
const getMe = async (userId: string) => { const getMe = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId); const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" }); if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" });
return user;
return {
...user,
encryptionVersion: user.encryptionVersion!
};
}; };
const deleteUser = async (userId: string) => { const deleteUser = async (userId: string) => {
@@ -212,25 +214,6 @@ export const userServiceFactory = ({
); );
}; };
const getUserPrivateKey = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user?.serverEncryptedPrivateKey || !user.serverEncryptedPrivateKeyIV || !user.serverEncryptedPrivateKeyTag) {
throw new NotFoundError({ message: `Private key for user with ID '${userId}' not found` });
}
const privateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
ciphertext: user.serverEncryptedPrivateKey,
tag: user.serverEncryptedPrivateKeyTag,
iv: user.serverEncryptedPrivateKeyIV,
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
return privateKey;
};
const getUserProjectFavorites = async (userId: string, orgId: string) => { const getUserProjectFavorites = async (userId: string, orgId: string) => {
const orgMembership = await orgMembershipDAL.findOne({ const orgMembership = await orgMembershipDAL.findOne({
userId, userId,
@@ -311,7 +294,6 @@ export const userServiceFactory = ({
listUserGroups, listUserGroups,
getUserAction, getUserAction,
unlockUser, unlockUser,
getUserPrivateKey,
getAllMyAccounts, getAllMyAccounts,
getUserProjectFavorites, getUserProjectFavorites,
removeMyDuplicateAccounts, removeMyDuplicateAccounts,
@@ -0,0 +1,4 @@
---
title: "Project Events"
openapi: "POST /api/v1/events/subscribe/project-events"
---
+6
View File
@@ -41,6 +41,8 @@
"group": "Platform Reference", "group": "Platform Reference",
"pages": [ "pages": [
"documentation/platform/organization", "documentation/platform/organization",
"documentation/platform/event-subscriptions",
"documentation/platform/folder",
{ {
"group": "Projects", "group": "Projects",
"pages": [ "pages": [
@@ -777,6 +779,10 @@
"group": "Admin", "group": "Admin",
"pages": ["api-reference/endpoints/admin/bootstrap-instance"] "pages": ["api-reference/endpoints/admin/bootstrap-instance"]
}, },
{
"group": "Events",
"pages": ["api-reference/endpoints/events/project-events"]
},
{ {
"group": "Identities", "group": "Identities",
"pages": [ "pages": [
@@ -0,0 +1,118 @@
---
title: "Event Subscriptions"
sidebarTitle: "Events"
description: "Subscribe to events in Infisical for real-time updates"
---
<Info>
**Note:** Event Subscriptions is a paid feature. - **Infisical Cloud users:** Event Subscriptions is available under
the **Enterprise Tier**. - **Self-Hosted Infisical:** Please contact [[email protected]](mailto:[email protected])
to purchase an enterprise license.
</Info>
Event Subscriptions in Infisical allow you to receive real-time notifications when specific actions occur within your account or organization. These notifications include changes to secrets, users, teams, and many more **coming soon**.
## How It Works
- Server receives message over pubsub connection indicating changes have occurred
- Server processes the change notification
- Updated data is synchronized across all connected Infisical instances
- Client applications receive real-time updates through [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
- All servers maintain consistent state without manual intervention
This ensures your infrastructure stays up-to-date automatically, without requiring restarts or manual synchronization.
<Note>
Event Subscriptions are designed for real-time communication and do not include persistence or replay
capabilities—events are delivered once and are not stored for future retrieval.
</Note>
## Supported Resources
You can currently subscribe to notifications for the following resources and event types:
- **Secrets**
- `secret:created`: Triggered when a secret is created
- `secret:updated`: Triggered when a secret is updated
- `secret:deleted`: Triggered when a secret is deleted
## Permissions Setup
To receive events on a supported resource, the identity must have `Subscribe` action permission on that resource.
Follow these steps to set up the necessary permissions:
<Steps>
<Step title="Select a project and copy the Project ID">
![Select Project](/images/platform/events/select-project.png)
On your project page, open **Project Settings** from the sidebar.
In the Project name section, click **Copy Project ID** to copy your Project ID, or extract it from the URL:
`https://app.infisical.com/project/<your_project_id>/settings`
</Step>
<Step title="Navigate to Access Management and open Project Roles">
![Project Detail](/images/platform/events/project-detail.png) ![Project
Access](/images/platform/events/project-access.png) Navigate to **Access Management**, then select **Project Roles**.
</Step>
<Step title="Select an existing role or create a new one">
![Project Role](/images/platform/events/project-role.png) You can either edit an existing role or create a new role
for event subscriptions.
</Step>
<Step title="Assign policies to the role">
![Role Detail](/images/platform/events/role-detail.png) Select the specific resources that the role should have access
to. ![Add policy](/images/platform/events/add-policy.png)
</Step>
<Step title="Enable the Subscribe action in permissions">
![Policy setting](/images/platform/events/policy-setting.png)
Ensure the **Subscribe** action is selected for the relevant resources and events.
## Conditions
By default, the role will have access to all events for the selected resources in this project.
<AccordionGroup>
<Accordion title="Full Access">
![Policy setting](/images/platform/events/access-full.png)
</Accordion>
<Accordion title="Path Prefix">
![Policy setting](/images/platform/events/access-path.png)
</Accordion>
<Accordion title="Environment">
![Policy setting](/images/platform/events/access-dev.png)
</Accordion>
</AccordionGroup>
</Step>
</Steps>
## Getting Started
Currently, events are only available via [API](/api-reference/endpoints/events) but will soon be available in our SDKs, Kubernetes Operator, and more.
### API Usage
You need an auth token to use this API. To get an authentication token, follow the authentication guide for one of our supported auth methods from the [machine identities documentation](/documentation/platform/identities/machine-identities#authentication-methods).
#### Creating a Subscription
![Postman Subscription](/images/platform/events/postman-subscribe.png)
**Request Parameters:**
- `projectId`: Project whose events you want to subscribe to
- `register`: List of event filters
- `conditions`: Conditions to filter events on
- `environmentSlug`: Project environment
- `secretPath`: Path of the secrets
![Postman Subscription Response](/images/platform/events/postman-sse-response.png)
The subscribe endpoint responds with a `text/event-stream` content type to initiate SSE streaming.
For more specific details, please refer to our [API Reference](/api-reference/endpoints/events).
@@ -6,6 +6,7 @@ description: "Learn how to automatically rotate Azure Client Secrets."
## Prerequisites ## Prerequisites
- Create an [Azure Client Secret Connection](/integrations/app-connections/azure-client-secrets). - Create an [Azure Client Secret Connection](/integrations/app-connections/azure-client-secrets).
- Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
## Create an Azure Client Secret Rotation in Infisical ## Create an Azure Client Secret Rotation in Infisical
@@ -14,6 +14,7 @@ description: "Learn how to automatically rotate LDAP passwords."
## Prerequisites ## Prerequisites
- Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements - Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements
- Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
## Create an LDAP Password Rotation in Infisical ## Create an LDAP Password Rotation in Infisical
@@ -30,6 +30,7 @@ An example creation statement might look like:
To learn more about Microsoft SQL Server's permission system, please visit their [documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver16). To learn more about Microsoft SQL Server's permission system, please visit their [documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver16).
</Tip> </Tip>
3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
## Create a Microsoft SQL Server Credentials Rotation in Infisical ## Create a Microsoft SQL Server Credentials Rotation in Infisical
@@ -25,7 +25,7 @@ description: "Learn how to automatically rotate MySQL credentials."
<Tip> <Tip>
To learn more about the MySQL permission system, please visit their [documentation](https://dev.mysql.com/doc/refman/8.4/en/grant.html). To learn more about the MySQL permission system, please visit their [documentation](https://dev.mysql.com/doc/refman/8.4/en/grant.html).
</Tip> </Tip>
3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
## Create a MySQL Credentials Rotation in Infisical ## Create a MySQL Credentials Rotation in Infisical
@@ -31,6 +31,7 @@ description: "Learn how to automatically rotate Oracle Database credentials."
To learn more about the Oracle Database permission system, please visit their [documentation](https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-privilege-and-role-authorization.html). To learn more about the Oracle Database permission system, please visit their [documentation](https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-privilege-and-role-authorization.html).
</Tip> </Tip>
3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
## Create an Oracle Database Credentials Rotation in Infisical ## Create an Oracle Database Credentials Rotation in Infisical
@@ -27,6 +27,7 @@ description: "Learn how to automatically rotate PostgreSQL credentials."
To learn more about PostgreSQL's permission system, please visit their [documentation](https://www.postgresql.org/docs/current/sql-grant.html). To learn more about PostgreSQL's permission system, please visit their [documentation](https://www.postgresql.org/docs/current/sql-grant.html).
</Tip> </Tip>
3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
## Create a PostgreSQL Credentials Rotation in Infisical ## Create a PostgreSQL Credentials Rotation in Infisical
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

@@ -7,6 +7,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <Tab title="Infisical UI">
@@ -7,6 +7,7 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <Tab title="Infisical UI">
@@ -7,6 +7,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration) - Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration)
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
<Note> <Note>
The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal
@@ -7,6 +7,7 @@ description: "Learn how to configure a Azure DevOps Sync for Infisical."
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create an [Azure DevOps Connection](/integrations/app-connections/azure-devops) - Create an [Azure DevOps Connection](/integrations/app-connections/azure-devops)
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <Tab title="Infisical UI">
@@ -7,6 +7,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical."
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault) - Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault)
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
<Note> <Note>
The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal
@@ -11,6 +11,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png) ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png)
![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png) ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png)
![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-service-usage-api.png) ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-service-usage-api.png)
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <Tab title="Infisical UI">
@@ -7,6 +7,7 @@ description: "Learn how to configure a GitHub Sync for Infisical."
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create a [GitHub Connection](/integrations/app-connections/github) - Create a [GitHub Connection](/integrations/app-connections/github)
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <Tab title="Infisical UI">
@@ -7,6 +7,7 @@ description: "Learn how to configure a GitLab Sync for Infisical."
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create a [GitLab Connection](/integrations/app-connections/gitlab) - Create a [GitLab Connection](/integrations/app-connections/gitlab)
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <Tab title="Infisical UI">
@@ -14,6 +14,7 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f
- Create an [OCI Connection](/integrations/app-connections/oci) with the required **Secret Sync** permissions - Create an [OCI Connection](/integrations/app-connections/oci) with the required **Secret Sync** permissions
- [Create](https://docs.oracle.com/en-us/iaas/Content/Identity/compartments/To_create_a_compartment.htm) or use an existing OCI Compartment (which the OCI Connection is authorized to access) - [Create](https://docs.oracle.com/en-us/iaas/Content/Identity/compartments/To_create_a_compartment.htm) or use an existing OCI Compartment (which the OCI Connection is authorized to access)
- [Create](https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/managingvaults_topic-To_create_a_new_vault.htm#createnewvault) or use an existing OCI Vault - [Create](https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/managingvaults_topic-To_create_a_new_vault.htm#createnewvault) or use an existing OCI Vault
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <Tab title="Infisical UI">
+32 -111
View File
@@ -1,12 +1,8 @@
import crypto from "crypto";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import jsrp from "jsrp";
import { useServerConfig } from "@app/context";
import { initProjectHelper } from "@app/helpers/project"; import { initProjectHelper } from "@app/helpers/project";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
@@ -14,15 +10,9 @@ import { onRequestError } from "@app/hooks/api/reactQuery";
import InputField from "../basic/InputField"; import InputField from "../basic/InputField";
import checkPassword from "../utilities/checks/password/checkPassword"; import checkPassword from "../utilities/checks/password/checkPassword";
import Aes256Gcm from "../utilities/cryptography/aes-256-gcm";
import { deriveArgonKey, generateKeyPair } from "../utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage";
import SecurityClient from "../utilities/SecurityClient"; import SecurityClient from "../utilities/SecurityClient";
import { Button, Input } from "../v2"; import { Button, Input } from "../v2";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface UserInfoStepProps { interface UserInfoStepProps {
incrementStep: () => void; incrementStep: () => void;
email: string; email: string;
@@ -76,7 +66,6 @@ export default function UserInfoStep({
}: UserInfoStepProps): JSX.Element { }: UserInfoStepProps): JSX.Element {
const [nameError, setNameError] = useState(false); const [nameError, setNameError] = useState(false);
const [organizationNameError, setOrganizationNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false);
const { config } = useServerConfig();
const [errors, setErrors] = useState<Errors>({}); const [errors, setErrors] = useState<Errors>({});
@@ -108,109 +97,41 @@ export default function UserInfoStep({
}); });
if (!errorCheck) { if (!errorCheck) {
// Generate a random pair of a public and a private key try {
const pair = await generateKeyPair(config.fipsEnabled); const response = await completeAccountSignup({
email,
password,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
providerAuthToken,
organizationName,
attributionSource
});
localStorage.setItem("PRIVATE_KEY", pair.privateKey); // unset signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken("");
client.init( if (response.organizationId) {
{ await selectOrganization({ organizationId: response.organizationId });
username: email,
password
},
async () => {
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
try {
// TODO: moduralize into KeyService
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: pair.privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
const response = await completeAccountSignup({
email,
password,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey: pair.publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
providerAuthToken,
salt: result.salt,
verifier: result.verifier,
organizationName,
attributionSource
});
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken("");
if (response.organizationId) {
await selectOrganization({ organizationId: response.organizationId });
}
saveTokenToLocalStorage({
publicKey: pair.publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey: pair.privateKey
});
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]?.id;
await initProjectHelper({
projectName: "Example Project"
});
localStorage.setItem("orgData.id", orgId);
incrementStep();
} catch (error) {
onRequestError(error);
setIsLoading(false);
console.error(error);
}
});
} }
);
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: "info"
});
return null;
}
}
throw err;
});
if (data === null) {
return attemptLogin({
email,
password,
providerAuthToken,
captchaToken,
loginMode: LoginMode.LegacySrp
});
}
SecurityClient.setProviderAuthToken("");
SecurityClient.setToken(data.accessToken);
return {
success: true,
loginResponse: {
email,
privateKey: "",
JTWToken: data.accessToken
}
};
}
return new Promise((resolve, reject) => { 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: "info"
});
return null;
}
}
throw err;
});
if (data === null) {
return attemptLogin({
email,
password,
providerAuthToken,
captchaToken,
loginMode: LoginMode.LegacySrp
});
}
SecurityClient.setProviderAuthToken("");
SecurityClient.setToken(data.accessToken);
if (email) {
telemetry.identify(email, email);
telemetry.capture("User Logged In");
}
return {
success: true
};
}
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
await new Promise((resolve) => { await new Promise((resolve) => {
@@ -48,18 +100,7 @@ const attemptLogin = async ({
client.setServerPublicKey(serverPublicKey); client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1 const clientProof = client.getProof(); // called M1
const { const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await login2({
captchaToken, captchaToken,
email, email,
password, password,
@@ -67,56 +108,22 @@ const attemptLogin = async ({
providerAuthToken providerAuthToken
}); });
if (mfaEnabled) { if (encryptionVersion && encryptedPrivateKey && iv && tag && token) {
// case: MFA is enabled
// set temporary (MFA) JWT token
SecurityClient.setMfaToken(token);
return {
mfaEnabled,
success: true
};
}
if (!mfaEnabled && encryptionVersion && encryptedPrivateKey && iv && tag && token) {
// case: MFA is not enabled
// unset provider auth token in case it was used // unset provider auth token in case it was used
SecurityClient.setProviderAuthToken(""); SecurityClient.setProviderAuthToken("");
// set JWT token // set JWT token
SecurityClient.setToken(token); SecurityClient.setToken(token);
const privateKey = await decryptPrivateKeyHelper({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
if (email) { if (email) {
telemetry.identify(email, email); telemetry.identify(email, email);
telemetry.capture("User Logged In"); telemetry.capture("User Logged In");
} }
return { return {
mfaEnabled: false,
success: true success: true
}; };
} }
return { success: false, mfaEnabled: false }; return { success: false };
}; };
export default attemptLogin; export default attemptLogin;
@@ -1,95 +0,0 @@
/* eslint-disable prefer-destructuring */
import jsrp from "jsrp";
import { decryptPrivateKeyHelper } from "@app/helpers/key";
import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
import SecurityClient from "./SecurityClient";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
/**
* Return whether or not MFA-login is successful for user with email [email]
* and MFA token [mfaToken]
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {String} obj.mfaToken - MFA code/token
*/
const attemptLoginMfa = async ({
email,
password,
providerAuthToken,
mfaToken
}: {
email: string;
password: string;
providerAuthToken?: string;
mfaToken: string;
}): Promise<boolean> => {
return new Promise((resolve, reject) => {
client.init(
{
username: email,
password
},
async () => {
try {
const clientPublicKey = client.getPublicKey();
const { salt } = await login1({
email,
clientPublicKey,
providerAuthToken
});
const {
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await verifyMfaToken({
email,
mfaCode: mfaToken
});
// unset temporary (MFA) JWT token and set JWT token
SecurityClient.setMfaToken("");
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
const privateKey = await decryptPrivateKeyHelper({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
resolve(true);
} catch (err) {
reject(err);
}
}
);
});
};
export default attemptLoginMfa;
@@ -1,114 +0,0 @@
/* eslint-disable new-cap */
import crypto from "crypto";
import jsrp from "jsrp";
import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries";
import generateBackupPDF from "../generateBackupPDF";
import Aes256Gcm from "./aes-256-gcm";
const clientPassword = new jsrp.client();
const clientKey = new jsrp.client();
interface BackupKeyProps {
email: string;
password: string;
personalName: string;
setBackupKeyError: (value: boolean) => void;
setBackupKeyIssued: (value: boolean) => void;
}
/**
* This function issue a backup key for a user
* @param {obkect} obj
* @param {string} obj.email - email of a user issuing a backup key
* @param {string} obj.password - password of a user issuing a backup key
* @param {string} obj.personalName - name of a user issuing a backup key
* @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key
* @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly
* @returns
*/
const issueBackupKey = async ({
email,
password,
personalName,
setBackupKeyError,
setBackupKeyIssued
}: BackupKeyProps) => {
try {
setBackupKeyError(false);
setBackupKeyIssued(false);
clientPassword.init(
{
username: email,
password
},
async () => {
const clientPublicKey = clientPassword.getPublicKey();
let serverPublicKey;
let salt;
try {
const res = await srp1({
clientPublicKey
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
} catch (err) {
setBackupKeyError(true);
console.log("Wrong current password", err, 1);
}
clientPassword.setSalt(salt as string);
clientPassword.setServerPublicKey(serverPublicKey as string);
const clientProof = clientPassword.getProof(); // called M1
const generatedKey = crypto.randomBytes(16).toString("hex");
clientKey.init(
{
username: email,
password: generatedKey
},
async () => {
clientKey.createVerifier(
async (_err: any, result: { salt: string; verifier: string }) => {
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: String(localStorage.getItem("PRIVATE_KEY")),
secret: generatedKey
});
try {
await issueBackupPrivateKey({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
clientProof
});
generateBackupPDF({
personalName,
personalEmail: email,
generatedKey
});
setBackupKeyIssued(true);
} catch {
setBackupKeyError(true);
}
}
);
}
);
}
);
} catch {
setBackupKeyError(true);
console.log("Failed to issue a backup key");
}
return true;
};
export default issueBackupKey;
@@ -1,67 +0,0 @@
interface Props {
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
privateKey?: string;
}
export const saveTokenToLocalStorage = ({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
}: Props) => {
try {
if (protectedKey) {
localStorage.removeItem("protectedKey");
localStorage.setItem("protectedKey", protectedKey);
}
if (protectedKeyIV) {
localStorage.removeItem("protectedKeyIV");
localStorage.setItem("protectedKeyIV", protectedKeyIV);
}
if (protectedKeyTag) {
localStorage.removeItem("protectedKeyTag");
localStorage.setItem("protectedKeyTag", protectedKeyTag);
}
if (publicKey) {
localStorage.removeItem("publicKey");
localStorage.setItem("publicKey", publicKey);
}
if (encryptedPrivateKey) {
localStorage.removeItem("encryptedPrivateKey");
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
}
if (iv) {
localStorage.removeItem("iv");
localStorage.setItem("iv", iv);
}
if (tag) {
localStorage.removeItem("tag");
localStorage.setItem("tag", tag);
}
if (privateKey) {
localStorage.removeItem("PRIVATE_KEY");
localStorage.setItem("PRIVATE_KEY", privateKey);
}
} catch (err) {
if (err instanceof Error) {
throw new Error(`Unable to send the tokens in local storage:${err.message}`);
}
}
};
+7 -48
View File
@@ -8,26 +8,24 @@ import { organizationKeys } from "../organization/queries";
import { setAuthToken } from "../reactQuery"; import { setAuthToken } from "../reactQuery";
import { workspaceKeys } from "../workspace"; import { workspaceKeys } from "../workspace";
import { import {
ChangePasswordDTO,
CompleteAccountDTO, CompleteAccountDTO,
CompleteAccountSignupDTO, CompleteAccountSignupDTO,
GetAuthTokenAPI, GetAuthTokenAPI,
GetBackupEncryptedPrivateKeyDTO, GetBackupEncryptedPrivateKeyDTO,
IssueBackupPrivateKeyDTO,
Login1DTO, Login1DTO,
Login1Res, Login1Res,
Login2DTO, Login2DTO,
Login2Res, Login2Res,
LoginLDAPDTO, LoginLDAPDTO,
LoginLDAPRes, LoginLDAPRes,
LoginV3DTO,
LoginV3Res,
MfaMethod, MfaMethod,
ResetPasswordDTO, ResetPasswordDTO,
ResetPasswordV2DTO, ResetPasswordV2DTO,
ResetUserPasswordV2DTO, ResetUserPasswordV2DTO,
SendMfaTokenDTO, SendMfaTokenDTO,
SetupPasswordDTO, SetupPasswordDTO,
SRP1DTO,
SRPR1Res,
TOauthTokenExchangeDTO, TOauthTokenExchangeDTO,
UserAgentType, UserAgentType,
UserEncryptionVersion, UserEncryptionVersion,
@@ -50,21 +48,14 @@ export const login2 = async (loginDetails: Login2DTO) => {
return data; return data;
}; };
export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => { export const loginV3 = async (loginDetails: LoginV3DTO) => {
const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token const { data } = await apiRequest.post<LoginV3Res>("/api/v3/auth/login", loginDetails);
return data; return data;
}; };
export const useLogin1 = () => { export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
return useMutation({ const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token
mutationFn: async (details: { return data;
email: string;
clientPublicKey: string;
providerAuthToken?: string;
}) => {
return login1(details);
}
});
}; };
export const selectOrganization = async (data: { export const selectOrganization = async (data: {
@@ -143,11 +134,6 @@ export const useOauthTokenExchange = () => {
}); });
}; };
export const srp1 = async (details: SRP1DTO) => {
const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details);
return data;
};
export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => { export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => {
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details); const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details);
return data; return data;
@@ -158,14 +144,6 @@ export const completeAccountSignupInvite = async (details: CompleteAccountDTO) =
return data; return data;
}; };
export const useCompleteAccountSignup = () => {
return useMutation({
mutationFn: async (details: CompleteAccountSignupDTO) => {
return completeAccountSignup(details);
}
});
};
export const useSendMfaToken = () => { export const useSendMfaToken = () => {
return useMutation<object, object, SendMfaTokenDTO>({ return useMutation<object, object, SendMfaTokenDTO>({
mutationFn: async ({ email }) => { mutationFn: async ({ email }) => {
@@ -263,11 +241,6 @@ export const useVerifyPasswordResetCode = () => {
}); });
}; };
export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => {
const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details);
return data;
};
export const getBackupEncryptedPrivateKey = async ({ export const getBackupEncryptedPrivateKey = async ({
verificationToken verificationToken
}: GetBackupEncryptedPrivateKeyDTO) => { }: GetBackupEncryptedPrivateKeyDTO) => {
@@ -328,20 +301,6 @@ export const useResetUserPasswordV2 = () => {
}); });
}; };
export const changePassword = async (details: ChangePasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/change-password", details);
return data;
};
export const useChangePassword = () => {
// note: use after srp1
return useMutation({
mutationFn: async (details: ChangePasswordDTO) => {
return changePassword(details);
}
});
};
// Refresh token is set as cookie when logged in // Refresh token is set as cookie when logged in
// Using that we fetch the auth bearer token needed for auth calls // Using that we fetch the auth bearer token needed for auth calls
export const fetchAuthToken = async () => { export const fetchAuthToken = async () => {

Some files were not shown because too many files have changed in this diff Show More