diff options
author | João Augusto Costa Branco Marado Torres <torres.dev@disroot.org> | 2025-08-05 18:50:37 +0100 |
---|---|---|
committer | João Augusto Costa Branco Marado Torres <torres.dev@disroot.org> | 2025-08-05 18:50:37 +0100 |
commit | 0af094770c4ebabc56ff761a8bd215bc397c0f7e (patch) | |
tree | a9ad669c8b84b4d13897732ed93ccfcbbeb2cb25 /src/components/signature | |
parent | 84eef3f848c4efa18985a776021a58720744523a (diff) |
refactor: reading page review
Signed-off-by: João Augusto Costa Branco Marado Torres <torres.dev@disroot.org>
Diffstat (limited to 'src/components/signature')
-rw-r--r-- | src/components/signature/Authors.astro | 275 | ||||
-rw-r--r-- | src/components/signature/Commit.astro | 86 | ||||
-rw-r--r-- | src/components/signature/Downloads.astro | 63 | ||||
-rw-r--r-- | src/components/signature/Signature.astro | 44 | ||||
-rw-r--r-- | src/components/signature/Summary.astro | 279 |
5 files changed, 0 insertions, 747 deletions
diff --git a/src/components/signature/Authors.astro b/src/components/signature/Authors.astro deleted file mode 100644 index 4e52d4e..0000000 --- a/src/components/signature/Authors.astro +++ /dev/null @@ -1,275 +0,0 @@ ---- -import { toPK } from "@lib/pgp"; -import { createKeyFromArmor } from "@lib/pgp/create"; -import type { Verification } from "@lib/pgp/verify"; -import { defined, get, instanciate } from "@utils/anonymous"; -import { z } from "astro:content"; -import type { EntityTypesEnum } from "src/consts"; -import qrcode from "yaqrcode"; -import type { getSigners } from "@lib/collection/helpers"; - -interface Props { - verifications: NonNullable<Verification["verifications"]>; - expectedSigners: Awaited<ReturnType<typeof getSigners>>; - commitSignerKey?: string; -} - -const { - verifications: verificationsPromise, - expectedSigners, - commitSignerKey, -} = Astro.props; - -const fingerprintToData = new Map< - string, - { websites: URL[]; role: z.infer<typeof EntityTypesEnum> } ->(); - -for (const { entity, role } of expectedSigners) { - const key = await createKeyFromArmor(entity.data.publickey.armor); - const fingerprint = key.getFingerprint(); - fingerprintToData.set(fingerprint, { - websites: entity.data.websites?.map(instanciate(URL)) ?? [], - role, - }); -} - -let verifications = await Promise.all( - verificationsPromise.map(async ({ key, keyID, userID, verified }) => { - return { - key: await key, - keyID, - userID: await userID, - verified: await verified.catch(() => false), - }; - }), -); - -const expectedKeys = await Promise.all( - expectedSigners.map(get("entity")).map(({ data }) => - createKeyFromArmor(data.publickey.armor) - ), -); - -const expectedFingerprints = new Set( - expectedKeys.map((key) => key.getFingerprint()), -); - -const verifiedFingerprints = new Set( - verifications.map((v) => v.key).filter(defined).map(toPK).map((key) => - key.getFingerprint() - ), -); - -if (!expectedFingerprints.isSubsetOf(verifiedFingerprints)) { - throw new Error( - `Missing signature from expected signers: ${[ - ...expectedFingerprints.difference(verifiedFingerprints).values(), - ]}`, - ); -} ---- - -<div> - <table> - <caption> - <strong>Assinaturas</strong> - <p> - Para verificar uma assinatura é necessário a <a href="#message" - >mensagem</a>, a <a href="#signature">assinatura digital</a> e as <em - >chaves públicas</em> dos assinantes. Esta tabela mostra algumas - informações sobre os assinantes e as suas chaves públicas. - </p> - </caption> - <colgroup> - <col /> - <col /> - </colgroup> - <colgroup> - <col /> - <col /> - <col /> - </colgroup> - <thead> - <tr> - <th scope="col">Assinante</th> - <th scope="col">Função</th> - <th scope="col">Fingerprint</th> - <th scope="col">Válido</th> - <th scope="col">Commiter</th> - </tr> - </thead> - <tbody> - { - verifications.map(({ userID, key, keyID, verified }) => { - const fingerprint = key - ? toPK(key).getFingerprint() - : undefined; - const info = fingerprint - ? fingerprintToData.get(fingerprint) - : undefined; - const primary = userID?.[0]; - let role = ""; - switch (info?.role) { - case "author": { - role = "Autor"; - break; - } - case "co-author": { - role = "Co-autor"; - break; - } - case "translator": { - role = "Tradutor"; - break; - } - } - return ( - <tr> - <th scope="row"> - <address - itemprop="author" - itemscope - itemtype="https://schema.org/Person" - > - { - primary?.name - ? info?.websites[0] ? ( - <a - itemprop="url" - rel="author external noreferrer" - target="_blank" - href={info.websites[0]} - ><span itemprop="name">{primary.name}</span></a> - ) : <span itemprop="name">{primary.name}</span> - : primary?.email - ? ( - <><<a - itemprop="email" - rel="author external noreferrer" - target="_blank" - href={primary?.email && `mailto:${primary.email}`} - >{primary?.email}</a>></> - ) - : "" - } - { - primary && ( - <> - <button - popovertarget={`user-id-${fingerprint}`} - class="emoji" - > - ➕ - </button> - <section - class="user-id" - popover - id={`user-id-${fingerprint}`} - > - { - userID && ( - <><p><code>UserID</code>s</p><ul> - {userID.map((x) => <li>{x.userID}</li>)} - </ul></> - ) - } - { - info?.websites && ( - <><p>Websites</p><ul> - { - info.websites.map(( - x, - ) => <li><a href={x}>{x}</a></li>) - } - </ul></> - ) - } - </section> - </> - ) - } - </address> - </th> - <td>{role}</td> - <td> - <><span title={fingerprint?.replace(/(....)/g, "$1 ")}>{ - key - ? "0x" + toPK(key).getKeyID().toHex() - : "0x" + keyID.toHex() - }</span> - { - key && false && ( - <img - src={qrcode(toPK(key).armor(), { - typeNumber: 40, - errorCorrectLevel: "L", - })} - /> - ) - } - { - key && - ( - <button popovertarget={`armor-${fingerprint}`}> - Armor - </button> - <section class="armor" popover id={`armor-${fingerprint}`}> - <pre><code>{toPK(key).armor()}</code></pre> - </section> - ) - } - </> - </td> - <td>{verified ? "✅" : "❌"}</td> - <td> - { - commitSignerKey && - key?.getFingerprint().toUpperCase()?.endsWith( - commitSignerKey.toUpperCase(), - ) && "✅" - } - </td> - </tr> - ); - }) - } - </tbody> - </table> -</div> - -<style> - div { - overflow-x: auto; - } - - table { - table-layout: fixed; - border-collapse: collapse; - border: 3px solid; - margin-inline: auto; - max-width: 90svw; - } - - th, - td { - padding: 1rem; - text-align: center; - } - - tbody tr:nth-child(odd) { - background-color: #e7e7e7; - } - - section[popover] { - text-align: initial; - } - section[popover].armor { - max-height: calc(200dvh / 3); - } - @media (prefers-color-scheme: dark) { - tbody tr:nth-child(odd) { - background-color: #181818; - } - } -</style> diff --git a/src/components/signature/Commit.astro b/src/components/signature/Commit.astro deleted file mode 100644 index 328a8f9..0000000 --- a/src/components/signature/Commit.astro +++ /dev/null @@ -1,86 +0,0 @@ ---- -import { gitDir } from "@lib/git"; -import type { Commit } from "@lib/git/types"; -import { toIso8601Full } from "@utils/datetime"; - -type Props = { commit: Commit; lang: string }; - -const dir = await gitDir(); -const { hash, files, author, committer, signature } = Astro.props.commit; - -const formatter = new Intl.DateTimeFormat([Astro.props.lang], { - dateStyle: "short", - timeStyle: "short", -}); ---- - -<section> - <details> - <summary> - Informações sobre o último commit que modificou ficheiros relacionados a - este blog post: - </summary> - <dl class="divider"> - <dt>Hash</dt> - <dd><samp title={hash.long}>0x{hash.short.toUpperCase()}</samp></dd> - <dt>Ficheiros modificados</dt> - { - files.length > 0 - ? files.map((file) => ( - <dd><samp>{file.path.pathname.replace(dir.pathname, "")}</samp></dd> - )) - : <dd>Nenhum ficheiro modificado</dd> - } - <dt> - Autor (<time datetime={toIso8601Full(author.date)}>{ - formatter.format(author.date) - }</time>) - </dt> - <dd> - {author.name} <<a href={`mailto:${author.email}`}>{ - author.email - }</a>> - </dd> - <dt> - Commiter (<time datetime={toIso8601Full(committer.date)}>{ - formatter.format(committer.date) - }</time>) - </dt> - <dd> - {committer.name} <<a href={`mailto:${committer.email}`}>{ - committer.email - }</a>> - </dd> - { - signature && - ( - <dt>Assinatura do commit</dt> - <dd> - <dl> - <dt>Tipo</dt> - <dd><samp>{signature.type}</samp></dd> - <dt>Assinante</dt> - <dd>{signature.signer}</dd> - <dt>Fingerprint da chave</dt> - <dd><samp>0x{signature.key.short}</samp></dd> - </dl> - </dd> - ) - } - </dl> - </details> -</section> - -<style> - section { - font-size: smaller; - } - - dl { - margin-block: 0; - } - - details { - padding-block: 1rem; - } -</style> diff --git a/src/components/signature/Downloads.astro b/src/components/signature/Downloads.astro deleted file mode 100644 index 3497b37..0000000 --- a/src/components/signature/Downloads.astro +++ /dev/null @@ -1,63 +0,0 @@ ---- -import { gitDir } from "@lib/git"; -import { get } from "@utils/anonymous"; - -interface Props { - lang: string; -} - -const { lang } = Astro.props; - -let source = new URL( - `${Astro.url.href.replace(/\/$/, "")}.md`, -); - -const dir = await gitDir(); - -const format: Intl.NumberFormatOptions = { - notation: "compact", - style: "unit", - unit: "byte", - unitDisplay: "narrow", -}; - -const formatter = new Intl.NumberFormat(lang, format); - -const sourceSize = formatter.format( - await Deno.stat( - new URL("public" + source.pathname, dir), - ).then(get("size")), -); -const sig = await Deno.stat( - new URL("public" + source.pathname + ".sig", dir), -).then(get("size")).catch(() => undefined); -const sigSize = formatter.format(sig); ---- - -<section> - <p>Ficheiros para descarregar:</p> - <dl> - <dt>Blog post</dt> - <dd> - <a - id="message" - href={source} - download - type="text/markdown; charset=utf-8" - ><samp>text/markdown</samp>, <samp>{sourceSize}</samp></a> - </dd> - { - sig && ( - <dt>Assinatura digital</dt> - <dd> - <a - id="signature" - href={`${source}.sig`} - download - type="application/pgp-signature" - ><samp>application/pgp-signature</samp>, <samp>{sigSize}</samp></a> - </dd> - ) - } - </dl> -</section> diff --git a/src/components/signature/Signature.astro b/src/components/signature/Signature.astro deleted file mode 100644 index 57e9902..0000000 --- a/src/components/signature/Signature.astro +++ /dev/null @@ -1,44 +0,0 @@ ---- -import type { Verification } from "@lib/pgp/verify"; -import Summary from "./Summary.astro"; -import Downloads from "./Downloads.astro"; -import Commit from "./Commit.astro"; - -interface Props { - verification: Verification; - lang: string; -} - -const { verification, lang } = Astro.props; -const commit = await verification.commit; ---- - -<aside id="signatures"> - <p><strong>Verificação da assinatura digital</strong></p> - <Summary {...verification} /> - <Downloads {lang} /> - {commit && <Commit {commit} {lang} />} -</aside> - -<style is:global> - #signatures > section > p:first-child { - font-weight: bolder; - } -</style> -<style> - #signatures { - margin-inline: 1.5rem; - margin-block-end: 1.5rem; - box-shadow: 0 0 calc(1em) #e7e7e7; - border-radius: calc(1rem / 3); - padding: 1rem; - } - - #signatures > p:first-child { - font-size: larger; - - & > strong { - font-weight: bolder; - } - } -</style> diff --git a/src/components/signature/Summary.astro b/src/components/signature/Summary.astro deleted file mode 100644 index f25c1d1..0000000 --- a/src/components/signature/Summary.astro +++ /dev/null @@ -1,279 +0,0 @@ ---- -import { - createVerificationSummary, - logLevel, - type Summary, - VerificationResult, -} from "@lib/pgp/summary"; -import type { Verification } from "@lib/pgp/verify"; -import { Level } from "@utils/index"; -import type { NonEmptyArray } from "@utils/iterator"; - -interface Props extends Verification {} - -let [errors, keys] = await createVerificationSummary(Astro.props); -const failed = errors.filter((summary) => "reason" in summary); - -if (failed.length > 0) { - errors = failed as NonEmptyArray<Summary>; -} - -let worst; - -for (const summary of errors) { - if (worst === undefined) { - worst = summary; - } - - const { result } = summary; - const a = logLevel(worst.result); - const b = logLevel(result); - if (a[0] === b[0] && !a[1] && b[1]) { - worst = summary; - } else if (b[0] === Level.ERROR) { - worst = summary; - } else if (a[0] === Level.OK && b[0] === Level.WARN) { - worst = summary; - } -} - -let lvl: [Level, boolean] | undefined = undefined; - -let label; - -let title = ""; -let content; -const error = worst && "reason" in worst ? worst.reason : undefined; - -if (worst) { - lvl = logLevel(worst.result); - switch (lvl[0]) { - case Level.OK: { - label = "OK"; - break; - } - case Level.WARN: { - label = "Aviso"; - break; - } - case Level.ERROR: { - label = "Erro"; - break; - } - default: { - throw new Error("Unreachable"); - } - } - - switch (worst.result) { - case VerificationResult.NO_SIGNATURE: { - title = "Assinatura não encontrada"; - content = `<p> -Este blog post não foi assinado. -</p> -<p> -<strong>Não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito</strong>. -</p> -`; - break; - } - case VerificationResult.MISSING_KEY: { - title = "Chave não encontrada"; - content = `<p> -Este blog post está assinado digitalmente, porém a chave pública com <code>KeyID</code> <samp>0x${worst.keyID}</samp> com que foi assinado não foi encontrada no chaveiro sendo <strong>impossível verificar a assinatura, quer dizer, não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito</strong>. -</p> -<p> -Procure a chave noutro sítio da internet para conseguir fazer a verificação manualmente. -</p> -`; - break; - } - case VerificationResult.SIGNATURE_CORRUPTED: { - title = "Assinatura corrumpida"; - content = `<p> -Exite um ficheiro que supostamente é a assinatura, mas ele está corrompido ou com um formato inválido. -</p> -<p> -<strong>Não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito</strong>. -</p> -`; - break; - } - case VerificationResult.SIGNATURE_COULD_NOT_BE_CHECKED: { - title = "Erro desconhecido"; - content = `<p> -A assinatura foi encontrada mas ocorreu um erro inesperado durante a verificação. -</p> -<p> -<strong>Não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito</strong>. -</p> -`; - break; - } - case VerificationResult.BAD_SIGNATURE: { - title = "Assinatura inválida"; - content = `<p> -Existe uma assinatura digital porém o conteúdo da blog post não corresponde à assinatura. Talvez o texto tenha sido alterado sem ter sido criada uma nova assinatura. -</p> -<p> -Pode tentar verificar a assinatura com versões antigas do blog post, mas esta versão <strong> não pode ser verificada quanto à autentacidade do autor ou à integridade do texto escrito</strong>. -</p> -`; - break; - } - case VerificationResult.UNTRUSTED_KEY: { - title = "Assinatura válida (chave não confiada)"; - content = `<p> -A assinatura digital é criptograficamente válida, porém a chave utilizada não é suficientemente confiada pelo servidor. Mas podes ter a certeza que <strong>o dono da chave pública é a mesma pessoa que assinou este blog post</strong>. -</p> -`; - break; - } - case VerificationResult.TRUSTED_KEY: { - title = "Assinatura válida"; - content = `<p> -A assinatura digital é criptograficamente válida. <strong>O dono da chave pública é a mesma pessoa que assinou este blog post exatamente como ele está, sem alterações</strong>. -</p> -`; - break; - } - case VerificationResult.EXPIRATION_AFTER_SIGNATURE: { - break; - } - case VerificationResult.EXPIRATION_BEFORE_SIGNATURE: { - break; - } - case VerificationResult.REVOCATION_AFTER_SIGNATURE: { - break; - } - case VerificationResult.REVOCATION_BEFORE_SIGNATURE: { - break; - } - case VerificationResult.KEY_DOES_NOT_SIGN: { - break; - } - default: { - throw new Error("Unreachable"); - } - } -} ---- - -{ - lvl && - ( - <details - class:list={{ - ok: lvl[0] === Level.OK, - warn: lvl[0] === Level.WARN, - error: lvl[0] === Level.ERROR, - super: lvl[1], - }} - > - <summary>{label?.toUpperCase()}: {title.toUpperCase()}</summary> - <Fragment set:html={content} /> - {error && <pre><samp>{error}</samp></pre>} - </details> - ) -} - -<style> - pre { - overflow-x: auto; - } - details { - &.error { - --bg: #fff; - --fg: var(--color-active); - - &.super { - --bg: var(--color-active); - --fg: #fff; - } - } - - &.warn { - --bg: #fff; - --fg: #f46d43; - - &.super { - --bg: #f46d43; - --fg: #fff; - } - } - - &.ok { - --bg: #fff; - --fg: var(--color-visited); - - &.super { - --bg: var(--color-visited); - --fg: #fff; - } - } - - padding-inline: 0.5em; - padding-block: 0.5em; - - & > summary { - background-color: var(--bg); - padding-inline: 0.5em; - padding-block: calc(1em / 3); - color: var(--fg); - border-color: var(--fg); - border-width: 1px; - border-style: solid; - border-radius: calc(1em / 3); - font-weight: bolder; - - &:focus { - outline-color: var(--fg); - } - - &::marker { - color: var(--fg); - } - } - - & > :not(summary) { - padding-inline: 1em; - /* font-size: smaller; */ - } - - & > summary + * { - margin-block: 0.5em; - padding-block-start: 1em; - border-block-start: 1px solid var(--fg); - } - } - - @media (prefers-color-scheme: dark) { - details { - &.error { - --bg: #000; - - &.super { - --fg: #000; - } - } - - &.warn { - --bg: #000; - --fg: #f46d43; - - &.super { - --bg: #f46d43; - --fg: #000; - } - } - - &.ok { - --bg: #000; - - &.super { - --fg: #000; - } - } - } - } -</style> |