Skip to content

Documentation

Cryptography Module Reference

Enterprise-grade cryptography using Argon2id and AES-256-GCM for password hashing, key derivation, and data encryption.

3/27/2026
19 min read Edit on GitHub

Overview

SveltyCMS implements military-grade cryptography through the centralized @utils/crypto module, using Argon2id for password hashing and key derivation, and AES-256-GCM for data encryption. This provides enterprise-level security against current classical computing threats and maintains strong resistance against emerging quantum computing capabilities.

Module Location: src/utils/crypto.ts

Understanding Classical vs. Quantum Cryptography

It’s crucial to understand the difference between classical and quantum cryptographic systems:

  • Argon2id is a classical password hashing algorithm designed to run on traditional computers (CPUs). It provides security through computational cost (time and memory requirements), making brute-force attacks economically infeasible.

  • Shor’s Algorithm and Grover’s Algorithm are quantum algorithms that run on quantum computers:

    • Shor’s Algorithm: Breaks asymmetric (public-key) cryptography like RSA and ECC by efficiently factoring large numbers
    • Grover’s Algorithm: Provides quadratic speedup for searching unstructured databases, reducing symmetric key security by half (e.g., AES-256 becomes AES-128 equivalent)

Key Point: Argon2 and quantum algorithms operate in fundamentally different computational paradigms. SveltyCMS’s current cryptographic approach provides strong quantum resistance through symmetric encryption and memory-hard hashing.

Why Centralized?

  • Single audit surface for security reviews
  • Consistent algorithms across entire CMS
  • Easy to update if vulnerabilities discovered
  • Prevents crypto mistakes in individual features
  • Testable and auditable cryptographic operations

Security Architecture

Algorithm Combination

SveltyCMS uses a two-algorithm approach for maximum security:

1. Argon2id - Password Hashing & Key Derivation

Why Argon2id?

  • 🏆 Winner of Password Hashing Competition (2015)
  • 🛡️ Memory-hard - Requires 64 MB RAM per attempt (stops GPU farms)
  • 🚫 ASIC-resistant - Can’t build custom hardware to crack it
  • Time-hard - 3 iterations minimum
  • 🔒 10-100x more secure than PBKDF2
  • 🌍 Future-proof - Resistant to AI/quantum advances

Configuration:

{
  memory: 65536,      // 64 MB RAM per hash
  time: 3,            // 3 iterations
  parallelism: 4,     // 4 parallel threads
  type: argon2id,     // Hybrid (resistant to side-channel + GPU attacks)
  hashLength: 32      // 256-bit output
}

2. AES-256-GCM - Data Encryption

Why AES-256-GCM?

  • 🔐 Military-grade - Used by US government for TOP SECRET data
  • Authenticated encryption - Built-in tamper detection
  • Hardware-accelerated - Modern CPUs have AES-NI instructions
  • 🌍 Industry standard - NIST approved, widely audited

Configuration:

{
  algorithm: 'aes-256-gcm',
  keyLength: 32,      // 256 bits
  ivLength: 16,       // 128 bits
  saltLength: 32,     // 256 bits for key derivation
  authTagLength: 16   // 128 bits for integrity
}

API Reference

Password Hashing

hashPassword()

Hashes a plain text password using Argon2id.

Signature:

async function hashPassword(password: string): Promise<string>;

Parameters:

  • password (string) - Plain text password to hash

Returns:

  • Promise - Argon2id hash in PHC string format

Example:

import { hashPassword } from "@utils/crypto";

// Registration - hash user password
const plainPassword = "MySecurePassword123!";
const hashedPassword = await hashPassword(plainPassword);

// Store in database
await db.users.create({
  email: "user@example.com",
  password: hashedPassword, // $argon2id$v=19$m=65536,t=3,p=4$...
});

Hash Format:

$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>

Where:

  • argon2id - Algorithm type (hybrid mode)
  • v=19 - Algorithm version
  • m=65536 - Memory cost (64 MB)
  • t=3 - Time cost (3 iterations)
  • p=4 - Parallelism (4 threads)
  • <salt> - Base64-encoded random salt
  • <hash> - Base64-encoded hash

verifyPassword()

Verifies a plain text password against an Argon2id hash.

Signature:

async function verifyPassword(password: string, hash: string): Promise<boolean>;

Parameters:

  • password (string) - Plain text password to verify
  • hash (string) - Argon2id hash to compare against

