'use client'; import { useEffect, useState } from 'react'; import { apiFetch } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/cn'; interface KeyRow { version: number; active: boolean; createdAt: string; retiredAt: string | null; } interface Status { activeVersion: number | null; keyCount: number; secretCount: number; legacySecretCount: number; keys: KeyRow[]; } export default function AdminEncryptionPage() { const [status, setStatus] = useState(null); const [rotating, setRotating] = useState(false); const [message, setMessage] = useState(null); async function reload() { setStatus(await apiFetch('/v1/admin/encryption')); } useEffect(() => { reload(); }, []); async function rotate() { if ( !confirm( 'Rotate the encryption key?\n\nA fresh Data Encryption Key is generated and EVERY stored secret is re-encrypted under it in one transaction. The environment KEK is untouched. This is safe to run any time you suspect key compromise.', ) ) { return; } setRotating(true); setMessage(null); try { const r = await apiFetch<{ newVersion: number; reEncrypted: number }>( '/v1/admin/encryption/rotate', { method: 'POST', body: '{}' }, ); setMessage(`Rotated to key v${r.newVersion} — ${r.reEncrypted} secret(s) re-encrypted.`); await reload(); } catch (e) { const detail = (e as { detail?: { detail?: string; error?: string } }).detail; setMessage(`Rotation failed: ${detail?.detail ?? detail?.error ?? (e as Error).message}`); } finally { setRotating(false); } } return (

Encryption

Envelope encryption for customer secrets. The KEK lives only in the environment; Data Encryption Keys are stored wrapped and rotated here.

{!status &&
Loading…
} {status && ( <>
0 ? 'rotate once to migrate them onto a DEK' : 'all on a managed DEK' } />

Rotate encryption key

Generates a new DEK and re-encrypts all {status.secretCount} secret(s) under it atomically. The environment KEK is never exposed or changed.

{message && (

{message}

)}

Key history

{status.keys.map((k) => ( ))}
Version Status Created Retired
v{k.version} {k.active ? 'active' : 'retired'} {new Date(k.createdAt).toLocaleString()} {k.retiredAt ? new Date(k.retiredAt).toLocaleString() : '—'}

How it works: a 32-byte Data Encryption Key (DEK) is generated, AES-256-GCM encrypted with the environment Key Encryption Key (KEK = SECRETS_ENCRYPTION_KEY), and stored wrapped. Secrets are encrypted with the DEK. Rotation mints a fresh DEK, re-encrypts every secret, and retires the old one — recoverable from a suspected DEK compromise without ever touching the KEK.

)}
); } function Card({ label, value, sub }: { label: string; value: string; sub?: string }) { return (
{label}
{value}
{sub &&
{sub}
}
); }