Skip to main content

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:
src/lib/crypto/crypto.ts
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:
  1. Random IV: A new 12-byte IV is generated for each encryption operation
  2. Cipher Creation: Creates an AES-256-GCM cipher with the key and IV
  3. Encryption: Converts plaintext UTF-8 to encrypted hex
  4. Auth Tag: Retrieves the authentication tag for tamper detection
  5. 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:
src/lib/crypto/crypto.ts
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:
  1. Format Validation: Ensures the encrypted string has all three parts
  2. Buffer Conversion: Converts hex strings back to buffers
  3. Auth Tag Verification: Sets the auth tag to verify data integrity
  4. Decryption: Converts encrypted hex back to UTF-8 plaintext
  5. 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:
.env
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:
src/lib/crypto/crypto.ts
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:
  1. Dual-key period: Add new key alongside old key
  2. Decrypt with old key: Read and decrypt all existing data
  3. Re-encrypt with new key: Encrypt data with new key and update records
  4. 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:
Example Test
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

Build docs developers (and LLMs) love