Returns:

  • Promise - true if password matches, false otherwise

Example:

import { verifyPassword } from "@utils/crypto";

// Login - verify user password
const user = await db.users.findByEmail("user@example.com");
const isValid = await verifyPassword(plainPassword, user.password);

if (isValid) {
  // Password correct - create session
  const session = await createSession(user._id);
} else {
  // Password incorrect - reject login
  throw new Error("Invalid credentials");
}

Security Notes:

  • ⏱️ Constant-time comparison (prevents timing attacks)
  • 🔒 Automatically extracts parameters from hash (memory, time, parallelism)
  • 🛡️ Resistant to timing side-channel attacks
  • 🧪 Timing-Safe Test Bypass: CI/CD test secrets are verified using crypto.timingSafeEqual (via @src/hooks/handle-turbo-pipeline.server.ts) to prevent side-channel leaks of administrative test credentials.

Data Encryption

encryptData()

Encrypts data using AES-256-GCM with Argon2id key derivation.

Signature:

async function encryptData(data: Record<string, unknown>, password: string): Promise<string>;

Parameters:

  • data (Record) - Object to encrypt
  • password (string) - Password for key derivation

Returns:

  • Promise - Base64-encoded encrypted blob

Example:

import { encryptData } from "@utils/crypto";

// Encrypt sensitive configuration data
const sensitiveData = {
  apiKey: "sk_live_abc123...",
  dbPassword: "secretPassword",
  jwtSecret: "myJWTSecret",
};

const encrypted = await encryptData(sensitiveData, "UserPassword123!");

// encrypted = "A1B2C3...base64-encoded-blob"
// Safe to store or transfer

Encryption Process:

1. Generate random salt (32 bytes)
2. Derive key from password using Argon2id (64 MB RAM, 3 iterations)
3. Generate random IV (16 bytes)
4. Encrypt data with AES-256-GCM
5. Get authentication tag (16 bytes)
6. Combine: salt + IV + authTag + encrypted data
7. Base64 encode result

Output Format:

[salt:32][iv:16][authTag:16][encryptedData:variable]

All base64-encoded as single string.


decryptData()

Decrypts data encrypted by encryptData().

Signature:

async function decryptData(
  encryptedData: string,
  password: string,
): Promise<Record<string, unknown>>;

Parameters:

  • encryptedData (string) - Base64-encoded encrypted blob from encryptData()
  • password (string) - Password used for encryption

Returns:

  • Promise> - Decrypted object

Throws:

  • Error if password is incorrect
  • Error if data is tampered with (auth tag mismatch)
  • Error if data is corrupted

Example:

import { decryptData } from "@utils/crypto";

// Decrypt sensitive configuration
try {
  const decrypted = await decryptData(encrypted, "UserPassword123!");

  console.log(decrypted);
  // {
  //   apiKey: 'sk_live_abc123...',
  //   dbPassword: 'secretPassword',
  //   jwtSecret: 'myJWTSecret'
  // }
} catch (error) {
  console.error("Decryption failed:", error.message);
  // Possible reasons:
  // - Wrong password
  // - Data tampered with
  // - Data corrupted
}

Decryption Process:

1. Base64 decode encrypted blob
2. Extract: salt, IV, authTag, encrypted data
3. Derive same key from password using Argon2id with extracted salt
4. Decrypt with AES-256-GCM
5. Verify authentication tag (ensures data integrity)
6. Parse and return decrypted JSON

Security Features:

  • Authentication tag verification - Detects tampering
  • Same salt reuse - Ensures consistent key derivation
  • Constant-time operations - Prevents timing attacks
  • Automatic validation - Throws on any integrity issue

Key Derivation

deriveKey()

Derives a cryptographic key from a password using Argon2id.

Signature:

async function deriveKey(password: string, salt: Buffer): Promise<Buffer>;

Parameters:

  • password (string) - Password to derive key from
  • salt (Buffer) - Random salt (32 bytes recommended)

Returns:

  • Promise - 256-bit (32-byte) key

Example:

import crypto from "crypto";
import { deriveKey } from "@utils/crypto";

// Generate random salt
const salt = crypto.randomBytes(32);

// Derive key from password
const key = await deriveKey("UserPassword123!", salt);

// Use key for custom encryption
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);

Use Cases:

  • Custom encryption schemes
  • API key generation
  • Token signing keys
  • Database encryption keys

