Skip to content
18 changes: 18 additions & 0 deletions backend/src/db/migrations/20250723220500_remove-srp.ts
Original file line number Diff line number Diff line change
@@ -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();
Comment thread
varonix0 marked this conversation as resolved.
});
}

export async function down(): Promise<void> {
// do nothing for now to avoid breaking down migrations
}
12 changes: 6 additions & 6 deletions backend/src/db/schemas/user-encryption-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ export const UserEncryptionKeysSchema = z.object({
protectedKey: z.string().nullable().optional(),
protectedKeyIV: z.string().nullable().optional(),
protectedKeyTag: z.string().nullable().optional(),
publicKey: z.string(),
encryptedPrivateKey: z.string(),
iv: z.string(),
tag: z.string(),
salt: z.string(),
verifier: z.string(),
publicKey: z.string().nullable().optional(),
encryptedPrivateKey: z.string().nullable().optional(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
salt: z.string().nullable().optional(),
verifier: z.string().nullable().optional(),
userId: z.string().uuid(),
hashedPassword: z.string().nullable().optional(),
serverEncryptedPrivateKey: z.string().nullable().optional(),
Expand Down
4 changes: 4 additions & 0 deletions backend/src/db/seed-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ export const generateUserSrpKeys = async (password: string) => {
};

export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
throw new Error("User encrypted private key not found");
}

const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,
Expand Down
7 changes: 4 additions & 3 deletions backend/src/db/seeds/1-user.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Knex } from "knex";

import { crypto } from "@app/lib/crypto";
import { initLogger } from "@app/lib/logger";
import { initEnvConfig } from "@app/lib/config/env";
import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";

import { AuthMethod } from "../../services/auth/auth-type";
Expand All @@ -17,14 +17,15 @@ export async function seed(knex: Knex): Promise<void> {
initLogger();

const superAdminDAL = superAdminDALFactory(knex);
await crypto.initialize(superAdminDAL);
await initEnvConfig(superAdminDAL, logger);

await knex(TableName.SuperAdmin).insert([
// eslint-disable-next-line
// @ts-ignore
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
]);
// Inserts seed entries

const [user] = await knex(TableName.Users)
.insert([
{
Expand Down
199 changes: 180 additions & 19 deletions backend/src/db/seeds/3-project.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,188 @@
import { Knex } from "knex";

import { initEnvConfig } from "@app/lib/config/env";
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { initLogger, logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AuthMethod } from "@app/services/auth/auth-type";
import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns";
import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { userDALFactory } from "@app/services/user/user-dal";

import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
import {
OrgMembershipRole,
OrgMembershipStatus,
ProjectMembershipRole,
ProjectType,
SecretEncryptionAlgo,
SecretKeyEncoding,
TableName
} from "../schemas";
import { seedData1 } from "../seed-data";

export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
{ name: "Staging", slug: "staging" },
{ 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> {
// Deletes ALL existing entries
await knex(TableName.Project).del();
await knex(TableName.Environment).del();
await knex(TableName.SecretFolder).del();

initLogger();

const superAdminDAL = superAdminDALFactory(knex);
await initEnvConfig(superAdminDAL, logger);

const [project] = await knex(TableName.Project)
.insert({
name: seedData1.project.name,
Expand All @@ -29,29 +195,24 @@ export async function seed(knex: Knex): Promise<void> {
})
.returning("*");

const projectMembership = await knex(TableName.ProjectMembership)
.insert({
projectId: project.id,
const userOrgMembership = await knex(TableName.OrgMembership)
.where({
orgId: seedData1.organization.id,
userId: seedData1.id
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectMembershipId: projectMembership[0].id
});
.first();

if (!userOrgMembership) {
throw new Error("User org membership not found");
}
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
if (!user) throw new Error("User not found");

const userPrivateKey = await getUserPrivateKey(seedData1.password, user);
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey);
await knex(TableName.ProjectKeys).insert({
projectId: project.id,
nonce: projectKey.nonce,
encryptedKey: projectKey.ciphertext,
receiverId: seedData1.id,
senderId: seedData1.id
});
if (!user.publicKey) {
throw new Error("User public key not found");
}

await createUserWithGhostUser(seedData1.organization.id, project.id, seedData1.id, userOrgMembership.id, knex);

// create default environments and default folders
const envs = await knex(TableName.Environment)
Expand Down
8 changes: 8 additions & 0 deletions backend/src/db/seeds/5-machine-identity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Knex } from "knex";

import { initEnvConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";

import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
Expand All @@ -10,6 +13,11 @@ export async function seed(knex: Knex): Promise<void> {
await knex(TableName.Identity).del();
await knex(TableName.IdentityOrgMembership).del();

initLogger();

const superAdminDAL = superAdminDALFactory(knex);
await initEnvConfig(superAdminDAL, logger);

// Inserts seed entries
await knex(TableName.Identity).insert([
{
Expand Down
26 changes: 25 additions & 1 deletion backend/src/ee/services/group/group-fns.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Knex } from "knex";

import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";

Expand Down Expand Up @@ -65,6 +65,18 @@ const addAcceptedUsersToGroup = async ({
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));

for await (const projectId of projectIds) {
const project = await projectDAL.findById(projectId, tx);
if (!project) {
throw new NotFoundError({
message: `Failed to find project with ID '${projectId}'`
});
}

if (project.version !== ProjectVersion.V1 && project.version !== ProjectVersion.V2) {
// eslint-disable-next-line no-continue
continue;
}

const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));

if (usersToAddProjectKeyFor.length) {
Expand All @@ -86,6 +98,12 @@ const addAcceptedUsersToGroup = async ({
});
}

if (!ghostUserLatestKey.sender.publicKey) {
throw new NotFoundError({
message: `Failed to find project owner's public key in project with ID '${projectId}'`
});
}

const bot = await projectBotDAL.findOne({ projectId }, tx);

if (!bot) {
Expand All @@ -112,6 +130,12 @@ const addAcceptedUsersToGroup = async ({
});

const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
if (!user.publicKey) {
throw new NotFoundError({
message: `Failed to find user's public key in project with ID '${projectId}'`
});
}

const { ciphertext: encryptedKey, nonce } = crypto
.encryption()
.asymmetric()
Expand Down
2 changes: 1 addition & 1 deletion backend/src/ee/services/group/group-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = {
TUserGroupMembershipDALFactory,
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
>;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
Expand Down
Loading
Loading