Cryptography Module Reference
Enterprise-grade cryptography using Argon2id and AES-256-GCM for password hashing, key derivation, and data encryption.
On this page
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 versionm=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 verifyhash(string) - Argon2id hash to compare against
Returns:
- Promise
- trueif password matches,falseotherwise
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 fromencryptData()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 fromsalt(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:
-
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
-
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
-
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
- NIST Post-Quantum Cryptography
- CRYSTALS-Kyber Specification
- CRYSTALS-Dilithium Specification
- Open Quantum Safe Project
- RFC 9180: Hybrid Public Key Encryption
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
- Build-Time Security Plugin - Prevents private settings exposure to client
- Authentication System - Complete authentication implementation
- Import/Export System - Uses crypto module for sensitive data
- Security Best Practices - General security guidelines
References
- Argon2 RFC 9106 - Official Argon2 specification
- NIST AES-GCM - AES-GCM specification
- OWASP Password Storage - Password storage best practices
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