Security Notes:

  • 🔒 Same password + salt = same key (deterministic)
  • 🎲 Different salt = different key (unique per use)
  • ⏱️ 64 MB RAM + 3 iterations = slow brute force (~100 attempts/sec on modern GPU)

Security Properties

Attack Resistance

Brute Force Attack

Scenario: Attacker tries all possible passwords

Password Strength Keyspace Time to Crack (Argon2)
8 chars (mixed) 62^8 = 2.2×10^14 ~70 years
12 chars (mixed) 62^12 = 3.2×10^21 10 billion years
16 chars (mixed + symbols) 95^16 = 4.4×10^31 10^23 years

Why so slow?

  • 64 MB RAM per attempt (limits parallel attacks)
  • 3 iterations (computational cost)
  • No ASIC acceleration (memory-hard)

GPU Farm Attack

Scenario: Attacker uses 1000 high-end GPUs (RTX 4090)

  • Without Argon2 (e.g., SHA-256): 1 trillion hashes/sec
  • With Argon2: ~100,000 hashes/sec (10 million times slower)

Reason: Each hash requires 64 MB dedicated RAM, GPUs can’t parallelize effectively.

AI/ML Attack

Scenario: Neural network tries to predict passwords

  • Without Argon2: Can train on billions of hashes quickly
  • With Argon2: Training is prohibitively expensive (64 MB per training sample)

Result: AI attacks are economically infeasible.

Quantum Computing Attack

Current Status: SveltyCMS uses quantum-resistant cryptographic techniques.

Understanding Quantum Threats:

  1. Shor’s Algorithm (for Public-Key Cryptography):

    • Threat: Can break RSA, DSA, and ECC in polynomial time
    • SveltyCMS Impact: ✅ NOT AFFECTED - We don’t use public-key cryptography for data at rest
    • Timeline: Large-scale quantum computers capable of breaking 2048-bit RSA are estimated 10-20+ years away
  2. Grover’s Algorithm (for Symmetric Encryption):

    • Threat: Provides quadratic speedup for brute-force searches
    • Impact on AES-256: Reduces effective security from 256 bits to 128 bits
    • Real-world implication: 2^128 operations is still computationally infeasible
    • Timeline: Quantum computers with sufficient qubits are decades away
  3. Argon2 Quantum Resistance:

    • Memory-hard algorithms like Argon2 are inherently quantum-resistant
    • Quantum computers don’t provide speedup for memory-bound operations
    • 64 MB memory requirement per hash limits quantum advantage
    • Grover’s algorithm doesn’t help when the bottleneck is memory access, not computation

Security Analysis:

Attack Vector Classical Security Quantum Security (Grover’s) SveltyCMS Status
AES-256-GCM Brute Force 2^256 operations 2^128 operations ✅ Secure
Argon2id Password Cracking 2^128+ operations Limited quantum advantage ✅ Secure
RSA/ECC Public-Key Secure Vulnerable (Shor’s) ✅ Not Used

Key Points:

  • AES-256 remains secure: Even with Grover’s algorithm reducing it to “AES-128” equivalent, 2^128 operations is still infeasible (would take billions of years)
  • Argon2 is quantum-resistant: Memory-hard algorithms resist quantum speedup because quantum computers don’t have memory advantages
  • No vulnerable public-key crypto: SveltyCMS doesn’t use RSA or ECC for data encryption, avoiding Shor’s algorithm vulnerabilities

Security Timeline:

Current (2025):    ████████████████████████████████ 100% secure
+10 years (2035):  ██████████████████████████████   ~98% secure (small quantum computers emerging)
+20 years (2045):  ████████████████████████         ~85% secure (larger quantum computers)
+30 years (2055):  ████████████████                 ~70% secure (post-quantum migration recommended)

Migration Path: See “Post-Quantum Cryptography Roadmap” section below for future-proofing strategy.


Post-Quantum Cryptography Roadmap

Current Quantum Resistance Status

SveltyCMS’s current cryptographic implementation is already well-positioned for the quantum era:

✅ Quantum-Resistant Components:

  • Argon2id: Memory-hard algorithms resist quantum speedup
  • AES-256-GCM: Maintains 128-bit quantum security (still computationally infeasible)
  • Symmetric-key only: No vulnerable public-key cryptography (RSA/ECC) for data encryption

⚠️ Future Considerations:

  • When quantum computers become practical (15-30+ years), consider post-quantum algorithms
  • Monitor NIST post-quantum standardization efforts
  • Plan hybrid cryptographic approaches

