feat(infisical-pg): completed test for secret folder and import

This commit is contained in:
Akhil Mohan
2024-01-27 12:39:54 +05:30
parent e02c082c7a
commit e4f32f092a
11 changed files with 646 additions and 99 deletions
@@ -22,7 +22,7 @@ export async function up(knex: Knex): Promise<void> {
t.foreign("statusChangeBy")
.references("id")
.inTable(TableName.ProjectMembership)
.onDelete("CASCADE");
.onDelete("SET NULL");
t.uuid("committerId").notNullable();
t.foreign("committerId")
.references("id")
+125 -1
View File
@@ -1,3 +1,19 @@
import crypto from "node:crypto";
import argon2, { argon2id } from "argon2";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import {
decryptAsymmetric,
decryptSymmetric,
encryptAsymmetric,
encryptSymmetric
} from "@app/lib/crypto";
import { TUserEncryptionKeys } from "./schemas";
export const seedData1 = {
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
email: "[email protected]",
@@ -8,9 +24,117 @@ export const seedData1 = {
},
project: {
id: "77fa7aed-9288-401e-a4c9-3a9430be62a0",
name: "first project"
name: "first project",
slug: "first-project"
},
environment: {
name: "Development",
slug: "dev"
},
token: {
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
}
};
export const generateUserSrpKeys = async (password: string) => {
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
// eslint-disable-next-line
const client = new jsrp.client();
await new Promise((resolve) => {
client.init({ username: seedData1.email, password: seedData1.password }, () => resolve(null));
});
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>(
(resolve, reject) => {
client.createVerifier((err, res) => {
if (err) return reject(err);
return resolve(res);
});
}
);
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(salt),
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
hashLength: 32,
type: argon2id,
raw: true
});
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
} = encryptSymmetric(privateKey, key.toString("base64"));
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64"));
return {
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
};
};
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
hashLength: 32,
type: argon2id,
raw: true
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = decryptSymmetric({
ciphertext: user.protectedKey,
iv: user.protectedKeyIV,
tag: user.protectedKeyTag,
key: derivedKey.toString("base64")
});
const privateKey = decryptSymmetric({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
key
});
return privateKey;
};
export const buildUserProjectKey = async (privateKey: string, publickey: string) => {
const randomBytes = crypto.randomBytes(16).toString("hex");
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
return { nonce, ciphertext };
};
export const getUserProjectKey = async (privateKey: string) => {
const key = decryptAsymmetric({
ciphertext: decryptFileKey.encryptedKey,
nonce: decryptFileKey.nonce,
publicKey: decryptFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
};
+1 -71
View File
@@ -1,78 +1,8 @@
import crypto from "node:crypto";
import argon2, { argon2id } from "argon2";
import jsrp from "jsrp";
import { Knex } from "knex";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import { encryptSymmetric } from "@app/lib/crypto";
import { AuthMethod } from "../../services/auth/auth-type";
import { TableName } from "../schemas";
import { seedData1 } from "../seed-data";
export const generateUserSrpKeys = async (password: string) => {
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
// eslint-disable-next-line
const client = new jsrp.client();
await new Promise((resolve) => {
client.init({ username: seedData1.email, password: seedData1.password }, () => resolve(null));
});
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>(
(resolve, reject) => {
client.createVerifier((err, res) => {
if (err) return reject(err);
return resolve(res);
});
}
);
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(salt),
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
hashLength: 32,
type: argon2id,
raw: true
});
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
} = encryptSymmetric(privateKey, key.toString("base64"));
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64"));
return {
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
};
};
import { generateUserSrpKeys, seedData1 } from "../seed-data";
export async function seed(knex: Knex): Promise<void> {
// Deletes ALL existing entries
+1
View File
@@ -17,6 +17,7 @@ export async function seed(knex: Knex): Promise<void> {
// @ts-ignore because we need that id for api calls
id: seedData1.organization.id,
name: "infisical",
slug: "infisical",
customerId: null
}
])
+13 -1
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { OrgMembershipRole, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
@@ -19,10 +19,22 @@ export async function seed(knex: Knex): Promise<void> {
.insert({
name: seedData1.project.name,
orgId: seedData1.organization.id,
slug: "first-project",
// @ts-ignore pre calc id
id: seedData1.project.id
})
.returning("*");
await knex(TableName.ProjectKeys).insert({
projectId: project.id,
senderId: seedData1.id
});
await knex(TableName.ProjectMembership).insert({
projectId: project.id,
role: OrgMembershipRole.Admin,
userId: seedData1.id
});
const envs = await knex(TableName.Environment)
.insert(
DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({
@@ -149,7 +149,7 @@ export const permissionServiceFactory = ({
// user permission for a project in an organization
const getUserProjectPermission = async (userId: string, projectId: string) => {
const membership = await permissionDAL.getProjectPermission(userId, projectId);
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
if (!membership) throw new UnauthorizedError({ name: "User not in project" });
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" });
}
@@ -161,7 +161,7 @@ export const permissionServiceFactory = ({
const getIdentityProjectPermission = async (identityId: string, projectId: string) => {
const membership = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
if (!membership) throw new UnauthorizedError({ name: "Identity not in org" });
if (!membership) throw new UnauthorizedError({ name: "Identity not in project" });
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" });
}
@@ -208,7 +208,7 @@ export const permissionServiceFactory = ({
default:
throw new UnauthorizedError({
message: "Permission not defined",
name: "Get org permission"
name: "Get project permission"
});
}
};
@@ -84,22 +84,17 @@ export const secretFolderServiceFactory = ({
.filter(Boolean);
if (missingSegment.length) {
const newFolders: Array<TSecretFoldersInsert & { id: string }> = missingSegment.map(
(segment, i) =>
i === 0
? {
name: segment,
parentId: parentFolder.id,
id: uuidv4(),
envId: env.id,
version: 1
}
: {
name: segment,
parentId: newFolders[i - 1].id,
id: uuidv4(),
envId: env.id,
version: 1
}
(segment) => {
const newFolder = {
name: segment,
parentId: parentFolderId,
id: uuidv4(),
envId: env.id,
version: 1
};
parentFolderId = newFolder.id;
return newFolder;
}
);
parentFolderId = newFolders.at(-1)?.id as string;
const docs = await folderDAL.insertMany(newFolders, tx);
@@ -156,12 +151,12 @@ export const secretFolderServiceFactory = ({
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env)
throw new BadRequestError({ message: "Environment not found", name: "Update folder" });
let folder = await folderDAL.findOne({ envId: env.id, id, parentId: parentFolder.id });
// now folder api accepts id based change
// this is for cli and when cli removes this will remove this logic
if (!folder) {
folder = await folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id });
}
const folder = await folderDAL
.findOne({ envId: env.id, id, parentId: parentFolder.id })
// now folder api accepts id based change
// this is for cli backward compatiability and when cli removes this, we will remove this logic
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const newFolder = await folderDAL.transaction(async (tx) => {