Overview
VizBoard uses AES-256-GCM (Galois/Counter Mode) encryption to protect database credentials stored in the application database. This ensures that sensitive connection information remains secure even if the database is compromised.
AES-256-GCM provides both confidentiality (encryption) and authenticity (authentication tag), protecting against both data disclosure and tampering.
Encryption Algorithm
Algorithm Details
Algorithm : AES-256-GCM (Advanced Encryption Standard in Galois/Counter Mode)
Key Size : 256 bits (32 bytes)
IV Length : 12 bytes (96 bits)
Authentication : Built-in authentication tag
Mode : Authenticated Encryption with Associated Data (AEAD)
Why AES-256-GCM?
Strong Security AES-256 is approved by NSA for TOP SECRET information and is considered unbreakable with current technology.
Authenticated Encryption GCM mode provides authentication tags that detect any tampering or corruption of encrypted data.
Performance GCM is highly efficient and can be hardware-accelerated on modern CPUs.
NIST Approved Recommended by NIST for authenticated encryption in NIST Special Publication 800-38D.
Implementation
Encryption Function
The encryption function generates a random IV, encrypts the data, and returns a formatted string:
import crypto from 'crypto'
const ENCRYPTION_KEY_HEX = process . env . ENCRYPTION_KEY
const ENCRYPTION_KEY = Buffer . from ( ENCRYPTION_KEY_HEX , 'hex' )
const ALGORITHM = 'aes-256-gcm'
const IV_LENGTH = 12
export function encrypt ( text : string ) : string {
const iv = crypto . randomBytes ( IV_LENGTH )
const cipher = crypto . createCipheriv ( ALGORITHM , ENCRYPTION_KEY , iv )
let encrypted = cipher . update ( text , 'utf8' , 'hex' )
encrypted += cipher . final ( 'hex' )
const authTag = cipher . getAuthTag ()
return ` ${ iv . toString ( 'hex' ) } : ${ encrypted } : ${ authTag . toString ( 'hex' ) } `
}
Key components:
Random IV : A new 12-byte IV is generated for each encryption operation
Cipher Creation : Creates an AES-256-GCM cipher with the key and IV
Encryption : Converts plaintext UTF-8 to encrypted hex
Auth Tag : Retrieves the authentication tag for tamper detection
Format : Returns IV:EncryptedData:AuthTag format
Never reuse IVs with the same encryption key. Each encryption operation generates a fresh random IV to maintain security.
Decryption Function
The decryption function parses the encrypted string, verifies the auth tag, and decrypts:
export function decrypt ( encryptedTextWithMeta : string ) : string {
const textParts = encryptedTextWithMeta . split ( ':' )
if ( textParts . length !== 3 ) {
throw new Error ( "Invalid encrypted text format. Expected IV:EncryptedText:AuthTag." )
}
const iv = Buffer . from ( textParts [ 0 ], 'hex' )
const encryptedText = textParts [ 1 ]
const authTag = Buffer . from ( textParts [ 2 ], 'hex' )
const decipher = crypto . createDecipheriv ( ALGORITHM , ENCRYPTION_KEY , iv )
decipher . setAuthTag ( authTag )
try {
let decrypted = decipher . update ( encryptedText , 'hex' , 'utf8' )
decrypted += decipher . final ( 'utf8' )
return decrypted
} catch ( error : unknown ) {
if ( error instanceof Error ) {
console . error ( 'Decryption failed:' , error . message )
} else {
console . error ( 'Decryption failed with an unknown error.' )
}
throw new Error ( 'Authentication failed or data corrupted. Cannot decrypt.' )
}
}
Key components:
Format Validation : Ensures the encrypted string has all three parts
Buffer Conversion : Converts hex strings back to buffers
Auth Tag Verification : Sets the auth tag to verify data integrity
Decryption : Converts encrypted hex back to UTF-8 plaintext
Error Handling : Catches tampered or corrupted data
If the authentication tag doesn’t match, the decryption will fail. This prevents using tampered or corrupted data.
Usage in VizBoard
Encrypting Database Credentials
When creating or updating a database connection, credentials are encrypted before storage:
src/app/actions/project/crud.ts
import { encrypt } from "@/lib/crypto/crypto"
const encryptedAccess = encrypt (
JSON . stringify ({
host: conn . host ,
port: conn . port ,
database: conn . database ,
user: conn . user ,
password: conn . password ,
})
)
await tx . dbConnection . create ({
data: {
projectId: createdProject . id ,
title: conn . title ,
dbAccess: encryptedAccess , // Stored as encrypted string
},
})
Decrypting Database Credentials
When accessing database connections, credentials are decrypted:
src/lib/db/decryptDbConfig.ts
import { decrypt } from "@/lib/crypto/crypto"
export function decryptDbConfig ( encryptedDbAccess : unknown ) {
if ( ! encryptedDbAccess || typeof encryptedDbAccess !== "string" ) {
throw new Error ( "Invalid or missing dbAccess for this project." )
}
const decrypted = decrypt ( encryptedDbAccess )
return JSON . parse ( decrypted )
}
Full Workflow Example
Here’s how encrypted credentials are used when validating a connection:
src/app/actions/project/validation.ts
import { decryptDbConfig } from "@/lib/db/decryptDbConfig"
const project = await prisma . project . findFirst ({
where: { id: projectId , userId },
include: { dbconnections: true },
})
for ( const connection of project . dbconnections ) {
// Decrypt credentials
const dbAccess = decryptDbConfig ( connection . dbAccess )
// Use decrypted credentials
const response = await fetch ( ` ${ process . env . NEXTAUTH_URL } /api/testdbconnection` , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
host: dbAccess . host ,
port: dbAccess . port ,
database: dbAccess . database ,
user: dbAccess . user ,
password: dbAccess . password ,
}),
})
}
Encryption Key Management
Generating an Encryption Key
Generate a secure 256-bit (32-byte) encryption key:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
This will output a 64-character hexadecimal string like:
a3f8c9e2b1d4f7e6a9c2d5e8b1f4a7c0e3f6b9d2c5e8a1b4f7e0c3d6a9b2e5f8
Setting the Environment Variable
Add the encryption key to your .env file:
ENCRYPTION_KEY = a3f8c9e2b1d4f7e6a9c2d5e8b1f4a7c0e3f6b9d2c5e8a1b4f7e0c3d6a9b2e5f8
Critical Security Practices:
Use different encryption keys for development, staging, and production
Never commit the .env file to version control
Store production keys in a secure secrets management system
Rotate encryption keys periodically (requires re-encrypting all data)
Restrict access to encryption keys to only necessary systems
Key Validation
The application validates the encryption key format at startup:
const ENCRYPTION_KEY_HEX = process . env . ENCRYPTION_KEY
if ( ! ENCRYPTION_KEY_HEX ) {
throw new Error (
"Missing ENCRYPTION_KEY environment variable. It should be a 64-char hexadecimal string."
)
}
const ENCRYPTION_KEY = Buffer . from ( ENCRYPTION_KEY_HEX , 'hex' )
if ( ENCRYPTION_KEY . length !== 32 ) {
throw new Error (
`ENCRYPTION_KEY must be 32 bytes (64 hex characters) long. Current length: ${ ENCRYPTION_KEY . length } bytes.`
)
}
Security Considerations
1. Key Storage
Environment Variables Store the encryption key in environment variables, never in code or configuration files committed to version control.
2. IV Uniqueness
Random IV Generation Each encryption operation uses a fresh random IV. Never reuse IVs with the same key, as this breaks GCM security.
3. Authentication Tag
Tamper Detection The authentication tag ensures that any modification to the encrypted data is detected during decryption.
4. Error Handling
try {
let decrypted = decipher . update ( encryptedText , 'hex' , 'utf8' )
decrypted += decipher . final ( 'utf8' )
return decrypted
} catch ( error : unknown ) {
// Generic error message - don't leak information
throw new Error ( 'Authentication failed or data corrupted. Cannot decrypt.' )
}
Error messages are intentionally generic to avoid leaking information about the encryption system or data format.
5. Key Rotation Strategy
If you need to rotate encryption keys:
Dual-key period : Add new key alongside old key
Decrypt with old key : Read and decrypt all existing data
Re-encrypt with new key : Encrypt data with new key and update records
Remove old key : Once all data is re-encrypted, remove old key
Key rotation is a complex operation. Plan carefully and test thoroughly in a staging environment before rotating production keys.
Testing Encryption
You can test the encryption functions:
import { encrypt , decrypt } from '@/lib/crypto/crypto'
// Test data
const originalData = JSON . stringify ({
host: 'localhost' ,
port: 5432 ,
database: 'mydb' ,
user: 'admin' ,
password: 'secretpassword123'
})
// Encrypt
const encrypted = encrypt ( originalData )
console . log ( 'Encrypted:' , encrypted )
// Output: a1b2c3d4e5f6....:9f8e7d6c5b4a....:1a2b3c4d5e6f....
// Decrypt
const decrypted = decrypt ( encrypted )
console . log ( 'Decrypted:' , decrypted )
// Output: {"host":"localhost","port":5432,...}
// Verify
console . log ( 'Match:' , originalData === decrypted ) // true
Security Overview Learn about authentication, authorization, and security best practices
Database Connections How to configure and manage database connections in VizBoard