NIST Post-Quantum Standards (2024)

The National Institute of Standards and Technology (NIST) has standardized post-quantum cryptographic algorithms:

For Public-Key Encryption (Key Encapsulation)

CRYSTALS-Kyber (now standardized as ML-KEM):

  • Use case: Secure key exchange
  • Based on: Lattice-based cryptography (Module Learning With Errors)
  • Security levels: Kyber-512, Kyber-768, Kyber-1024
  • Advantages: Fast, small key sizes, strong security proofs
  • SveltyCMS application: Future key exchange mechanisms, API authentication

For Digital Signatures

CRYSTALS-Dilithium (now standardized as ML-DSA):

  • Use case: Digital signatures for authentication
  • Based on: Lattice-based cryptography (FIPS 204)
  • Security levels: Dilithium-2, Dilithium-3, Dilithium-5
  • Advantages: Efficient verification, secure against quantum attacks
  • SveltyCMS application: Future session token signing, API request verification

SPHINCS+ (now standardized as SLH-DSA):

  • Use case: Stateless hash-based signatures
  • Based on: Hash functions only
  • Advantages: Conservative security assumptions, no secret state
  • SveltyCMS application: Backup signature scheme

Hybrid Cryptographic Approach (Recommended)

For maximum security during the quantum transition, implement hybrid cryptography:

// Future hybrid encryption example (conceptual)
async function hybridEncrypt(data: string, publicKey: string) {
  // Step 1: Use classical AES-256-GCM (current system)
  const classicalKey = generateRandomKey(32);
  const classicalCiphertext = await aesEncrypt(data, classicalKey);

  // Step 2: Use post-quantum key encapsulation (future)
  const pqCiphertext = await kyberEncapsulate(classicalKey, publicKey);

  // Step 3: Combine both
  return {
    ciphertext: classicalCiphertext,
    encapsulatedKey: pqCiphertext,
    algorithm: "AES-256-GCM + Kyber-1024",
  };
}

Benefits:

  • ✅ Protected against classical attacks (AES-256)
  • ✅ Protected against quantum attacks (Kyber)
  • ✅ Backward compatible
  • ✅ Future-proof migration path

Migration Timeline

Phase 1: Research & Planning (2025-2027)

  • Monitor NIST post-quantum standard implementations
  • Evaluate JavaScript/TypeScript libraries (e.g., liboqs-js, pqcrypto)
  • Test performance in Node.js environment
  • Design hybrid encryption architecture

Phase 2: Implementation (2028-2030)

  • Add post-quantum algorithms alongside existing crypto
  • Implement hybrid encryption for new sensitive data
  • Provide migration tools for existing encrypted data
  • Update documentation and security guidelines

Phase 3: Gradual Migration (2030-2040)

  • Enable post-quantum crypto by default for new installations
  • Provide automated migration path for existing data
  • Maintain backward compatibility
  • Monitor quantum computing progress

Phase 4: Full Transition (2040+)

  • Deprecate classical-only encryption
  • Require post-quantum algorithms
  • Remove legacy encryption support

Current Recommendations

For New Deployments (2025-2030):

  • ✅ Continue using current Argon2id + AES-256-GCM system
  • ✅ Keep encryption keys updated and rotated
  • ✅ Monitor quantum computing developments
  • ✅ Stay informed about NIST PQC standards

For Long-Term Data (20+ year retention):

  • ⚠️ Consider additional encryption layers
  • ⚠️ Plan for data re-encryption with post-quantum algorithms
  • ⚠️ Implement key rotation policies
  • ⚠️ Use hybrid encryption for maximum security

For High-Security Applications:

  • 🔒 Increase Argon2 parameters (memory: 131072 KiB, time: 5)
  • 🔒 Use AES-256-GCM with 256-bit keys (no reduction)
  • 🔒 Consider hardware security modules (HSM) with PQC support
  • 🔒 Implement multi-layer encryption

Resources


Usage Examples

User Authentication System

import { hashPassword, verifyPassword } from "@utils/crypto";

// User Registration
export async function registerUser(email: string, password: string) {
  // Hash password with Argon2id
  const hashedPassword = await hashPassword(password);

  // Store in database
  const user = await db.users.create({
    email,
    password: hashedPassword,
    createdAt: new Date(),
  });

  return user;
}

