Claude-skill-registry encryption-aes-gcm
AES-256-GCM encryption for sensitive credentials (BYOD/BYOK). Node crypto, IV + authTag storage, multi-field colon-separated IVs. Triggers on "encryption", "decrypt", "AES-256-GCM", "BYOD", "BYOK", "credentials".
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/encryption-aes-gcm" ~/.claude/skills/majiayu000-claude-skill-registry-encryption-aes-gcm && rm -rf "$T"
skills/data/encryption-aes-gcm/SKILL.mdAES-256-GCM Encryption
Project uses AES-256-GCM for encrypting user credentials (BYOD Convex keys, BYOK API keys). Node crypto module, requires
"use node" directive.
Core Pattern
// From convex/lib/encryption.ts "use node"; import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; const ALGORITHM = "aes-256-gcm";
All encryption functions in files with
"use node" at top (Convex Node runtime required).
Environment Key
// From convex/lib/encryption.ts function getEncryptionKey(): Buffer { const key = process.env.BYOD_ENCRYPTION_KEY; if (!key) { throw new Error("BYOD_ENCRYPTION_KEY environment variable not set"); } // 32 bytes (64 hex chars) - use directly if (key.length === 64) { return Buffer.from(key, "hex"); } // Otherwise hash to 32 bytes const crypto = require("node:crypto"); return crypto.createHash("sha256").update(key).digest(); }
Key requirement:
BYOD_ENCRYPTION_KEY in Convex environment. Accepts 64 hex chars or any string (hashed).
Encryption Function
// From convex/lib/encryption.ts export async function encryptCredential( plaintext: string, ): Promise<{ encrypted: string; iv: string; authTag: string }> { const key = getEncryptionKey(); const iv = randomBytes(16); const cipher = createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(plaintext, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag(); return { encrypted, iv: iv.toString("hex"), authTag: authTag.toString("hex"), }; }
Returns three parts: encrypted data, IV (16 bytes), authTag (for GCM authentication).
Always generate new IV per encryption (never reuse).
Decryption Function
// From convex/lib/encryption.ts export async function decryptCredential( encrypted: string, iv: string, authTag: string, ): Promise<string> { const key = getEncryptionKey(); const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(iv, "hex")); decipher.setAuthTag(Buffer.from(authTag, "hex")); let decrypted = decipher.update(encrypted, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; }
Must set authTag before decrypting - validates data integrity. Throws if tampered.
Multi-Field Storage (BYOK Pattern)
Store multiple encrypted keys with shared IV/authTag arrays:
// From convex/byok/saveCredentials.ts const FIELD_MAP = { vercelGateway: "encryptedVercelKey", openRouter: "encryptedOpenRouterKey", groq: "encryptedGroqKey", deepgram: "encryptedDeepgramKey", }; const KEY_INDEX = { vercelGateway: 0, openRouter: 1, groq: 2, deepgram: 3, }; // Parse colon-separated string: "iv1:iv2:iv3:iv4" function parseParts(str: string | undefined): string[] { if (!str) return ["", "", "", ""]; const parts = str.split(":"); while (parts.length < 4) parts.push(""); return parts; }
Encryption storage:
// From convex/byok/saveCredentials.ts (saveApiKey action) const encrypted = await encryptCredential(args.apiKey); // Get existing IVs/authTags const existing = await ctx.runQuery(internal.byok.credentials.getConfigInternal, { userId: user._id, }); const ivParts = parseParts(existing?.encryptionIVs); const authTagParts = parseParts(existing?.authTags); // Update specific index const idx = KEY_INDEX[args.keyType]; ivParts[idx] = encrypted.iv; authTagParts[idx] = encrypted.authTag; // Store as colon-separated await ctx.runMutation(internal.byok.credentials.upsertConfig, { userId: user._id, [FIELD_MAP[args.keyType]]: encrypted.encrypted, encryptionIVs: ivParts.join(":"), // "v_iv:or_iv:groq_iv:dg_iv" authTags: authTagParts.join(":"), });
Decryption retrieval:
// From convex/byok/saveCredentials.ts (revalidateKey action) const existing = await ctx.runQuery(internal.byok.credentials.getConfigInternal, { userId: user._id, }); const encryptedKey = existing[FIELD_MAP[args.keyType]]; const ivParts = parseParts(existing.encryptionIVs); const authTagParts = parseParts(existing.authTags); const idx = KEY_INDEX[args.keyType]; const iv = ivParts[idx]; const authTag = authTagParts[idx]; if (!iv || !authTag) { throw new ConvexError("Unable to decrypt key. Please re-add it."); } const apiKey = await decryptCredential(encryptedKey, iv, authTag);
Database Schema
// userApiKeys table (BYOK) { userId: v.id("users"), encryptedVercelKey: v.optional(v.string()), encryptedOpenRouterKey: v.optional(v.string()), encryptedGroqKey: v.optional(v.string()), encryptedDeepgramKey: v.optional(v.string()), encryptionIVs: v.string(), // "iv1:iv2:iv3:iv4" authTags: v.string(), // "tag1:tag2:tag3:tag4" lastValidated: v.optional(v.object({...})), } // userDatabaseConfig table (BYOD) { userId: v.id("users"), encryptedDeploymentUrl: v.string(), encryptedDeployKey: v.string(), encryptionIV: v.string(), // Single IV: "urlIV:keyIV" authTags: v.string(), // Single authTags: "urlTag:keyTag" }
BYOK: 4 keys → 4 indexes → colon-separated IVs/authTags BYOD: 2 fields → colon-separated
urlIV:keyIV and urlTag:keyTag
Security Rules
-
Never log plaintext - only log key types, not values:
logger.warn("Failed to validate API key", { tag: "BYOK", keyType, // ✅ Log type error: String(error), }); // ❌ DON'T: apiKey: apiKey -
Never return encrypted credentials to client:
// From convex/byod/credentials.ts (getConfig query) // Never return encrypted credentials to client return { _id: config._id, connectionStatus: config.connectionStatus, lastConnectionTest: config.lastConnectionTest, // ❌ DON'T include: encryptedDeploymentUrl, encryptedDeployKey }; -
Use internal queries for encrypted data:
// From convex/byod/credentials.ts export const getConfigInternal = internalQuery({ args: { userId: v.id("users") }, handler: async (ctx, args): Promise<Doc<"userDatabaseConfig"> | null> => { return await ctx.db .query("userDatabaseConfig") .withIndex("by_user", (q) => q.eq("userId", args.userId)) .first(); }, }); -
Always verify authTag -
throws if tampered.decipher.final() -
Fail secure on validation errors:
// From convex/byok/saveCredentials.ts try { encrypted = await encryptCredential(args.apiKey); } catch (error) { logger.error("BYOK encryption failed", { tag: "BYOK", error: String(error) }); throw new ConvexError( "BYOK is not available right now. Please contact support.", ); }
Use Cases
BYOK (Bring Your Own Key): User-provided API keys for Vercel Gateway, OpenRouter, Groq, Deepgram
- File:
convex/byok/saveCredentials.ts - Actions:
,saveApiKey
,removeApiKeyrevalidateKey - Table:
userApiKeys
BYOD (Bring Your Own Database): User Convex deployment credentials
- File:
convex/byod/credentials.ts - Table:
userDatabaseConfig - Fields:
,encryptedDeploymentUrlencryptedDeployKey
Error Handling
// Encryption failure (missing env key) throw new ConvexError("BYOK is not available right now. Please contact support."); // Decryption failure (missing IV/authTag) throw new ConvexError("Unable to decrypt key. Please re-add it."); // Validation failure (invalid API key) throw new ConvexError("Invalid API key. Please check the key is correct.");
Key Files
- Core encrypt/decrypt functionsconvex/lib/encryption.ts
- Multi-key BYOK patternconvex/byok/saveCredentials.ts
- BYOK queries/mutationsconvex/byok/credentials.ts
- BYOD config managementconvex/byod/credentials.ts
- Field mappings, key indexesconvex/byok/constants.ts
Avoid
- ❌ Reusing IVs (always generate new with
)randomBytes(16) - ❌ Storing plaintext anywhere (logs, DB, client responses)
- ❌ Using algorithms other than
(project standard)aes-256-gcm - ❌ Forgetting
directive (encryption requires Node runtime)"use node" - ❌ Missing authTag validation (set before decrypt, verify on final)
- ❌ Hardcoding encryption keys in code (use env vars)