Skip to content

Commit

Permalink
working encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
psiddharthdesign committed Jul 24, 2024
1 parent 1f015f4 commit d732e62
Show file tree
Hide file tree
Showing 12 changed files with 448 additions and 365 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,48 +1,23 @@
'use client'

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
// Adjust the import path as needed
import { upsertTFVarsByProjectId } from "@/data/user/runs";
import { Json } from '@/lib/database.types'; // Import the Json type from your database types
import { EnvVar } from "@/types/userTypes";
import { motion } from "framer-motion";
import { useState } from "react";
import TFVarTable from "./TFVarTable";

type TFVarsDetailsProps = {
tfvarsdata: {
id: string;
project_id: string;
tfvars: Json;
tfvars: EnvVar[];
updated_at: string;
} | null;
projectId: string;
}

function parseTFVars(tfvars: Json): string {
if (typeof tfvars === 'string') {
return tfvars;
} else if (tfvars === null) {
return '[]';
} else {
return JSON.stringify(tfvars);
}
}

export default function TFVarsDetails({ tfvarsdata, projectId }: TFVarsDetailsProps) {
const [tfvars, setTfvars] = useState<string>(
tfvarsdata ? parseTFVars(tfvarsdata.tfvars) : '[]'
);

const handleUpdate = async (updatedTFVarsJSON: string): Promise<void> => {
try {
await upsertTFVarsByProjectId(projectId, { tfvars: updatedTFVarsJSON });
setTfvars(updatedTFVarsJSON);
} catch (error) {
console.error("Failed to update TF variables:", error);
throw error;
}
};
onUpdate: (name: string, value: string, isSecret: boolean) => Promise<EnvVar[]>;
onDelete: (name: string) => Promise<EnvVar[]>;
onBulkUpdate: (vars: EnvVar[]) => Promise<EnvVar[]>;
}

export default function TFVarsDetails({ tfvarsdata, onUpdate, onDelete, onBulkUpdate }: TFVarsDetailsProps) {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
Expand All @@ -51,24 +26,17 @@ export default function TFVarsDetails({ tfvarsdata, projectId }: TFVarsDetailsPr
transition={{ duration: 0.1 }}
>
<Card className="w-full">
<motion.div
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.15, delay: 0.1 }}
>
<CardHeader>
<CardTitle>Terraform Variables</CardTitle>
<CardDescription>Manage Terraform variables for project</CardDescription>
</CardHeader>
</motion.div>
<CardHeader>
<CardTitle>Terraform Variables</CardTitle>
<CardDescription>Manage Terraform variables for project</CardDescription>
</CardHeader>
<CardContent>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.15, delay: 0.2 }}
>
<TFVarTable initialVariables={tfvars} onUpdate={handleUpdate} />
</motion.div>
<TFVarTable
envVars={tfvarsdata.tfvars}
onUpdate={onUpdate}
onDelete={onDelete}
onBulkUpdate={onBulkUpdate}
/>
</CardContent>
</Card>
</motion.div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { deleteEnvVar, getAllEnvVars, storeEncryptedEnvVar } from "@/data/admin/encryption";
import { getSlimProjectBySlug } from "@/data/user/projects";
import { getTFVarsByProjectId } from "@/data/user/runs";
import { EnvVar } from "@/types/userTypes";
import { projectSlugParamSchema } from "@/utils/zod-schemas/params";
import type { Metadata } from "next";
import TFVarsDetails from '../TFVarsDetails';

type TFVarsPageProps = {
params: {
projectSlug: string;
};
};

