import { useState } from "react";
import { currentAppName, useAppName } from "@/lib/brand";
import { BadgeCheck, ShieldAlert, ShieldQuestion, ShieldX } from "lucide-react";
import { formatFingerprint } from "@/lib/smime/x509";
import type { SignatureState } from "@/lib/smime/useSignature";
import type { Reason } from "@/lib/smime/verify";
import { t, tNode } from "@/lib/i18n";
import { formatFullDate } from "@/lib/format";
/**
* What a checked signature is allowed to say on screen.
*
* The wording here is the feature. ihasmail has no certificate authority to ask
* and none is bundled, so the strong word — "verified", full stop — is never
* used: the certificate arrives inside the message, and on its own a good
* signature only shows that whoever wrote the message held the key attached to
* it. What can honestly be said is whether this is the same signer as last
* time, and that is what the banner leads with.
*
* Which means the *reassuring* case is deliberately the quiet one and the
* changed-signer case is the loud one. A green tick on a first sighting would
* be telling somebody that an unknown certificate is trustworthy because it
* verified against itself.
*/
export function SignatureBanner({ state }: { state: SignatureState }) {
const appName = useAppName();
const [open, setOpen] = useState(false);
if (state.status !== "done") return null;
const { crypto, trust, previous, warnings } = state.report;
if (crypto.kind === "none") return null;
if (crypto.kind === "unsupported") {
return (
}>
{t("This message is signed, and {app} could not check the signature.", { app: appName })} {explain(crypto.reason)}
{crypto.detail && {crypto.detail}}
);
}
if (crypto.kind === "broken") {
return (
}>
{t("This signature does not check out.")} {explain(crypto.reason)}
);
}
const name = crypto.cert.subject.commonName || crypto.cert.emails[0] || t("an unnamed signer");
const changed = trust === "changed";
const mismatch = warnings.includes("address-mismatch");
const tone = changed || mismatch ? "danger" : warnings.length > 0 ? "warn" : trust === "same-as-before" ? "good" : "quiet";
return (
: trust === "same-as-before" ? : }>
{changed ? (
<>
{t("The signer has changed.")}{" "}
{tNode("Earlier messages from this address were signed by {previous}. This one is signed by {current}.", {
previous: {previous?.name ?? t("a different certificate")},
current: {name},
})}{" "}
{t("That can mean a renewed certificate, and it can mean somebody else. Check with them by some other route before trusting it.")}
>
) : mismatch ? (
<>
{t("The signature is not for this sender.")}{" "}
{tNode("It was made with a certificate belonging to {name}, which does not cover this address.", {
name: {name},
})}
>
) : trust === "same-as-before" ? (
tNode("Signed by {name} — the same signer as before.", { name: {name} })
) : (
<>
{tNode("Signed by {name}, seen here for the first time.", { name: {name} })}{" "}
{t("{app} will tell you if a later message from this address is signed by anybody else.", { app: appName })}
>
)}
{warnings.includes("certificate-expired") && <> {t("The certificate has expired.")}>}
{warnings.includes("certificate-not-yet-valid") && <> {t("The certificate is not valid yet.")}>}
{open && (
{formatFullDate(crypto.signer.signingTime.toISOString())} {t("as claimed by the signer")}
)}
{t("Fingerprint")}
{formatFingerprint(crypto.cert.fingerprint)}
{previous && (
{t("Previous fingerprint")}
{formatFingerprint(previous.fingerprint)} {t("first seen {date}", { date: formatFullDate(previous.firstSeen) })}
)}
)}
);
}
/** The sayable version of why a check did not happen, or did not hold. */
function explain(reason: Reason): string {
const appName = currentAppName();
switch (reason) {
case "openpgp":
return t("It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.", { app: appName });
case "rsa-pss":
return t("It uses a signature algorithm {app} cannot check yet.", { app: appName });
case "no-certificate":
return t("The signature carries no certificate that can be read.");
case "not-signed-properly":
return t("The signed part is missing either the message or the signature.");
case "digest-mismatch":
return t("The message does not match what was signed — it was altered after signing, or damaged on the way.");
case "signature-mismatch":
return t("The signature does not match the certificate sent with it.");
case "other":
return t("The signature could not be read.");
}
}
function Banner({ tone, icon, children }: { tone: "good" | "warn" | "danger" | "quiet"; icon: React.ReactNode; children: React.ReactNode }) {
return (