"use client"; import { useState, useEffect, useRef } from "react"; import { useTranslations } from "next-intl"; import { Upload, Trash2, Eye, Lock, Unlock, Download, ShieldCheck, ShieldAlert, Users, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { SettingsSection, SettingItem, ToggleSwitch } from "@/components/settings/settings-section"; import { SmimePassphraseDialog } from "@/components/settings/smime-passphrase-dialog"; import { SmimeCertificateModal } from "@/components/settings/smime-certificate-modal"; import { useSmimeStore } from "@/stores/smime-store"; import { useIdentityStore } from "@/stores/identity-store"; import { useAuthStore } from "@/stores/auth-store"; import { exportPkcs12, downloadPkcs12 } from "@/lib/smime/pkcs12-export"; import type { SmimeKeyRecord, SmimePublicCert } from "@/lib/smime/types"; export function SmimeSettings() { const t = useTranslations("smime"); const { keyRecords, publicCerts, identityKeyBindings, defaultSignIdentity, defaultEncrypt, rememberUnlockedKeys, autoImportSignerCerts, isLoading, error, load, importPKCS12, removeKeyRecord, removePublicCert, bindIdentityToKey, unlockKey, lockKey, setSignDefault, setEncryptDefault, setRememberUnlockedKeys, setAutoImportSignerCerts, isKeyUnlocked, setError, } = useSmimeStore(); const { identities } = useIdentityStore(); const activeAccountId = useAuthStore((s) => s.activeAccountId); // Local UI state const [importDialogOpen, setImportDialogOpen] = useState(false); const [unlockDialogOpen, setUnlockDialogOpen] = useState(false); const [unlockTargetId, setUnlockTargetId] = useState(null); const [certModalRecord, setCertModalRecord] = useState(null); const [certModalType, setCertModalType] = useState<"private" | "public">("private"); const [importError, setImportError] = useState(null); const [unlockError, setUnlockError] = useState(null); const [pendingFile, setPendingFile] = useState(null); const [pendingP12Pass, setPendingP12Pass] = useState(""); const fileInputRef = useRef(null); const pubCertInputRef = useRef(null); // State for the two-step PKCS#12 flow const [importStep, setImportStep] = useState<"p12" | "storage">("p12"); // Export flow state const [exportDialogOpen, setExportDialogOpen] = useState(false); const [exportTargetRecord, setExportTargetRecord] = useState(null); const [exportStep, setExportStep] = useState<"storage" | "export">("storage"); const [exportStoragePass, setExportStoragePass] = useState(""); const [exportError, setExportError] = useState(null); useEffect(() => { load(activeAccountId ?? undefined); }, [load, activeAccountId]); // ── PKCS#12 import flow ──────────────────────────────────────── const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { setPendingFile(reader.result as ArrayBuffer); setImportStep("p12"); setImportError(null); setImportDialogOpen(true); }; reader.readAsArrayBuffer(file); // Reset so same file can be re-selected e.target.value = ""; }; const handleImportSubmit = async (passphrase: string) => { if (importStep === "p12") { setPendingP12Pass(passphrase); setImportStep("storage"); setImportError(null); return; } // Storage passphrase step if (!pendingFile) return; try { await importPKCS12(pendingFile, pendingP12Pass, passphrase); setImportDialogOpen(false); setPendingFile(null); setPendingP12Pass(""); setImportError(null); } catch (err) { setImportError(err instanceof Error ? err.message : "Import failed"); } }; // ── Public cert import ───────────────────────────────────────── const handlePublicCertFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = async () => { try { const store = useSmimeStore.getState(); await store.importPublicCert(reader.result as ArrayBuffer, "manual"); } catch (err) { setError(err instanceof Error ? err.message : "Failed to import certificate"); } }; reader.readAsArrayBuffer(file); e.target.value = ""; }; // ── Unlock ───────────────────────────────────────────────────── const handleUnlockRequest = (id: string) => { setUnlockTargetId(id); setUnlockError(null); setUnlockDialogOpen(true); }; const handleUnlockSubmit = async (passphrase: string) => { if (!unlockTargetId) return; try { await unlockKey(unlockTargetId, passphrase); setUnlockDialogOpen(false); setUnlockTargetId(null); setUnlockError(null); } catch (err) { setUnlockError(err instanceof Error ? err.message : "Unlock failed"); } }; // ── Export flow ──────────────────────────────────────────────── const handleExportRequest = (record: SmimeKeyRecord) => { setExportTargetRecord(record); setExportStep("storage"); setExportStoragePass(""); setExportError(null); setExportDialogOpen(true); }; const handleExportSubmit = async (passphrase: string) => { if (!exportTargetRecord) return; if (exportStep === "storage") { // Verify storage passphrase by attempting to decrypt try { const { decryptPrivateKeyBytes } = await import("@/lib/smime/pkcs12-import"); await decryptPrivateKeyBytes(exportTargetRecord, passphrase); setExportStoragePass(passphrase); setExportStep("export"); setExportError(null); } catch { setExportError(t("incorrect_passphrase")); } return; } // Export passphrase step try { const p12Bytes = await exportPkcs12(exportTargetRecord, exportStoragePass, passphrase); const filename = `${exportTargetRecord.email.replace(/[^a-zA-Z0-9.-]/g, '_')}.p12`; downloadPkcs12(p12Bytes, filename); setExportDialogOpen(false); setExportTargetRecord(null); setExportStoragePass(""); setExportError(null); } catch (err) { setExportError(err instanceof Error ? err.message : "Export failed"); } }; // ── Helpers ──────────────────────────────────────────────────── const isExpired = (dateStr: string) => new Date(dateStr) < new Date(); const formatDate = (dateStr: string) => { try { return new Date(dateStr).toLocaleDateString(); } catch { return dateStr; } }; const getBoundIdentityNames = (keyId: string): string[] => { return Object.entries(identityKeyBindings) .filter(([, kId]) => kId === keyId) .map(([identityId]) => { const identity = identities.find((i) => i.id === identityId); return identity?.email ?? identityId; }); }; return (
{error && (
{error}
)} {/* ── Your Certificates ──────────────────────────────────── */}
{keyRecords.map((record) => { const expired = isExpired(record.notAfter); const unlocked = isKeyUnlocked(record.id); const boundIdentities = getBoundIdentityNames(record.id); return (
{expired ? ( ) : ( )}

{record.email || record.subject}

{record.issuer} · {t("expires")} {formatDate(record.notAfter)} {expired && ({t("expired")})}

{boundIdentities.length > 0 && (

{t("bound_to")}: {boundIdentities.join(", ")}

)}
{unlocked ? ( ) : ( )}
); })} {keyRecords.length === 0 && !isLoading && (

{t("no_certificates")}

)}
{/* ── Recipient Certificates ─────────────────────────────── */}
{publicCerts.map((cert) => { const expired = isExpired(cert.notAfter); return (

{cert.email || cert.subject}

{cert.issuer} · {cert.source} {expired && ({t("expired")})}

); })} {publicCerts.length === 0 && !isLoading && (

{t("no_recipient_certs")}

)}
{/* ── Identity Bindings ──────────────────────────────────── */} {identities.length > 0 && keyRecords.length > 0 && ( {identities.map((identity) => { const boundKeyId = identityKeyBindings[identity.id]; return ( ); })} )} {/* ── Defaults ───────────────────────────────────────────── */} {identities.map((identity) => { const bound = identityKeyBindings[identity.id]; if (!bound) return null; return ( setSignDefault(identity.id, v)} /> ); })} {/* ── Dialogs ────────────────────────────────────────────── */} { setImportDialogOpen(false); setPendingFile(null); setPendingP12Pass(""); setImportError(null); setImportStep("p12"); }} onSubmit={handleImportSubmit} title={importStep === "p12" ? t("enter_p12_passphrase") : t("enter_storage_passphrase")} description={importStep === "p12" ? t("p12_passphrase_desc") : t("storage_passphrase_desc")} submitText={importStep === "p12" ? t("next") : t("import")} error={importError} showConfirm={importStep === "storage"} /> { setUnlockDialogOpen(false); setUnlockTargetId(null); setUnlockError(null); }} onSubmit={handleUnlockSubmit} title={t("unlock_key")} description={t("unlock_key_desc")} error={unlockError} /> setCertModalRecord(null)} record={certModalRecord} type={certModalType} /> { setExportDialogOpen(false); setExportTargetRecord(null); setExportStoragePass(""); setExportError(null); setExportStep("storage"); }} onSubmit={handleExportSubmit} title={exportStep === "storage" ? t("enter_storage_passphrase") : t("enter_export_passphrase")} description={exportStep === "storage" ? t("export_storage_desc") : t("export_passphrase_desc")} submitText={exportStep === "storage" ? t("next") : t("export")} error={exportError} showConfirm={exportStep === "export"} />
); }