diff --git a/src/data/admin/encryption.ts b/src/data/admin/encryption.ts deleted file mode 100644 index 8ba9814a..00000000 --- a/src/data/admin/encryption.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { supabaseAdminClient } from '@/supabase-clients/admin/supabaseAdminClient'; -import { EnvVar } from '@/types/userTypes'; -import { - createCipheriv, - createDecipheriv, - publicEncrypt, - randomBytes, -} from 'crypto'; - -import { constants, scrypt } from 'crypto'; -import { promisify } from 'util'; - -const scryptAsync = promisify(scrypt); - -async function deriveKey(projectId: string, salt: Buffer): Promise { - // Combine masterPassword, projectId, and salt to create a unique key - const keyMaterial = process.env.MASTER_PASSWORD + projectId; - return scryptAsync(keyMaterial, salt, 32) as Promise; -} - -export async function encryptSecretWithPublicKey( - text: string, - projectId: string, -): Promise { - const { data: orgData } = await supabaseAdminClient - .from('projects') - .select('organization_id') - .eq('id', projectId) - .single(); - const { data: publicKeyData } = await supabaseAdminClient - .from('organizations') - .select('public_key') - .eq('id', orgData?.organization_id || '') - .single(); - const publicKey = publicKeyData?.public_key; - if (!publicKey) { - console.error('No secrets key in the org'); - throw new Error('No secrets key in the org'); - } - const buffer = Buffer.from(text, 'utf8'); - const encrypted = publicEncrypt( - { - key: publicKey, - padding: constants.RSA_PKCS1_OAEP_PADDING, - oaepHash: 'sha256', - }, - buffer, - ); - return encrypted.toString('base64'); -} - -export async function encryptWithDerivedKey( - text: string, - projectId: string, -): Promise { - const iv = randomBytes(12); - const salt = randomBytes(16); - const key = await deriveKey(projectId, salt); - const cipher = createCipheriv('aes-256-gcm', key, iv); - let encrypted = cipher.update(text, 'utf8', 'base64'); - encrypted += cipher.final('base64'); - const authTag = cipher.getAuthTag(); - - // Combine salt, IV, auth tag, and encrypted data - return ( - salt.toString('base64') + - '.' + - iv.toString('base64') + - '.' + - authTag.toString('base64') + - '.' + - encrypted - ); -} - -export async function decryptWithDerivedKey( - encryptedText: string, - projectId: string, -): Promise { - const [saltBase64, ivBase64, authTagBase64, encryptedData] = - encryptedText.split('.'); - const salt = Buffer.from(saltBase64, 'base64'); - const iv = Buffer.from(ivBase64, 'base64'); - const authTag = Buffer.from(authTagBase64, 'base64'); - const key = await deriveKey(projectId, salt); - - 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, -) { - console.log('Encryption: Storing encrypted var:', { - projectId, - name, - isSecret, - }); - //TODO better name - const plainTextValue = isSecret - ? encryptSecretWithPublicKey(value, projectId) - : value; - - const encryptedWithDerivedKeyValue = await encryptWithDerivedKey( - value, - projectId, - ); - - const { data, error } = await supabaseAdminClient - .from('encrypted_env_vars') - .upsert( - { - project_id: projectId, - name, - encrypted_value: encryptedWithDerivedKeyValue, - 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) { - 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 decryptWithDerivedKey( - 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): Promise { - 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 decryptWithDerivedKey(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, - }; - } - }), - ); -} diff --git a/src/data/admin/env-vars.ts b/src/data/admin/env-vars.ts new file mode 100644 index 00000000..f0f7c589 --- /dev/null +++ b/src/data/admin/env-vars.ts @@ -0,0 +1,132 @@ +import { supabaseAdminClient } from '@/supabase-clients/admin/supabaseAdminClient'; +import { EnvVar } from '@/types/userTypes'; +import { constants, publicEncrypt } from 'crypto'; + +export async function encryptSecretWithPublicKey( + text: string, + projectId: string, +): Promise { + const { data: orgData } = await supabaseAdminClient + .from('projects') + .select('organization_id') + .eq('id', projectId) + .single(); + const { data: publicKeyData } = await supabaseAdminClient + .from('organizations') + .select('public_key') + .eq('id', orgData?.organization_id || '') + .single(); + const publicKey = publicKeyData?.public_key; + if (!publicKey) { + console.error('No secrets key in the org'); + throw new Error('No secrets key in the org'); + } + const buffer = Buffer.from(text, 'utf8'); + const encrypted = publicEncrypt( + { + key: publicKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256', + }, + buffer, + ); + return encrypted.toString('base64'); +} + +export async function storeEnvVar( + projectId: string, + name: string, + value: string, + isSecret: boolean, +) { + const storedValue = isSecret + ? await encryptSecretWithPublicKey(value, projectId) + : value; // non-secret values stored in plain text + + const { data, error } = await supabaseAdminClient.from('env_vars').upsert( + { + project_id: projectId, + name, + value: storedValue, + 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 getEnvVar(projectId: string, name: string) { + const { data, error } = await supabaseAdminClient + .from('env_vars') + .select('value, is_secret') + .eq('project_id', projectId) + .eq('name', name) + .single(); + + if (error) throw error; + if (data.is_secret) { + return { value: null, isSecret: true }; + } else { + return { value: data.value, isSecret: false }; + } +} + +export async function getAllEnvVarNames(projectId: string) { + const { data, error } = await supabaseAdminClient + .from('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('env_vars') + .delete() + .eq('project_id', projectId) + .eq('name', name); + + if (error) throw error; +} + +export async function getAllEnvVars(projectId: string): Promise { + const { data, error } = await supabaseAdminClient + .from('env_vars') + .select('name, value, is_secret, updated_at') + .eq('project_id', projectId); + + if (error) throw error; + + // convert bytea to string + return Promise.all( + data.map(async (item) => { + console.log(item.value); + try { + return { + name: item.name, + value: item.is_secret ? '********' : item.value, + 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 get env vars]', + is_secret: item.is_secret, + updated_at: item.updated_at, + }; + } + }), + ); +} diff --git a/src/data/user/tfvars.ts b/src/data/user/tfvars.ts index fee2d479..d40683d8 100644 --- a/src/data/user/tfvars.ts +++ b/src/data/user/tfvars.ts @@ -2,11 +2,7 @@ 'use server'; import { EnvVar } from '@/types/userTypes'; -import { - deleteEnvVar, - getAllEnvVars, - storeEncryptedEnvVar, -} from '../admin/encryption'; +import { deleteEnvVar, getAllEnvVars, storeEnvVar } from '../admin/env-vars'; export async function tfvarsOnUpdate( oldName: string, @@ -18,7 +14,7 @@ export async function tfvarsOnUpdate( if (oldName !== newName) { await deleteEnvVar(projectId, oldName); } - await storeEncryptedEnvVar(projectId, newName, value, isSecret); + await storeEnvVar(projectId, newName, value, isSecret); const vars = await getAllEnvVars(projectId); return vars.map((v) => ({ ...v, updated_at: new Date().toISOString() })); }