export async function generateMetadata({
params,
}: TFVarsPageProps): Promise<Metadata> {
}: { params: { projectSlug: string } }): Promise<Metadata> {
const { projectSlug } = projectSlugParamSchema.parse(params);
const project = await getSlimProjectBySlug(projectSlug);

Expand All @@ -25,13 +20,51 @@ export async function generateMetadata({
export default async function TFVarsPage({ params }: { params: unknown }) {
const { projectSlug } = projectSlugParamSchema.parse(params);
const project = await getSlimProjectBySlug(projectSlug);
const tfvars = await getTFVarsByProjectId(project.id);
const MASTER_PASSWORD = process.env.MASTER_PASSWORD || 'digger-password';
const ENCRYPTION_SALT = process.env.ENCRYPTION_SALT || 'digger-salt';

const MASTER_PASSWORD = process.env.MASTER_PASSWORD;
const ENCRYPTION_SALT = process.env.ENCRYPTION_SALT;

if (!MASTER_PASSWORD || !ENCRYPTION_SALT) {
throw new Error('MASTER_PASSWORD or ENCRYPTION_SALT is not set');
}

const envVars = await getAllEnvVars(project.id, MASTER_PASSWORD, ENCRYPTION_SALT);

async function handleUpdate(name: string, value: string, isSecret: boolean) {
'use server'
await storeEncryptedEnvVar(project.id, name, value, isSecret, MASTER_PASSWORD, ENCRYPTION_SALT);
return getAllEnvVars(project.id, MASTER_PASSWORD, ENCRYPTION_SALT);
}

async function handleDelete(name: string) {
'use server'
await deleteEnvVar(project.id, name);
return getAllEnvVars(project.id, MASTER_PASSWORD, ENCRYPTION_SALT);
}

async function handleBulkUpdate(vars: EnvVar[]) {
'use server'
for (const envVar of vars) {
if (!envVar.is_secret) {
await storeEncryptedEnvVar(project.id, envVar.name, envVar.value, envVar.is_secret, MASTER_PASSWORD, ENCRYPTION_SALT);
}
}
return getAllEnvVars(project.id, MASTER_PASSWORD, ENCRYPTION_SALT);
}

return (
<div className="flex flex-col space-y-4 max-w-5xl mt-2">
<TFVarsDetails tfvarsdata={tfvars} projectId={project.id} />
<TFVarsDetails
tfvarsdata={{
id: project.id,
project_id: project.id,
tfvars: envVars,
updated_at: new Date().toISOString()
}}
onUpdate={handleUpdate}
onDelete={handleDelete}
onBulkUpdate={handleBulkUpdate}
/>
</div>
);
}
}
Binary file added src/data/.DS_Store
Binary file not shown.
189 changes: 157 additions & 32 deletions src/data/admin/encryption.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,164 @@
import {
createCipheriv,
createDecipheriv,
pbkdf2Sync,
randomBytes,
} from 'crypto';
import { supabaseAdminClient } from '@/supabase-clients/admin/supabaseAdminClient';
import { EnvVar } from '@/types/userTypes';
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

function deriveKey(password: string, salt: string): Buffer {
return pbkdf2Sync(password, salt, 100000, 32, 'sha256');
import { scrypt } from 'crypto';
import { promisify } from 'util';

const scryptAsync = promisify(scrypt);

async function deriveKey(projectId: string): Promise<Buffer> {
// Use a fixed salt or derive it from the projectId
const salt = 'fixed_salt_value'; // You can also use projectId.slice(0, 16) if it's long enough
return scryptAsync(projectId, salt, 32) as Promise<Buffer>;
}

function encrypt(
export async function encrypt(
text: string,
ENCRYPTION_SALT: string,
MASTER_PASSWORD: string,
): { iv: string; encryptedData: string } {
const iv = randomBytes(16);
const key = deriveKey(MASTER_PASSWORD, ENCRYPTION_SALT);
const cipher = createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
iv: iv.toString('hex'),
encryptedData: encrypted,
};
}

function decrypt(
iv: string,
encryptedData: string,
ENCRYPTION_SALT: string,
MASTER_PASSWORD: string,
): string {
const key = deriveKey(MASTER_PASSWORD, ENCRYPTION_SALT);
const decipher = createDecipheriv('aes-256-cbc', key, Buffer.from(iv, 'hex'));
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
projectId: string,
): Promise<string> {
const iv = randomBytes(12);
const key = await deriveKey(projectId);
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(text, 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag();

// Combine IV, auth tag, and encrypted data
return (
iv.toString('base64') + '.' + authTag.toString('base64') + '.' + encrypted
);
}

export async function decrypt(
encryptedText: string,
projectId: string,
): Promise<string> {
const [ivBase64, authTagBase64, encryptedData] = encryptedText.split('.');
const iv = Buffer.from(ivBase64, 'base64');
const authTag = Buffer.from(authTagBase64, 'base64');
const key = await deriveKey(projectId);

const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);

let decrypted = decipher.update(encryptedData, 'base64', 'utf8');
decrypted += decipher.final('utf8');

return decrypted;
}
export async function storeEncryptedEnvVar(
projectId: string,
name: string,
value: string,
isSecret: boolean,
masterPassword: string,
salt: string,
) {
console.log('Encryption: Storing encrypted var:', {
projectId,
name,
isSecret,
});
const encrypted = await encrypt(value, projectId);

const { data, error } = await supabaseAdminClient
.from('encrypted_env_vars')
.upsert(
{
project_id: projectId,
name,
encrypted_value: encrypted,
is_secret: isSecret,
updated_at: new Date().toISOString(),
},
{
onConflict: 'project_id,name',
},
);

if (error) {
console.error('Encryption: Error storing variable:', error);
throw error;
}
console.log('Encryption: Variable stored successfully');
return data;
}
export async function getDecryptedEnvVar(
projectId: string,
name: string,
masterPassword: string,
) {
const { data, error } = await supabaseAdminClient
.from('encrypted_env_vars')
.select('encrypted_value, is_secret')
.eq('project_id', projectId)
.eq('name', name)
.single();

if (error) throw error;
if (data.is_secret) {
return { value: null, isSecret: true };
}
const decryptedValue = await decrypt(data.encrypted_value, projectId);
return { value: decryptedValue, isSecret: false };
}

export async function getAllEnvVarNames(projectId: string) {
const { data, error } = await supabaseAdminClient
.from('encrypted_env_vars')
.select('name, is_secret')
.eq('project_id', projectId);

if (error) throw error;
return data;
}

export async function deleteEnvVar(projectId: string, name: string) {
const { error } = await supabaseAdminClient
.from('encrypted_env_vars')
.delete()
.eq('project_id', projectId)
.eq('name', name);

if (error) throw error;
}

export async function getAllEnvVars(
projectId: string,
masterPassword: string,
salt: string,
): Promise<EnvVar[]> {
const { data, error } = await supabaseAdminClient
.from('encrypted_env_vars')
.select('name, encrypted_value, is_secret, updated_at')
.eq('project_id', projectId);

if (error) throw error;

console.log('Fetched data:', data);
// convert bytea to string
return Promise.all(
data.map(async (item) => {
console.log(item.encrypted_value);
try {
return {
name: item.name,
value: item.is_secret
? '********'
: await decrypt(item.encrypted_value, projectId),
is_secret: item.is_secret,
updated_at: item.updated_at,
};
} catch (error) {
console.error(`Error processing item ${item.name}:`, error);
return {
name: item.name,
value: '[Error: Unable to decrypt]',
is_secret: item.is_secret,
updated_at: item.updated_at,
};
}
}),
);
}
5 changes: 3 additions & 2 deletions src/environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ declare global {
UNKEY_API_ID: string;
TESTMAIL_PREFIX?: string;
OPENAI_API_KEY?: string;
MASTER_PASSWORD: string;
ENCRYPTION_SALT: string;
}
}
}

// eslint-disable-next-line prettier/prettier
export { };

export {};
Loading

0 comments on commit d732e62

Please sign in to comment.