@rapidmx/web-client 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/shared/calendar/calendarReminders.ts +35 -0
- package/apps/shared/calendar/sealedEvents.ts +42 -9
- package/apps/shared/calendar/useCalendarReminders.ts +28 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +29 -1
- package/apps/shared/components/calendar/EventDetails.tsx +13 -2
- package/apps/shared/components/calendar/EventEditor.tsx +124 -17
- package/apps/shared/components/calendar/EventFormParts.tsx +1 -0
- package/apps/shared/components/calendar/EventModal.tsx +19 -2
- package/apps/shared/components/calendar/RequestChangeForm.tsx +4 -1
- package/apps/shared/components/calendar/eventForm.ts +2 -0
- package/apps/shared/components/contacts/ContactCard.tsx +13 -1
- package/apps/shared/components/contacts/ContactDetailPane.tsx +189 -9
- package/apps/shared/components/contacts/ContactForm.tsx +56 -7
- package/apps/shared/components/contacts/KeyChangeReview.tsx +5 -1
- package/apps/shared/components/contacts/contactCardData.ts +6 -3
- package/apps/shared/components/contacts/contactKeys.ts +3 -3
- package/apps/shared/components/layout/AppShell.tsx +34 -5
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +11 -4
- package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +37 -6
- package/apps/shared/components/mail/InviteCard.tsx +11 -1
- package/apps/shared/components/mail/MessageDetailPane.tsx +25 -14
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +9 -2
- package/apps/shared/components/mail/compose/RecipientInput.tsx +2 -1
- package/apps/shared/components/mail/compose/protectedContactSuggestions.ts +121 -0
- package/apps/shared/components/mail/invite/inviteStore.ts +11 -1
- package/apps/shared/components/mail/pinnedSigners.ts +14 -0
- package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
- package/apps/shared/components/settings/KeyExportSetting.tsx +4 -1
- package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
- package/apps/shared/components/settings/ZeroAccessSetting.tsx +13 -2
- package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
- package/apps/shared/mail/outbox/sendJob.ts +349 -50
- package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
- package/apps/shared/search/LocalIndexLifecycle.tsx +62 -33
- package/apps/shared/search/localIndexBuilder.ts +35 -24
- package/apps/shared/search/localIndexRpcClient.ts +91 -11
- package/apps/shared/search/localIndexSchema.ts +18 -2
- package/apps/shared/search/localIndexTransport.ts +6 -3
- package/apps/shared/search/localIndexWorker.ts +35 -18
- package/apps/shared/search/searchTier2.ts +5 -0
- package/apps/shared/search/zeroAccessIndexSync.ts +232 -40
- package/apps/shared/search/zeroAccessSyncState.ts +70 -0
- package/apps/shared/zeroAccess/ProtectedMark.tsx +19 -0
- package/apps/shared/zeroAccess/useKeysLocked.ts +25 -0
- package/apps/shared/zeroAccess/useOpenedRecord.ts +71 -0
- package/apps/www/_shell.tsx +2 -0
- package/apps/www/calendar/index.tsx +49 -5
- package/apps/www/contacts/index.tsx +67 -11
- package/apps/www/index.tsx +10 -1
- package/apps/www/settings/encryption/index.tsx +16 -1
- package/apps/www/settings/privacy/index.tsx +87 -38
- package/apps/www/tasks/index.tsx +24 -6
- package/dist/apps/shared/calendar/calendarReminders.d.ts +14 -0
- package/dist/apps/shared/calendar/calendarReminders.js +26 -0
- package/dist/apps/shared/calendar/sealedEvents.d.ts +3 -2
- package/dist/apps/shared/calendar/sealedEvents.js +40 -9
- package/dist/apps/shared/calendar/useCalendarReminders.d.ts +2 -1
- package/dist/apps/shared/calendar/useCalendarReminders.js +27 -9
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +9 -2
- package/dist/apps/shared/components/calendar/EventDetails.js +13 -2
- package/dist/apps/shared/components/calendar/EventEditor.js +115 -18
- package/dist/apps/shared/components/calendar/EventFormParts.js +1 -1
- package/dist/apps/shared/components/calendar/EventModal.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventModal.js +15 -2
- package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +3 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +1 -1
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -0
- package/dist/apps/shared/components/contacts/ContactCard.js +12 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +2 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +103 -9
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +4 -1
- package/dist/apps/shared/components/contacts/ContactForm.js +37 -7
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +3 -1
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +3 -2
- package/dist/apps/shared/components/contacts/contactCardData.js +6 -3
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +1 -1
- package/dist/apps/shared/components/contacts/contactKeys.js +3 -3
- package/dist/apps/shared/components/layout/AppShell.d.ts +18 -1
- package/dist/apps/shared/components/layout/AppShell.js +17 -6
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -4
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +3 -1
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +30 -4
- package/dist/apps/shared/components/mail/InviteCard.js +11 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +15 -11
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +2 -2
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +3 -2
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.js +96 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.js +11 -1
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +6 -0
- package/dist/apps/shared/components/mail/pinnedSigners.js +10 -1
- package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +2 -2
- package/dist/apps/shared/components/mail/reading/conversationPrint.js +3 -3
- package/dist/apps/shared/components/settings/KeyExportSetting.js +3 -1
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.d.ts +13 -0
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.js +143 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.d.ts +2 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.js +12 -3
- package/dist/apps/shared/components/settings/ZeroAccessTypes.d.ts +29 -0
- package/dist/apps/shared/components/settings/ZeroAccessTypes.js +107 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +3 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +3 -2
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +37 -4
- package/dist/apps/shared/mail/outbox/sendJob.js +287 -42
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +12 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +76 -20
- package/dist/apps/shared/search/LocalIndexLifecycle.js +55 -33
- package/dist/apps/shared/search/localIndexBuilder.d.ts +4 -1
- package/dist/apps/shared/search/localIndexBuilder.js +31 -20
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
- package/dist/apps/shared/search/localIndexRpcClient.js +126 -10
- package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
- package/dist/apps/shared/search/localIndexSchema.js +16 -2
- package/dist/apps/shared/search/localIndexTransport.d.ts +6 -3
- package/dist/apps/shared/search/localIndexWorker.d.ts +9 -0
- package/dist/apps/shared/search/localIndexWorker.js +22 -15
- package/dist/apps/shared/search/searchTier2.js +5 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +37 -6
- package/dist/apps/shared/search/zeroAccessIndexSync.js +199 -37
- package/dist/apps/shared/search/zeroAccessSyncState.d.ts +20 -0
- package/dist/apps/shared/search/zeroAccessSyncState.js +65 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.d.ts +7 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.js +10 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.d.ts +6 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.js +20 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.d.ts +18 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.js +66 -0
- package/dist/apps/www/_shell.d.ts +1 -1
- package/dist/apps/www/_shell.js +2 -2
- package/dist/apps/www/calendar/index.js +47 -5
- package/dist/apps/www/contacts/index.js +64 -11
- package/dist/apps/www/index.js +10 -1
- package/dist/apps/www/settings/encryption/index.js +14 -4
- package/dist/apps/www/settings/privacy/index.js +46 -5
- package/dist/apps/www/tasks/index.js +21 -7
- package/dist/lib/admin/domainsApi.d.ts +1 -1
- package/dist/lib/calendar/calendarApi.d.ts +46 -6
- package/dist/lib/calendar/calendarApi.js +110 -17
- package/dist/lib/calendar/calendarMutations.d.ts +20 -7
- package/dist/lib/calendar/calendarMutations.js +73 -14
- package/dist/lib/calendar/inviteApi.d.ts +17 -3
- package/dist/lib/calendar/inviteApi.js +61 -4
- package/dist/lib/calendar/protectedEvents.d.ts +31 -0
- package/dist/lib/calendar/protectedEvents.js +113 -0
- package/dist/lib/calendar/recurrence.d.ts +2 -0
- package/dist/lib/calendar/recurrence.js +16 -0
- package/dist/lib/calendar/sealedInvites.d.ts +6 -8
- package/dist/lib/calendar/sealedInvites.js +43 -6
- package/dist/lib/contacts/contactsApi.d.ts +29 -3
- package/dist/lib/contacts/contactsApi.js +23 -7
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +5 -2
- package/dist/lib/contacts/useContactPhotoSrc.js +91 -11
- package/dist/lib/crypto/composeSecurity.d.ts +11 -3
- package/dist/lib/crypto/composeSecurity.js +13 -5
- package/dist/lib/crypto/keyExport.js +2 -2
- package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
- package/dist/lib/crypto/keyPairMatch.js +65 -0
- package/dist/lib/crypto/keySession.d.ts +19 -2
- package/dist/lib/crypto/keySession.js +89 -13
- package/dist/lib/crypto/keyvaultApi.d.ts +41 -5
- package/dist/lib/crypto/keyvaultApi.js +13 -3
- package/dist/lib/crypto/meetingKeys.d.ts +25 -0
- package/dist/lib/crypto/meetingKeys.js +87 -0
- package/dist/lib/crypto/messageSecurity.d.ts +26 -2
- package/dist/lib/crypto/messageSecurity.js +122 -10
- package/dist/lib/crypto/openPgpKey.d.ts +39 -0
- package/dist/lib/crypto/openPgpKey.js +89 -0
- package/dist/lib/crypto/openpgp.d.ts +47 -0
- package/dist/lib/crypto/openpgp.js +88 -0
- package/dist/lib/crypto/pgpMessage.d.ts +51 -0
- package/dist/lib/crypto/pgpMessage.js +271 -0
- package/dist/lib/crypto/sharedMailboxKeys.d.ts +15 -3
- package/dist/lib/crypto/sharedMailboxKeys.js +102 -29
- package/dist/lib/crypto/smimeMessage.d.ts +21 -0
- package/dist/lib/crypto/smimeMessage.js +11 -7
- package/dist/lib/crypto/zeroAccessKey.js +5 -0
- package/dist/lib/mail/mailApi.d.ts +33 -0
- package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
- package/dist/lib/mail/mailboxImportApi.js +15 -0
- package/dist/lib/search/zeroAccessIndexApi.d.ts +4 -0
- package/dist/lib/tasks/tasksApi.d.ts +11 -1
- package/dist/lib/tasks/tasksApi.js +10 -4
- package/dist/lib/util/clientPlatform.d.ts +14 -0
- package/dist/lib/util/clientPlatform.js +26 -0
- package/dist/lib/videoconf/protectedMeetings.d.ts +74 -0
- package/dist/lib/videoconf/protectedMeetings.js +181 -0
- package/dist/lib/videoconf/videoMeetingsApi.d.ts +25 -4
- package/dist/lib/videoconf/videoMeetingsApi.js +1 -1
- package/dist/lib/zeroAccess/sealedMoves.d.ts +41 -0
- package/dist/lib/zeroAccess/sealedMoves.js +63 -0
- package/dist/lib/zeroAccess/sealedRecords.d.ts +29 -0
- package/dist/lib/zeroAccess/sealedRecords.js +136 -0
- package/lib/admin/domainsApi.ts +1 -1
- package/lib/calendar/calendarApi.ts +140 -17
- package/lib/calendar/calendarMutations.ts +79 -12
- package/lib/calendar/inviteApi.ts +75 -5
- package/lib/calendar/protectedEvents.ts +143 -0
- package/lib/calendar/recurrence.ts +17 -0
- package/lib/calendar/sealedInvites.ts +47 -7
- package/lib/contacts/contactsApi.ts +43 -17
- package/lib/contacts/useContactPhotoSrc.ts +103 -13
- package/lib/crypto/composeSecurity.ts +20 -5
- package/lib/crypto/keyExport.ts +2 -2
- package/lib/crypto/keyPairMatch.ts +69 -0
- package/lib/crypto/keySession.ts +102 -12
- package/lib/crypto/keyvaultApi.ts +49 -7
- package/lib/crypto/meetingKeys.ts +118 -0
- package/lib/crypto/messageSecurity.ts +174 -13
- package/lib/crypto/openPgpKey.ts +118 -0
- package/lib/crypto/openpgp.ts +115 -0
- package/lib/crypto/pgpMessage.ts +336 -0
- package/lib/crypto/sharedMailboxKeys.ts +106 -28
- package/lib/crypto/smimeMessage.ts +11 -7
- package/lib/crypto/zeroAccessKey.ts +5 -0
- package/lib/mail/mailApi.ts +27 -0
- package/lib/mail/mailboxImportApi.ts +24 -0
- package/lib/search/zeroAccessIndexApi.ts +4 -0
- package/lib/tasks/tasksApi.ts +19 -5
- package/lib/util/clientPlatform.ts +50 -0
- package/lib/videoconf/protectedMeetings.ts +221 -0
- package/lib/videoconf/videoMeetingsApi.ts +15 -4
- package/lib/zeroAccess/sealedMoves.ts +79 -0
- package/lib/zeroAccess/sealedRecords.ts +153 -0
- package/package.json +12 -3
|
@@ -7,6 +7,7 @@ import { getUnlockedKeys, personallyUnlockedKeys, subscribeKeySession } from "..
|
|
|
7
7
|
import type { Mailbox } from "../../../../lib/mail/mailApi.js";
|
|
8
8
|
import type { ApiClient } from "../../../../lib/util/api.js";
|
|
9
9
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
10
|
+
import { notify } from "../../notifications/store.js";
|
|
10
11
|
|
|
11
12
|
/** How often the shared mailboxes are looked after again while some own mailbox stays unlocked. */
|
|
12
13
|
export const SHARED_MAILBOX_CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
|
@@ -19,6 +20,14 @@ const loadSharedKeys = () =>
|
|
|
19
20
|
throw err;
|
|
20
21
|
}));
|
|
21
22
|
|
|
23
|
+
/** The refused rotations already shown, per mailbox (the reason): each is shown once a session, not on every pass that retries it. */
|
|
24
|
+
const shownRefusals = new Map<string, string>();
|
|
25
|
+
|
|
26
|
+
/** Forgets which refused rotations were shown (tests). */
|
|
27
|
+
export function resetSharedMailboxKeyManager(): void {
|
|
28
|
+
shownRefusals.clear();
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
export interface SharedMailboxKeyManagerProps {
|
|
23
32
|
/** Every mailbox the user can open. */
|
|
24
33
|
mailboxes: Mailbox[];
|
|
@@ -29,7 +38,7 @@ export interface SharedMailboxKeyManagerProps {
|
|
|
29
38
|
* Looks after the zero-access keys of the user's shared mailboxes (`lib/crypto/sharedMailboxKeys.ts`) whenever a mailbox of their own is
|
|
30
39
|
* unlocked, and every `SHARED_MAILBOX_CHECK_INTERVAL_MS` after that: opens each mailbox shared with them through their member key wraps (no
|
|
31
40
|
* password), and - for a mailbox they own or manage - provisions, rotates and shares its key with members still waiting. Best effort and
|
|
32
|
-
* silent
|
|
41
|
+
* silent (a pass that fails is tried again on the next), except for a rotation the server refuses, shown once as a warning. Renders nothing; mounted by `MailShell`.
|
|
33
42
|
*/
|
|
34
43
|
export default function SharedMailboxKeyManager({ mailboxes, userUid }: SharedMailboxKeyManagerProps) {
|
|
35
44
|
const client = useApiClient();
|
|
@@ -75,17 +84,39 @@ async function look(
|
|
|
75
84
|
mailbox: Mailbox,
|
|
76
85
|
me: string | undefined,
|
|
77
86
|
client: ApiClient | undefined,
|
|
78
|
-
unlockSharedMailbox: (uid: string, client?: ApiClient) => Promise<string>,
|
|
79
|
-
maintainSharedMailbox: (
|
|
87
|
+
unlockSharedMailbox: (uid: string, client?: ApiClient, refresh?: boolean) => Promise<string>,
|
|
88
|
+
maintainSharedMailbox: (
|
|
89
|
+
uid: string,
|
|
90
|
+
address: string,
|
|
91
|
+
client?: ApiClient,
|
|
92
|
+
) => Promise<{ provisioned: boolean; rotated: boolean; rotationRefused?: string }>,
|
|
80
93
|
): Promise<void> {
|
|
81
94
|
const owned = !!mailbox.ownerUserUid && mailbox.ownerUserUid === me;
|
|
82
|
-
if (!owned
|
|
83
|
-
|
|
95
|
+
if (!owned) {
|
|
96
|
+
// Opened, or - once open - opened again when a rotation left it without the active key.
|
|
97
|
+
await unlockSharedMailbox(mailbox.uid, client, true);
|
|
84
98
|
}
|
|
85
99
|
const unlocked = getUnlockedKeys(mailbox.uid);
|
|
86
100
|
// An owner looks after their mailbox with its own keys; a member, with full access, whether or not it is open yet (a mailbox
|
|
87
101
|
// without an owner may have no key to open).
|
|
88
102
|
if (owned ? unlocked && !unlocked.shared : true) {
|
|
89
|
-
await maintainSharedMailbox(mailbox.uid, mailbox.primarySmtpAddress, client);
|
|
103
|
+
const done = await maintainSharedMailbox(mailbox.uid, mailbox.primarySmtpAddress, client);
|
|
104
|
+
// The server kept the current key (its vault can't take the new key's member wraps): say so once, members keep reading.
|
|
105
|
+
if (done.rotationRefused && shownRefusals.get(mailbox.uid) !== done.rotationRefused) {
|
|
106
|
+
shownRefusals.set(mailbox.uid, done.rotationRefused);
|
|
107
|
+
notify({
|
|
108
|
+
kind: "warning",
|
|
109
|
+
title: `${mailbox.displayName || mailbox.primarySmtpAddress}: the zero-access key wasn't replaced`,
|
|
110
|
+
message: "A member has left, but the server refused a new key for this shared mailbox. The current key stays active.",
|
|
111
|
+
details: done.rotationRefused,
|
|
112
|
+
dedupeKey: `shared-rotation-refused:${mailbox.uid}`,
|
|
113
|
+
});
|
|
114
|
+
} else if (!done.rotationRefused) {
|
|
115
|
+
shownRefusals.delete(mailbox.uid);
|
|
116
|
+
}
|
|
117
|
+
// A key this member just made (a mailbox without an owner) goes into their session now, not on the next pass.
|
|
118
|
+
if (!owned && (done.provisioned || done.rotated)) {
|
|
119
|
+
await unlockSharedMailbox(mailbox.uid, client, true);
|
|
120
|
+
}
|
|
90
121
|
}
|
|
91
122
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
respondToMessageInvite,
|
|
16
16
|
} from "../../../../lib/calendar/inviteApi.js";
|
|
17
17
|
import { guestPermissionsOf } from "../../../../lib/calendar/calendarApi.js";
|
|
18
|
+
import { openSealedEvent } from "../../calendar/sealedEvents.js";
|
|
18
19
|
import { formatMailAddress } from "../../../../lib/mail/mailAddress.js";
|
|
19
20
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
20
21
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
@@ -334,7 +335,7 @@ export default function InviteCard({ messageUid, headingLevel = 2 }: InviteCardP
|
|
|
334
335
|
aria-busy={pending === "acceptProposal" || undefined}
|
|
335
336
|
loading={pending === "acceptProposal"}
|
|
336
337
|
disabled={busy}
|
|
337
|
-
onClick={() => void run("acceptProposal", "Couldn't accept the proposed time", () => acceptProposal(messageUid, client))}
|
|
338
|
+
onClick={() => void run("acceptProposal", "Couldn't accept the proposed time", () => acceptProposal(messageUid, client, invite.calendarEventUid, openSealedEvent))}
|
|
338
339
|
>
|
|
339
340
|
Accept proposal
|
|
340
341
|
</Button>
|
|
@@ -387,6 +388,15 @@ export default function InviteCard({ messageUid, headingLevel = 2 }: InviteCardP
|
|
|
387
388
|
description: invite.description,
|
|
388
389
|
descriptionHtml: invite.descriptionHtml,
|
|
389
390
|
guestAddresses: invite.attendees.map((attendee) => attendee.address),
|
|
391
|
+
// The invitation's own content: what a protected copy of the event is named by.
|
|
392
|
+
invitationContent: invite.summary
|
|
393
|
+
? {
|
|
394
|
+
title: invite.summary,
|
|
395
|
+
location: invite.location ?? undefined,
|
|
396
|
+
description: invite.description ?? undefined,
|
|
397
|
+
descriptionHtml: invite.descriptionHtml ?? undefined,
|
|
398
|
+
}
|
|
399
|
+
: undefined,
|
|
390
400
|
}}
|
|
391
401
|
canChange={!!invite.canRequestChange}
|
|
392
402
|
canInvite={!!invite.canRequestInvite}
|
|
@@ -50,11 +50,11 @@ import { isCalendarAttachment } from "./invite/inviteFormat.js";
|
|
|
50
50
|
import { useMessageInvite } from "./invite/inviteStore.js";
|
|
51
51
|
import { rememberSealedInvite } from "../../../../lib/calendar/sealedInvites.js";
|
|
52
52
|
import { getUnlockedKeys, subscribeKeySession } from "../../../../lib/crypto/keySession.js";
|
|
53
|
-
import type { MessageSecurityResult, SignatureFailureReason } from "../../../../lib/crypto/messageSecurity.js";
|
|
53
|
+
import type { MessageSecurityResult, SignatureFailureReason, SignerPinsInput } from "../../../../lib/crypto/messageSecurity.js";
|
|
54
54
|
import { extractAddresses, type MimeAttachment } from "../../../../lib/crypto/mime.js";
|
|
55
55
|
import { SignerKeyConflictError, signingKeyFingerprints, trustSigner } from "../../../../lib/crypto/keyvaultApi.js";
|
|
56
56
|
import { isLikelyMailingList } from "../../../../lib/crypto/composeSecurity.js";
|
|
57
|
-
import { SenderKeyState, clearPinnedSignerCache,
|
|
57
|
+
import { SenderKeyState, clearPinnedSignerCache, getPinnedSigners, getSignerKeyState } from "./pinnedSigners.js";
|
|
58
58
|
import { currentVerificationSeal, getVaultGeneration, sendVerificationSeal } from "./verificationSeals.js";
|
|
59
59
|
import { getMyMailboxAccess } from "../../../../lib/mail/mailboxAccessApi.js";
|
|
60
60
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
@@ -612,9 +612,9 @@ function MessageDetailContent({
|
|
|
612
612
|
// mailbox's own signing keys when it sent the message itself. A failed lookup means no pins, which can only
|
|
613
613
|
// ever make a signature "signer not verified", never verified.
|
|
614
614
|
const senderAddress = message.from.address;
|
|
615
|
-
const pinsPromise =
|
|
616
|
-
(
|
|
617
|
-
() => ({ loaded: false, fingerprints: [] as string[] }),
|
|
615
|
+
const pinsPromise = getPinnedSigners(message.mailboxUid, senderAddress, client).then(
|
|
616
|
+
(signers) => ({ loaded: true, ...signers }),
|
|
617
|
+
() => ({ loaded: false, fingerprints: [] as string[], openPgpKeys: [] as string[] }),
|
|
618
618
|
);
|
|
619
619
|
// Verification seals (see `verificationSeals.ts`) need unlocked keys and the vault's current master key
|
|
620
620
|
// generation; without either the message is evaluated exactly as before, with no seal. The sender's key records
|
|
@@ -640,7 +640,9 @@ function MessageDetailContent({
|
|
|
640
640
|
const ownPins = signingKeyFingerprints(ownKeys);
|
|
641
641
|
const [contactPins, sealContext] = await Promise.all([pinsPromise, sealContextPromise]);
|
|
642
642
|
const pins = [...new Set([...contactPins.fingerprints, ...ownPins])];
|
|
643
|
-
|
|
643
|
+
// An OpenPGP signature is checked against the sender's pinned OpenPGP certificates themselves (it names only a key ID).
|
|
644
|
+
const pinned: SignerPinsInput =
|
|
645
|
+
contactPins.openPgpKeys.length > 0 ? { fingerprints: pins, openPgpKeys: contactPins.openPgpKeys } : pins.length > 0 ? pins : undefined;
|
|
644
646
|
// The raw content is a byte string - only ever handed to evaluateMessageSecurity(), never shown. Keys
|
|
645
647
|
// are read after the pin lookup's await, so a lock that happened meanwhile is honored.
|
|
646
648
|
const unlocked = getUnlockedKeys(message.mailboxUid);
|
|
@@ -677,7 +679,7 @@ function MessageDetailContent({
|
|
|
677
679
|
const viaAlias = await messageSecurity.evaluateMessageSecurity(rawMime, unlocked, pinned, alias);
|
|
678
680
|
result = { ...result, notAddressedToReader: viaAlias.notAddressedToReader };
|
|
679
681
|
}
|
|
680
|
-
const unpinned = contactPins.loaded && pins.length === 0;
|
|
682
|
+
const unpinned = contactPins.loaded && pins.length === 0 && contactPins.openPgpKeys.length === 0;
|
|
681
683
|
const keyChanged = signerKeyChanged(result);
|
|
682
684
|
const [keyState, access] = await Promise.all([
|
|
683
685
|
keyChanged || (unpinned && UNVERIFIED_SIGNER_STATES.has(result.state)) ? loadKeyState() : undefined,
|
|
@@ -783,11 +785,11 @@ function MessageDetailContent({
|
|
|
783
785
|
// Only ever invoked from the "Trust this signer" dialog, which only opens for an unverified-signer result carrying
|
|
784
786
|
// `signerCertificate`. The pin goes on the contact in the mailbox the message belongs to; the evaluation is then
|
|
785
787
|
// re-run against a fresh pinned-signer lookup, so the badge turns verified.
|
|
786
|
-
async function handleTrustSigner(address: string, certificate: string) {
|
|
788
|
+
async function handleTrustSigner(address: string, certificate: string, type?: "openpgp") {
|
|
787
789
|
setTrusting(true);
|
|
788
790
|
setTrustError(null);
|
|
789
791
|
try {
|
|
790
|
-
await trustSigner(message.mailboxUid, { address, certificate }, client);
|
|
792
|
+
await trustSigner(message.mailboxUid, { address, certificate, ...(type ? { type } : {}) }, client);
|
|
791
793
|
clearPinnedSignerCache();
|
|
792
794
|
setTrustConfirmOpen(false);
|
|
793
795
|
setUnlockRefresh((n) => n + 1);
|
|
@@ -1112,7 +1114,10 @@ function MessageDetailContent({
|
|
|
1112
1114
|
? security.signerCertificate
|
|
1113
1115
|
: undefined;
|
|
1114
1116
|
const keyChanged = security && signerKeyChanged(security) ? security : undefined;
|
|
1115
|
-
|
|
1117
|
+
// An OpenPGP signer's key is pinned as the contact's OpenPGP certificate, not a signing certificate.
|
|
1118
|
+
const openPgpSigner = keyChanged?.signerKeyType === "openpgp";
|
|
1119
|
+
const pinnedSignerKey = openPgpSigner ? senderKeyState?.openPgp?.[0] : senderKeyState?.pinned[0];
|
|
1120
|
+
const pinnedSignerSince = openPgpSigner ? senderKeyState?.openPgpPinnedSince : senderKeyState?.pinnedSince;
|
|
1116
1121
|
const recordedConflict =
|
|
1117
1122
|
keyChanged && sameFingerprint(senderKeyState?.conflict?.observedKey.fingerprint, keyChanged.signerFingerprint)
|
|
1118
1123
|
? senderKeyState!.conflict
|
|
@@ -1148,7 +1153,7 @@ function MessageDetailContent({
|
|
|
1148
1153
|
// Registered while rendering, not in an effect: the invitation lookup below must find it the moment it is enabled. Writing the same
|
|
1149
1154
|
// file for the same message again is a no-op.
|
|
1150
1155
|
if (sealedCalendar !== undefined) {
|
|
1151
|
-
rememberSealedInvite(message.uid, sealedCalendar);
|
|
1156
|
+
rememberSealedInvite(message.uid, sealedCalendar, message.mailboxUid);
|
|
1152
1157
|
}
|
|
1153
1158
|
const mayHoldInvite =
|
|
1154
1159
|
!message.encrypted &&
|
|
@@ -1349,6 +1354,11 @@ function MessageDetailContent({
|
|
|
1349
1354
|
)}
|
|
1350
1355
|
{security && <SecurityIndicator security={security} zeroAccess={!!message.zeroAccess} />}
|
|
1351
1356
|
</div>
|
|
1357
|
+
{security?.signerKeyUnknown && (
|
|
1358
|
+
<p role="status" className="mt-2 text-sm text-text-muted">
|
|
1359
|
+
Signed with an OpenPGP key RapidMX doesn’t have for this sender yet, so the signature couldn’t be checked.
|
|
1360
|
+
</p>
|
|
1361
|
+
)}
|
|
1352
1362
|
{senderNameCheck.misleading && (
|
|
1353
1363
|
<p role="status" className="mt-2 py-2 px-3 rounded-sm text-sm bg-warning/15 text-text">
|
|
1354
1364
|
The sender’s name “{senderName}” looks like an email address, but this message was
|
|
@@ -1577,9 +1587,10 @@ function MessageDetailContent({
|
|
|
1577
1587
|
<KeyChangeReview
|
|
1578
1588
|
mailboxUid={message.mailboxUid}
|
|
1579
1589
|
address={senderAddress}
|
|
1580
|
-
useType="sign"
|
|
1590
|
+
useType={openPgpSigner ? "encrypt" : "sign"}
|
|
1591
|
+
{...(openPgpSigner ? { keyType: "openpgp" as const } : {})}
|
|
1581
1592
|
ownerName="the sender"
|
|
1582
|
-
current={pinnedSignerKey && { fingerprint: pinnedSignerKey.fingerprint, since:
|
|
1593
|
+
current={pinnedSignerKey && { fingerprint: pinnedSignerKey.fingerprint, since: pinnedSignerSince! }}
|
|
1583
1594
|
proposed={{
|
|
1584
1595
|
fingerprint: keyChanged.signerFingerprint!,
|
|
1585
1596
|
emails: keyChanged.signerEmails,
|
|
@@ -1879,7 +1890,7 @@ function MessageDetailContent({
|
|
|
1879
1890
|
className="!w-auto"
|
|
1880
1891
|
loading={trusting}
|
|
1881
1892
|
disabled={trusting}
|
|
1882
|
-
onClick={() => handleTrustSigner(senderAddress, trustableCertificate
|
|
1893
|
+
onClick={() => handleTrustSigner(senderAddress, trustableCertificate!, security?.signerKeyType)}
|
|
1883
1894
|
>
|
|
1884
1895
|
Trust
|
|
1885
1896
|
</Button>
|
|
@@ -48,7 +48,7 @@ import { notify } from "../../../notifications/store.js";
|
|
|
48
48
|
import { isFileDrag, isInlineImage } from "./dropFiles.js";
|
|
49
49
|
import { evaluateEncryptionRequirement } from "./encryptionRequirement.js";
|
|
50
50
|
import { decideSend, SendBlock } from "../../../mail/outbox/sendDecision.js";
|
|
51
|
-
import { SendRequest, sendDecisionInput, startSend } from "../../../mail/outbox/sendJob.js";
|
|
51
|
+
import { SendRequest, sendDecisionInput, senderCanUseOpenPgp, startSend } from "../../../mail/outbox/sendJob.js";
|
|
52
52
|
import { useCompose, type ComposeSession } from "./ComposeContext.js";
|
|
53
53
|
import { ariaKeyShortcuts, withHint } from "../../../keyboard/format.js";
|
|
54
54
|
import { SHORTCUTS, ShortcutDef } from "../../../keyboard/keymap.js";
|
|
@@ -492,7 +492,14 @@ export default function ComposeWindow({
|
|
|
492
492
|
return;
|
|
493
493
|
}
|
|
494
494
|
lookupFailuresRef.current.delete(key);
|
|
495
|
-
const status = resolveRecipientEncryption(
|
|
495
|
+
const status = resolveRecipientEncryption(
|
|
496
|
+
mailbox.primarySmtpAddress,
|
|
497
|
+
ownPrefersMutual,
|
|
498
|
+
encryptionPolicy,
|
|
499
|
+
recipient.address,
|
|
500
|
+
result.lookup,
|
|
501
|
+
senderCanUseOpenPgp(getUnlockedKeys(mailbox.uid)),
|
|
502
|
+
);
|
|
496
503
|
setRecipientStatuses((prev) => ({ ...prev, [key]: status }));
|
|
497
504
|
});
|
|
498
505
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import React, { ChangeEvent, KeyboardEvent, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
6
6
|
import { createPortal } from "react-dom";
|
|
7
7
|
import { HiOutlineXMark } from "react-icons/hi2";
|
|
8
|
+
import { fetchSuggestionsWithProtectedContacts } from "./protectedContactSuggestions.js";
|
|
8
9
|
import {
|
|
9
10
|
fetchRecipientSuggestions,
|
|
10
11
|
RECIPIENT_SUGGESTION_MIN_QUERY_LENGTH,
|
|
@@ -111,7 +112,7 @@ export default function RecipientInput({
|
|
|
111
112
|
onBlur,
|
|
112
113
|
onCommit,
|
|
113
114
|
mailboxUid,
|
|
114
|
-
fetchSuggestions =
|
|
115
|
+
fetchSuggestions = fetchSuggestionsWithProtectedContacts,
|
|
115
116
|
debounceMs = RECIPIENT_SUGGESTION_DEBOUNCE_MS,
|
|
116
117
|
disabled,
|
|
117
118
|
autoFocus,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* Recipient suggestions that include contacts zero-access encryption protects. Their names are sealed (restapi `specs/search.md` §3 *What
|
|
7
|
+
* It Covers*), so the server's contact search finds them only by address; this matches their opened names on the device and puts them
|
|
8
|
+
* first. Names are opened only - never the rest of a contact - and kept for ten minutes, and only while the mailbox is unlocked: its lock
|
|
9
|
+
* (and signing out) forgets them, as does a search that finds it locked. Reading them never holds up the server's suggestions: what isn't read by the time those arrive (plus
|
|
10
|
+
* `PROTECTED_NAMES_WAIT_MS`) is left to the next keystroke.
|
|
11
|
+
*/
|
|
12
|
+
import { type Contact, listContacts, PINNED_FINGERPRINT_PAGE_SIZE } from "../../../../../lib/contacts/contactsApi.js";
|
|
13
|
+
import { getUnlockedKeys, subscribeKeySession } from "../../../../../lib/crypto/keySession.js";
|
|
14
|
+
import {
|
|
15
|
+
type ContactSuggestionOptions,
|
|
16
|
+
fetchRecipientSuggestions,
|
|
17
|
+
mergeRecipientSuggestions,
|
|
18
|
+
RECIPIENT_SUGGESTION_DEFAULT_LIMIT,
|
|
19
|
+
type RecipientSuggestion,
|
|
20
|
+
} from "../../../../../lib/mail/directoryApi.js";
|
|
21
|
+
import { listFolders } from "../../../../../lib/mail/mailApi.js";
|
|
22
|
+
import type { ApiClient } from "../../../../../lib/util/api.js";
|
|
23
|
+
import { isProtectedRecord, openSealedRecords } from "../../../../../lib/zeroAccess/sealedRecords.js";
|
|
24
|
+
import { onSigningOut } from "./composeFlushRegistry.js";
|
|
25
|
+
|
|
26
|
+
/** How long a mailbox's protected contact names are kept: reading them is a crawl of the whole address book. */
|
|
27
|
+
export const PROTECTED_NAMES_TTL_MS = 10 * 60_000;
|
|
28
|
+
|
|
29
|
+
/** How long suggestions wait, once the server's have arrived, for the protected contacts still being read. */
|
|
30
|
+
export const PROTECTED_NAMES_WAIT_MS = 250;
|
|
31
|
+
|
|
32
|
+
/** At most this many contacts are read per contacts folder. */
|
|
33
|
+
const MAX_PAGES = 20;
|
|
34
|
+
|
|
35
|
+
const cache = new Map<string, { loadedAt: number; contacts: Promise<Contact[]> }>();
|
|
36
|
+
|
|
37
|
+
/** Forgets every mailbox's names (signing out, or the test seam). */
|
|
38
|
+
export function clearProtectedContactNames(): void {
|
|
39
|
+
cache.clear();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onSigningOut(clearProtectedContactNames);
|
|
43
|
+
|
|
44
|
+
let watchingKeys = false;
|
|
45
|
+
|
|
46
|
+
/** A mailbox's names go the moment it locks, so nothing opened outlives its key session. Subscribed with the first crawl, not on import. */
|
|
47
|
+
function forgetOnLock(): void {
|
|
48
|
+
if (watchingKeys) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
watchingKeys = true;
|
|
52
|
+
subscribeKeySession(({ mailboxUid, state }) => {
|
|
53
|
+
if (state === "locked") {
|
|
54
|
+
cache.delete(mailboxUid);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `mailboxUid`'s protected contacts with their names opened. */
|
|
60
|
+
async function loadProtectedContacts(mailboxUid: string, client: ApiClient | undefined): Promise<Contact[]> {
|
|
61
|
+
const found: Contact[] = [];
|
|
62
|
+
// The folders side by side, each one's pages in turn.
|
|
63
|
+
await Promise.all(
|
|
64
|
+
(await listFolders(mailboxUid, client))
|
|
65
|
+
.filter((f) => f.type === "contacts")
|
|
66
|
+
.map(async (folder) => {
|
|
67
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
68
|
+
const batch = await listContacts(folder.uid, { limit: PINNED_FINGERPRINT_PAGE_SIZE, page }, client);
|
|
69
|
+
found.push(...batch.filter((contact) => isProtectedRecord(contact)));
|
|
70
|
+
if (batch.length < PINNED_FINGERPRINT_PAGE_SIZE) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
return openSealedRecords(found, ["displayName"]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function protectedContacts(mailboxUid: string, client: ApiClient | undefined): Promise<Contact[]> {
|
|
80
|
+
let entry = cache.get(mailboxUid);
|
|
81
|
+
if (!entry || Date.now() - entry.loadedAt > PROTECTED_NAMES_TTL_MS) {
|
|
82
|
+
forgetOnLock();
|
|
83
|
+
const created = { loadedAt: Date.now(), contacts: loadProtectedContacts(mailboxUid, client) };
|
|
84
|
+
created.contacts.catch(() => cache.delete(mailboxUid));
|
|
85
|
+
cache.set(mailboxUid, created);
|
|
86
|
+
entry = created;
|
|
87
|
+
}
|
|
88
|
+
return entry.contacts;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every address of the protected contacts of `mailboxUid` whose opened name contains `query` (case-insensitively). */
|
|
92
|
+
export async function searchProtectedContactNames(mailboxUid: string, query: string, client?: ApiClient): Promise<RecipientSuggestion[]> {
|
|
93
|
+
const wanted = query.trim().toLowerCase();
|
|
94
|
+
if (wanted.length < 2) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
if (!getUnlockedKeys(mailboxUid)?.zeroAccessKeys?.length) {
|
|
98
|
+
// Locked: nothing opened is kept, and nothing could be opened now.
|
|
99
|
+
cache.delete(mailboxUid);
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return (await protectedContacts(mailboxUid, client))
|
|
103
|
+
.filter((contact) => contact.displayName.toLowerCase().includes(wanted))
|
|
104
|
+
.flatMap((contact) => contact.emails.map((email) => ({ displayName: contact.displayName, address: email.address, kind: "contact" as const })));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `fetchRecipientSuggestions()` with the composing mailbox's protected contacts matched by name on the device, first - those read by the
|
|
108
|
+
* time the server's suggestions are in (plus `PROTECTED_NAMES_WAIT_MS`): a first search of a large address book shows the server's alone
|
|
109
|
+
* rather than waiting for the crawl, which goes on for the next keystroke. A failure to read them only leaves them out. */
|
|
110
|
+
export async function fetchSuggestionsWithProtectedContacts(
|
|
111
|
+
query: string,
|
|
112
|
+
options: ContactSuggestionOptions = {},
|
|
113
|
+
client?: ApiClient,
|
|
114
|
+
): Promise<RecipientSuggestion[]> {
|
|
115
|
+
const local: Promise<RecipientSuggestion[]> = options.mailboxUid ? searchProtectedContactNames(options.mailboxUid, query, client).catch(() => []) : Promise.resolve([]);
|
|
116
|
+
const server = await fetchRecipientSuggestions(query, options, client);
|
|
117
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
118
|
+
const ready = await Promise.race([local, new Promise<RecipientSuggestion[]>((resolve) => (timer = setTimeout(() => resolve([]), PROTECTED_NAMES_WAIT_MS)))]);
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
return mergeRecipientSuggestions(ready, server, options.limit ?? RECIPIENT_SUGGESTION_DEFAULT_LIMIT);
|
|
121
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import { useCallback, useEffect, useReducer } from "react";
|
|
6
6
|
import { MessageInvite, getMessageInvite } from "../../../../../lib/calendar/inviteApi.js";
|
|
7
|
-
import { clearSealedInvites, sealedInviteCalendar } from "../../../../../lib/calendar/sealedInvites.js";
|
|
7
|
+
import { clearSealedInvites, onSealedInvitesForgotten, sealedInviteCalendar } from "../../../../../lib/calendar/sealedInvites.js";
|
|
8
8
|
import { onSigningOut } from "../compose/composeFlushRegistry.js";
|
|
9
9
|
import { ApiClient } from "../../../../../lib/util/api.js";
|
|
10
10
|
import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
|
|
@@ -126,3 +126,13 @@ export function useMessageInvite(uid: string, enabled = true): { invite: Message
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
onSigningOut(clearSealedInvites);
|
|
129
|
+
onSigningOut(clearInviteCache);
|
|
130
|
+
// What the server read from a zero-access message's decrypted calendar file (its summary, description) goes with the file when the mailbox
|
|
131
|
+
// locks: mounted cards, chips and popovers draw nothing until it is read again.
|
|
132
|
+
onSealedInvitesForgotten((uids) => {
|
|
133
|
+
for (const uid of uids) {
|
|
134
|
+
if (entries.delete(uid)) {
|
|
135
|
+
notify(uid);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
PINNED_FINGERPRINT_PAGE_SIZE,
|
|
29
29
|
SignerKeyState,
|
|
30
30
|
listContacts,
|
|
31
|
+
pinnedOpenPgpKeysFor,
|
|
31
32
|
pinnedSigningFingerprintsFor,
|
|
32
33
|
signerKeyStateFor,
|
|
33
34
|
} from "../../../../lib/contacts/contactsApi.js";
|
|
@@ -35,6 +36,7 @@ import { keyPinnedSince } from "../contacts/contactKeys.js";
|
|
|
35
36
|
import { listFolders } from "../../../../lib/mail/mailApi.js";
|
|
36
37
|
import { subscribeKeySession } from "../../../../lib/crypto/keySession.js";
|
|
37
38
|
import type { ApiClient } from "../../../../lib/util/api.js";
|
|
39
|
+
import type { SignerPins } from "../../../../lib/crypto/messageSecurity.js";
|
|
38
40
|
|
|
39
41
|
/** How long one mailbox's loaded contact list is reused before it is fetched again. */
|
|
40
42
|
export const CONTACTS_CACHE_TTL_MS = 60_000;
|
|
@@ -120,12 +122,21 @@ export async function getPinnedSignerFingerprints(mailboxUid: string, address: s
|
|
|
120
122
|
return pinnedSigningFingerprintsFor(await cachedMailboxContacts(mailboxUid, client), address);
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/** The sender `address`'s pinned signing fingerprints and pinned OpenPGP certificates from `mailboxUid`'s contacts - what
|
|
126
|
+
* `evaluateMessageSecurity()` takes as `SignerPins`. Rejects if they can't be loaded. */
|
|
127
|
+
export async function getPinnedSigners(mailboxUid: string, address: string, client?: ApiClient): Promise<SignerPins> {
|
|
128
|
+
const contacts = await cachedMailboxContacts(mailboxUid, client);
|
|
129
|
+
return { fingerprints: pinnedSigningFingerprintsFor(contacts, address), openPgpKeys: pinnedOpenPgpKeysFor(contacts, address) };
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
/** A sender's signing-key state (`signerKeyStateFor()`) plus what a key-changed notice needs to show and link. */
|
|
124
133
|
export interface SenderKeyState extends SignerKeyState {
|
|
125
134
|
/** The contact holding the first pinned signing key, else the one holding the conflict, else the first match. */
|
|
126
135
|
contactUid?: string;
|
|
127
136
|
/** When the first pinned signing key started being trusted - see `keyPinnedSince()`. */
|
|
128
137
|
pinnedSince?: number;
|
|
138
|
+
/** When the first pinned OpenPGP certificate (`openPgp[0]`) started being trusted. */
|
|
139
|
+
openPgpPinnedSince?: number;
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
/** The sender `address`'s signing-key state from `mailboxUid`'s (cached) contacts, for a "signing key changed" comparison
|
|
@@ -142,9 +153,12 @@ export async function getSignerKeyState(mailboxUid: string, address: string, cli
|
|
|
142
153
|
holder ??
|
|
143
154
|
matching.find((candidate) => (candidate.keyConflicts ?? []).some((conflict) => conflict.useType === "sign")) ??
|
|
144
155
|
matching[0];
|
|
156
|
+
const openPgpKey = state.openPgp?.[0];
|
|
157
|
+
const openPgpHolder = openPgpKey && matching.find((candidate) => (candidate.keys ?? []).includes(openPgpKey));
|
|
145
158
|
return {
|
|
146
159
|
...state,
|
|
147
160
|
...(contact ? { contactUid: contact.uid } : {}),
|
|
148
161
|
...(holder ? { pinnedSince: keyPinnedSince(holder, pinned) } : {}),
|
|
162
|
+
...(openPgpHolder ? { openPgpPinnedSince: keyPinnedSince(openPgpHolder, openPgpKey) } : {}),
|
|
149
163
|
};
|
|
150
164
|
}
|
|
@@ -39,13 +39,13 @@ export async function olderConversationMessages(message: Message, client?: ApiCl
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* One earlier message of a conversation as it prints: the body the reading pane would show. An encrypted one
|
|
43
|
-
* its mailbox's keys unlocked; when it can't be, it prints a line saying so rather than the ciphertext. `client` is the explicit `ApiClient` every
|
|
42
|
+
* One earlier message of a conversation as it prints: the body the reading pane would show. An encrypted one - end to end, or sealed to its mailbox's
|
|
43
|
+
* zero-access key, whose subject and body the server doesn't hold readable - is decrypted here when this device has its mailbox's keys unlocked; when it can't be, it prints a line saying so rather than the ciphertext. `client` is the explicit `ApiClient` every
|
|
44
44
|
* request goes through when given (see `useApiClient()`); without one they are the cookie requests they always were.
|
|
45
45
|
*/
|
|
46
46
|
export async function printableOlderMessage(message: Message, client?: ApiClient): Promise<PrintableMessage> {
|
|
47
47
|
const attachments = await listAttachments(message.folderUid, message.uid, client).catch(() => []);
|
|
48
|
-
if (message.encrypted) {
|
|
48
|
+
if (message.encrypted || message.zeroAccess) {
|
|
49
49
|
const unlocked = getUnlockedKeys(message.mailboxUid);
|
|
50
50
|
if (unlocked) {
|
|
51
51
|
const { evaluateMessageSecurity } = await import("../../../../../lib/crypto/messageSecurity.js");
|
|
@@ -20,6 +20,9 @@ export interface KeyExportSettingProps {
|
|
|
20
20
|
|
|
21
21
|
const INPUT_CLASS = "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface";
|
|
22
22
|
|
|
23
|
+
/** How long the file's object URL is kept, since some browsers (WebKit, older Firefox) still read it after the click returns. */
|
|
24
|
+
const DOWNLOAD_URL_LIFETIME_MS = 60_000;
|
|
25
|
+
|
|
23
26
|
/** Hands `bytes` to the browser as a file download. */
|
|
24
27
|
function saveFile(bytes: Uint8Array, filename: string): void {
|
|
25
28
|
const url = URL.createObjectURL(new Blob([bytes as BlobPart], { type: "application/x-pkcs12" }));
|
|
@@ -29,7 +32,7 @@ function saveFile(bytes: Uint8Array, filename: string): void {
|
|
|
29
32
|
document.body.appendChild(link);
|
|
30
33
|
link.click();
|
|
31
34
|
link.remove();
|
|
32
|
-
URL.revokeObjectURL(url);
|
|
35
|
+
setTimeout(() => URL.revokeObjectURL(url), DOWNLOAD_URL_LIFETIME_MS);
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|