// User Login
export async function loginUser(email: string, password: string) {
  // Find user
  const user = await db.users.findByEmail(email);
  if (!user) {
    throw new Error("Invalid credentials");
  }

  // Verify password
  const isValid = await verifyPassword(password, user.password);
  if (!isValid) {
    throw new Error("Invalid credentials");
  }

  // Create session
  const session = await createSession(user._id);
  return { user, session };
}

Secure Configuration Export/Import

import { encryptData, decryptData } from "@utils/crypto";

// Export sensitive configuration
export async function exportConfig(password: string) {
  const config = {
    database: {
      host: "localhost",
      password: "dbPassword123",
    },
    api: {
      key: "sk_live_abc123",
      secret: "secret_xyz789",
    },
    jwt: {
      secret: "myJWTSecret",
    },
  };

  // Encrypt with password
  const encrypted = await encryptData(config, password);

  // Save to file
  await fs.writeFile("config.encrypted", encrypted);

  return { success: true };
}

// Import configuration
export async function importConfig(password: string) {
  // Read encrypted file
  const encrypted = await fs.readFile("config.encrypted", "utf8");

  try {
    // Decrypt with password
    const config = await decryptData(encrypted, password);

    // Apply configuration
    await applyConfig(config);

    return { success: true, config };
  } catch (error) {
    throw new Error("Invalid password or corrupted file");
  }
}

API Token Encryption

import { encryptData, decryptData } from "@utils/crypto";

// Store encrypted API token
export async function storeApiToken(userId: string, token: string, masterPassword: string) {
  const tokenData = {
    token,
    createdAt: new Date().toISOString(),
    expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days
  };

  // Encrypt token
  const encrypted = await encryptData(tokenData, masterPassword);

  // Store in database
  await db.apiTokens.create({
    userId,
    encryptedToken: encrypted,
  });
}

// Retrieve and decrypt API token
export async function getApiToken(userId: string, masterPassword: string) {
  const record = await db.apiTokens.findByUserId(userId);
  if (!record) return null;

  try {
    // Decrypt token
    const tokenData = await decryptData(record.encryptedToken, masterPassword);

    // Check expiration
    if (new Date(tokenData.expiresAt) < new Date()) {
      throw new Error("Token expired");
    }

    return tokenData.token;
  } catch (error) {
    throw new Error("Failed to decrypt token");
  }
}

Best Practices

Password Requirements

For maximum security with Argon2id:

export function validatePassword(password: string): { valid: boolean; message: string } {
  // Default minimum length is 8, but 12+ is recommended for enterprise security
  if (password.length < 8) {
    return { valid: false, message: "Password must be at least 8 characters" };
  }
  if (!/[A-Z]/.test(password)) {
    return { valid: false, message: "Password must contain uppercase letter" };
  }
  if (!/[a-z]/.test(password)) {
    return { valid: false, message: "Password must contain lowercase letter" };
  }
  if (!/[0-9]/.test(password)) {
    return { valid: false, message: "Password must contain number" };
  }
  if (!/[^A-Za-z0-9]/.test(password)) {
    return { valid: false, message: "Password must contain special character" };
  }
  return { valid: true, message: "Password is strong" };
}

Salt Generation

Always use cryptographically secure random salts:

import crypto from "crypto";

// ✅ GOOD: Cryptographically secure
const salt = crypto.randomBytes(32);

// ❌ BAD: Not secure
const salt = Buffer.from(Math.random().toString());

Key Storage

DO:

  • ✅ Store hashed passwords in database
  • ✅ Use environment variables for master passwords
  • ✅ Encrypt sensitive data at rest
  • ✅ Use hardware security modules (HSM) for keys in production
  • ✅ Rotate encryption keys periodically

DON’T:

  • ❌ Store plain text passwords
  • ❌ Hardcode passwords in source code
  • ❌ Commit passwords to version control
  • ❌ Share passwords via email or chat
  • ❌ Use weak passwords for encryption

Error Handling

Never reveal detailed cryptographic errors to users:

// ✅ GOOD: Generic error message
try {
  const decrypted = await decryptData(encrypted, password);
} catch (error) {
  throw new Error("Authentication failed"); // Don't reveal why
}

// ❌ BAD: Reveals information
try {
  const decrypted = await decryptData(encrypted, password);
} catch (error) {
  throw new Error("Wrong password"); // Reveals password was incorrect
}

Performance Considerations

Argon2 Performance

