diff --git a/src/lib/components/filesharing/UnsignedConfirmModal.svelte b/src/lib/components/filesharing/UnsignedConfirmModal.svelte new file mode 100644 index 0000000..8852d02 --- /dev/null +++ b/src/lib/components/filesharing/UnsignedConfirmModal.svelte @@ -0,0 +1,197 @@ + + + + + + + + diff --git a/src/lib/components/filesharing/verifiedAttributes.test.ts b/src/lib/components/filesharing/verifiedAttributes.test.ts index 1d16518..29ea33b 100644 --- a/src/lib/components/filesharing/verifiedAttributes.test.ts +++ b/src/lib/components/filesharing/verifiedAttributes.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from 'vitest' -import { verifiedAttributesFor } from './verifiedAttributes' +import { + isUnsignedSender, + isWeakSenderIdentity, + verifiedAttributesFor, +} from './verifiedAttributes' import type { FriendlySender } from '@e4a/pg-js' const sender = (attrs: { type: string; value?: string }[]): FriendlySender => ({ @@ -91,3 +95,102 @@ describe('verifiedAttributesFor', () => { expect(result).toEqual([]) }) }) + +describe('isWeakSenderIdentity', () => { + it('does not treat a missing sender as email-only weak', () => { + // An unsigned file has no email to caveat, so the email-only + // warning copy does not apply — flagging it would be inaccurate. + expect(isWeakSenderIdentity(null)).toBe(false) + expect(isWeakSenderIdentity(undefined)).toBe(false) + }) + + it('does not treat a sender with no verified email as email-only weak', () => { + // No email and nothing else disclosed: there is no email claim to + // warn about, so this is not the email-only case. + expect(isWeakSenderIdentity(sender([]))).toBe(false) + }) + + it('treats an email-only sender as weak', () => { + expect( + isWeakSenderIdentity( + sender([ + { + type: 'pbdf.sidn-pbdf.email.email', + value: 'a@b.com', + }, + ]) + ) + ).toBe(true) + }) + + it('treats a sender with any verified non-email attribute as strong', () => { + expect( + isWeakSenderIdentity( + sender([ + { + type: 'pbdf.sidn-pbdf.email.email', + value: 'a@b.com', + }, + { + type: 'pbdf.gemeente.personalData.fullname', + value: 'R.A. Hensen', + }, + ]) + ) + ).toBe(false) + }) + + it('ignores non-email attributes whose value is empty', () => { + // An attribute type without a value carries no signal — it must + // not flip the gate from "weak" to "strong". + expect( + isWeakSenderIdentity( + sender([ + { + type: 'pbdf.sidn-pbdf.email.email', + value: 'a@b.com', + }, + { + type: 'pbdf.gemeente.personalData.fullname', + value: undefined, + }, + ]) + ) + ).toBe(true) + }) +}) + +describe('isUnsignedSender', () => { + it('treats a missing sender as unsigned', () => { + expect(isUnsignedSender(null)).toBe(true) + expect(isUnsignedSender(undefined)).toBe(true) + }) + + it('treats a sender with no verified email as unsigned', () => { + expect(isUnsignedSender(sender([]))).toBe(true) + }) + + it('does not treat an email-only sender as unsigned', () => { + expect( + isUnsignedSender( + sender([ + { type: 'pbdf.sidn-pbdf.email.email', value: 'a@b.com' }, + ]) + ) + ).toBe(false) + }) + + it('does not treat a fully verified sender as unsigned', () => { + expect( + isUnsignedSender( + sender([ + { type: 'pbdf.sidn-pbdf.email.email', value: 'a@b.com' }, + { + type: 'pbdf.gemeente.personalData.fullname', + value: 'R.A. Hensen', + }, + ]) + ) + ).toBe(false) + }) +}) diff --git a/src/lib/components/filesharing/verifiedAttributes.ts b/src/lib/components/filesharing/verifiedAttributes.ts index 4f6bca9..fdb1d4b 100644 --- a/src/lib/components/filesharing/verifiedAttributes.ts +++ b/src/lib/components/filesharing/verifiedAttributes.ts @@ -23,3 +23,24 @@ export function verifiedAttributesFor( value: a.value as string, })) } + +/** A sender that verified an email address but disclosed nothing beyond + * it is a "weak" identity claim — anyone who controls the mailbox could + * have signed. The download-page trust gate uses this to escalate the + * warning. A missing sender (an unsigned file) is not "weak email-only": + * there is no email to caveat, so the email-only warning does not apply. */ +export function isWeakSenderIdentity( + sender: FriendlySender | null | undefined +): boolean { + return !!sender?.email && verifiedAttributesFor(sender).length === 0 +} + +/** A file with no verifiable sender at all: it was not signed, or the + * signature carries no email (the public signing identity). This is the + * weakest case — there is no identity claim to evaluate — so the download + * gate shows the strongest warning and time-locks the download button. */ +export function isUnsignedSender( + sender: FriendlySender | null | undefined +): boolean { + return !sender?.email +} diff --git a/src/lib/global.scss b/src/lib/global.scss index 151a5fa..460cbbc 100644 --- a/src/lib/global.scss +++ b/src/lib/global.scss @@ -70,6 +70,8 @@ body { --pg-input-hover: #45545e; --pg-input-active: #2b343b; --pg-input-error: #b61616; + --pg-warning: #c2710c; + --pg-success: #1f7a4d; --pg-on-primary: #ffffff; --pg-disabled-background: #6b7d8a; --pg-disabled-foreground: #ffffff; @@ -93,6 +95,8 @@ body { --pg-input-hover: #98a8b3; --pg-input-active: #c4cdd4; --pg-input-error: #de3030; + --pg-warning: #f5a623; + --pg-success: #3ea674; .invert { filter: invert(100%); diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index f542d50..455add5 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -170,8 +170,18 @@ "askDownloadText": "Press the button below to start decrypting and downloading your files", "doneMessage": "Your files are being downloaded and decrypted", "doneMessageComplete": "Your files have been downloaded and decrypted", + "readyToDownload": "Your files have been decrypted and are ready to download", "verifiedEmail": "The files are from", - "verifiedExtra": "The files were signed using", + "trustHeader": "Do you trust this sender?", + "trustWarnEmailOnly": "The sender only verified an email address. Anyone with access to that mailbox could have signed these files. Check that the address truly belongs to who you expect, ideally via a channel other than email, before downloading.", + "trustWarnUnsigned": "This file is not signed. There is no verified sender at all, so anyone could have created it and there is no way to confirm where it came from. Only continue if you are certain you trust the source.", + "trustConfirmHeader": "Download an unsigned file?", + "trustConfirmAccept": "Download anyway", + "trustConfirmCancel": "Cancel", + "trustAccept": "Download files", + "trustDecline": "Don't download", + "discardedTitle": "Files discarded", + "discardedBody": "The files were not downloaded. If you want them after all, ask the sender to share again.", "notFoundTitle": "These files no longer exist", "notFoundSubtitle": "The link may have expired, or there may be an error in it.", "notFoundMessage": "Ask the sender to send the files once more.", diff --git a/src/lib/locales/nl.json b/src/lib/locales/nl.json index 0067ebe..e5dbdd5 100644 --- a/src/lib/locales/nl.json +++ b/src/lib/locales/nl.json @@ -170,8 +170,18 @@ "askDownloadText": "Druk op onderstaande knop om de bestanden te ontsleutelen en downloaden.", "doneMessage": "Je bestanden worden gedownload en ontsleuteld", "doneMessageComplete": "Je bestanden zijn gedownload en ontsleuteld", + "readyToDownload": "Je bestanden zijn ontsleuteld en klaar om te downloaden", "verifiedEmail": "De bestanden komen van", - "verifiedExtra": "De bestanden zijn ondertekend met", + "trustHeader": "Vertrouw je deze afzender?", + "trustWarnEmailOnly": "De afzender heeft alleen een e-mailadres geverifieerd. Iedereen met toegang tot die mailbox kan deze bestanden hebben ondertekend. Controleer of het adres echt van degene is die je verwacht, bij voorkeur via een ander kanaal dan e-mail, voordat je downloadt.", + "trustWarnUnsigned": "Dit bestand is niet ondertekend. Er is geen geverifieerde afzender, dus iedereen kan het gemaakt hebben en er is geen manier om te bevestigen waar het vandaan komt. Ga alleen verder als je zeker weet dat je de bron vertrouwt.", + "trustConfirmHeader": "Een niet-ondertekend bestand downloaden?", + "trustConfirmAccept": "Toch downloaden", + "trustConfirmCancel": "Annuleren", + "trustAccept": "Bestanden downloaden", + "trustDecline": "Niet downloaden", + "discardedTitle": "Bestanden weggegooid", + "discardedBody": "De bestanden zijn niet gedownload. Wil je ze toch ontvangen, vraag de afzender ze opnieuw te delen.", "notFoundTitle": "Deze bestanden bestaan niet (meer)", "notFoundSubtitle": "Het kan zijn dat de link is verlopen, of dat er een fout in zit.", "notFoundMessage": "Vraag de verzender de bestanden nog een keer te versturen.", diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 300ab01..5e923a7 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -2,8 +2,13 @@ import Header from '$lib/components/Header.svelte' import { isLoading } from 'svelte-i18n' import { resolve } from '$app/paths' + import { page } from '$app/state' let { children } = $props() + + // Debug sandboxes are dev-only and use the full viewport for their + // own chrome; suppress the global footer there. + const isDebug = $derived(page.url.pathname.startsWith('/debug')) {#if !$isLoading} @@ -14,13 +19,15 @@
{@render children()}
- + {#if !isDebug} + + {/if} {/if}