Typical performance on modern hardware:

Hardware Hashes/Second
Intel i7-12700K (12 cores) ~200
AMD Ryzen 9 5950X (16 cores) ~250
NVIDIA RTX 4090 (GPU) ~100
AWS c6a.2xlarge ~150

Why so slow?

  • 64 MB memory allocation per hash
  • CPU-bound (not GPU-optimized)
  • Intentional slowdown for security

Optimization Tips

DO:

  • ✅ Cache results when appropriate (sessions)
  • ✅ Use background workers for bulk operations
  • ✅ Implement rate limiting on authentication endpoints
  • ✅ Use Redis for session storage

DON’T:

  • ❌ Hash passwords synchronously in request handlers
  • ❌ Reduce Argon2 parameters for “performance”
  • ❌ Skip encryption for “convenience”

Testing

Unit Tests

import { hashPassword, verifyPassword, encryptData, decryptData } from "@utils/crypto";
import { describe, it, expect } from "vitest";

describe("Cryptography Module", () => {
  describe("Password Hashing", () => {
    it("should hash and verify password", async () => {
      const password = "MySecurePassword123!";
      const hash = await hashPassword(password);

      expect(hash).toMatch(/^\$argon2id\$/);

      const isValid = await verifyPassword(password, hash);
      expect(isValid).toBe(true);

      const isInvalid = await verifyPassword("wrongpassword", hash);
      expect(isInvalid).toBe(false);
    });
  });

  describe("Data Encryption", () => {
    it("should encrypt and decrypt data", async () => {
      const data = { secret: "mySecret", apiKey: "key123" };
      const password = "EncryptionPassword123!";

      const encrypted = await encryptData(data, password);
      expect(encrypted).toBeTypeOf("string");

      const decrypted = await decryptData(encrypted, password);
      expect(decrypted).toEqual(data);
    });

    it("should fail with wrong password", async () => {
      const data = { secret: "mySecret" };
      const encrypted = await encryptData(data, "password1");

      await expect(decryptData(encrypted, "wrongpassword")).rejects.toThrow();
    });
  });
});

Related Documentation


References


Plugin Settings Encryption (Static-Key AES-256-GCM)

In addition to password-based encryption (encryptData/decryptData), SveltyCMS provides a static-key encryption system for plugin settings via @src/plugins/settings-crypto.ts.

Use Case

Plugin settings (API keys, webhook secrets, SMTP credentials) need to be encrypted at rest but decrypted transparently when the plugin runs — without requiring a user password each time. The static-key system uses an environment variable (SECRET_ENCRYPTION_KEY) as the encryption key.

Key Differences from encryptData

Feature encryptData (Password-based) encryptSecret (Static-key)
Key source User-provided password + Argon2id SECRET_ENCRYPTION_KEY env var
Key derivation Argon2id (64 MB, 3 iterations) Direct hex/base64 key (or SHA-256 derive)
Use case User data export/import Automated plugin settings
Interactive Yes (user enters password) No (fully automated)
Envelope format salt:iv:authTag:ciphertext version:iv:authTag:ciphertext

API Reference

encryptSecret()

Encrypts a plaintext value for plugin settings storage.

async function encryptSecret(plaintext: string): Promise<string | null>;

Throws if SECRET_ENCRYPTION_KEY is not configured.

Returns: Base64-encoded v1:iv:authTag:ciphertext envelope.

decryptSecret()

Decrypts a value from plugin settings storage. Server-only — never call from client code.

async function decryptSecret(envelope: string): Promise<string | null>;

Returns null if no encryption key is configured or decryption fails.

Configuration

Generate a 256-bit hex key:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Set in environment:

export SECRET_ENCRYPTION_KEY="a1b2c3d4e5f6..." # 64 hex characters

Envelope Format (version 1)

Byte 0:    Version (0x01)
Bytes 1-16: IV (128 bits)
Bytes 17-32: Auth Tag (128 bits)
Bytes 33+:  Ciphertext (variable length)

All base64-encoded as a single string.

Masking in API Responses

Secrets are never sent to the browser. API responses replace encrypted values with ••••••••. The decryption accessor (decryptSecret) is server-only and should only be used in .server.ts files or server-side plugin code.

See Plugin Settings Crypto and Plugin Settings Declaration for implementation details.


Last Updated: July 2026 Security Audit: Annual review recommended

securitycryptographyargon2aes-256encryption
Was this page helpful?