@rapidmx/web-client 0.35.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/escrow/matters/[uid].tsx +4 -3
- package/apps/shared/calendar/calendarReminders.ts +35 -0
- package/apps/shared/calendar/sealedEvents.ts +113 -0
- 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 +89 -3
- 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 +8 -4
- 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 +30 -1
- package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +122 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +34 -0
- package/apps/shared/components/mail/ConversationList.tsx +1 -1
- package/apps/shared/components/mail/InviteCard.tsx +11 -1
- package/apps/shared/components/mail/MessageDetailPane.tsx +59 -26
- 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/draftResume.ts +28 -3
- package/apps/shared/components/mail/compose/protectedContactSuggestions.ts +121 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +4 -3
- package/apps/shared/components/mail/invite/inviteStore.ts +17 -1
- package/apps/shared/components/mail/layout/MailShell.tsx +2 -0
- package/apps/shared/components/mail/pinnedSigners.ts +14 -0
- package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
- package/apps/shared/components/mail/reading/useMessageActions.ts +4 -1
- package/apps/shared/components/settings/KeyExportSetting.tsx +173 -0
- package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
- package/apps/shared/components/settings/PolicyLockedNotice.tsx +23 -0
- package/apps/shared/components/settings/ZeroAccessSetting.tsx +128 -0
- package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
- package/apps/shared/mail/decryptedMessages.ts +11 -3
- package/apps/shared/mail/outbox/sendJob.ts +349 -50
- package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
- package/apps/shared/mail/reportNotices.ts +3 -1
- package/apps/shared/mail/zeroAccessSource.ts +27 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +84 -24
- package/apps/shared/search/localIndexBuilder.ts +57 -20
- package/apps/shared/search/localIndexRpcClient.ts +96 -12
- package/apps/shared/search/localIndexSchema.ts +20 -4
- package/apps/shared/search/localIndexTransport.ts +6 -2
- package/apps/shared/search/localIndexWorker.ts +37 -18
- package/apps/shared/search/searchTier2.ts +5 -0
- package/apps/shared/search/zeroAccessIndexSync.ts +679 -0
- 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 +225 -61
- package/apps/www/contacts/index.tsx +67 -11
- package/apps/www/index.tsx +19 -3
- package/apps/www/settings/encryption/index.tsx +21 -1
- package/apps/www/settings/privacy/index.tsx +89 -39
- package/apps/www/tasks/index.tsx +24 -6
- package/dist/apps/escrow/matters/[uid].js +2 -2
- 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 +14 -0
- package/dist/apps/shared/calendar/sealedEvents.js +85 -0
- 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 +25 -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 +8 -4
- 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 +27 -2
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +17 -0
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +98 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +3 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +32 -0
- package/dist/apps/shared/components/mail/ConversationList.js +1 -1
- package/dist/apps/shared/components/mail/InviteCard.js +11 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +46 -21
- 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/draftResume.d.ts +2 -0
- package/dist/apps/shared/components/mail/compose/draftResume.js +28 -3
- 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/compose/quotedBody.d.ts +1 -1
- package/dist/apps/shared/components/mail/compose/quotedBody.js +4 -3
- package/dist/apps/shared/components/mail/invite/inviteStore.js +16 -1
- package/dist/apps/shared/components/mail/layout/MailShell.js +2 -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/mail/reading/useMessageActions.js +4 -1
- package/dist/apps/shared/components/settings/KeyExportSetting.d.ts +13 -0
- package/dist/apps/shared/components/settings/KeyExportSetting.js +90 -0
- 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/PolicyLockedNotice.d.ts +11 -0
- package/dist/apps/shared/components/settings/PolicyLockedNotice.js +9 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.d.ts +16 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.js +80 -0
- 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/decryptedMessages.d.ts +1 -1
- package/dist/apps/shared/mail/decryptedMessages.js +11 -3
- 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/mail/reportNotices.js +3 -1
- package/dist/apps/shared/mail/zeroAccessSource.d.ts +8 -0
- package/dist/apps/shared/mail/zeroAccessSource.js +26 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +2 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +74 -24
- package/dist/apps/shared/search/localIndexBuilder.d.ts +9 -1
- package/dist/apps/shared/search/localIndexBuilder.js +50 -17
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
- package/dist/apps/shared/search/localIndexRpcClient.js +129 -11
- package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
- package/dist/apps/shared/search/localIndexSchema.js +18 -4
- package/dist/apps/shared/search/localIndexTransport.d.ts +6 -2
- package/dist/apps/shared/search/localIndexWorker.d.ts +11 -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 +102 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.js +544 -0
- 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 +88 -20
- package/dist/apps/www/contacts/index.js +64 -11
- package/dist/apps/www/index.js +19 -3
- package/dist/apps/www/settings/encryption/index.js +16 -4
- package/dist/apps/www/settings/privacy/index.js +47 -6
- package/dist/apps/www/tasks/index.js +21 -7
- package/dist/lib/admin/domainsApi.d.ts +1 -1
- package/dist/lib/admin/escrowAccessRequestsApi.d.ts +6 -0
- package/dist/lib/calendar/calendarApi.d.ts +52 -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 +86 -7
- 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 +9 -0
- package/dist/lib/calendar/sealedInvites.js +65 -0
- 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.d.ts +54 -0
- package/dist/lib/crypto/keyExport.js +161 -0
- package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
- package/dist/lib/crypto/keyPairMatch.js +65 -0
- package/dist/lib/crypto/keySession.d.ts +64 -3
- package/dist/lib/crypto/keySession.js +179 -12
- package/dist/lib/crypto/keyvaultApi.d.ts +129 -8
- package/dist/lib/crypto/keyvaultApi.js +45 -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 +56 -3
- package/dist/lib/crypto/messageSecurity.js +201 -13
- package/dist/lib/crypto/mime.d.ts +5 -0
- package/dist/lib/crypto/mime.js +25 -0
- 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 +62 -0
- package/dist/lib/crypto/sharedMailboxKeys.js +337 -0
- package/dist/lib/crypto/smimeMessage.d.ts +21 -0
- package/dist/lib/crypto/smimeMessage.js +11 -7
- package/dist/lib/crypto/zeroAccessKey.d.ts +32 -0
- package/dist/lib/crypto/zeroAccessKey.js +71 -0
- package/dist/lib/mail/conversationsApi.d.ts +6 -0
- package/dist/lib/mail/mailApi.d.ts +63 -2
- package/dist/lib/mail/mailApi.js +5 -1
- package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
- package/dist/lib/mail/mailboxImportApi.js +15 -0
- package/dist/lib/search/zeroAccessIndexApi.d.ts +85 -0
- package/dist/lib/search/zeroAccessIndexApi.js +35 -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/admin/escrowAccessRequestsApi.ts +6 -0
- package/lib/calendar/calendarApi.ts +143 -17
- package/lib/calendar/calendarMutations.ts +79 -12
- package/lib/calendar/inviteApi.ts +101 -8
- package/lib/calendar/protectedEvents.ts +143 -0
- package/lib/calendar/recurrence.ts +17 -0
- package/lib/calendar/sealedInvites.ts +73 -0
- 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 +229 -0
- package/lib/crypto/keyPairMatch.ts +69 -0
- package/lib/crypto/keySession.ts +216 -12
- package/lib/crypto/keyvaultApi.ts +174 -10
- package/lib/crypto/meetingKeys.ts +118 -0
- package/lib/crypto/messageSecurity.ts +284 -16
- package/lib/crypto/mime.ts +26 -0
- 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 +402 -0
- package/lib/crypto/smimeMessage.ts +11 -7
- package/lib/crypto/zeroAccessKey.ts +102 -0
- package/lib/mail/conversationsApi.ts +3 -0
- package/lib/mail/mailApi.ts +60 -3
- package/lib/mail/mailboxImportApi.ts +24 -0
- package/lib/search/zeroAccessIndexApi.ts +123 -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 +14 -5
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* follow-up, not a silent gap: a mailbox enrolled *only* with a passkey has no way to unlock through this
|
|
22
22
|
* module today.
|
|
23
23
|
*/
|
|
24
|
-
import { findActivePublicKey, getKeyVault } from "./keyvaultApi.js";
|
|
24
|
+
import { findActivePublicKey, getKeyVault, isOpenPgpKey } from "./keyvaultApi.js";
|
|
25
25
|
import { fromBase64 } from "./encoding.js";
|
|
26
|
+
import { certificateHoldsKey } from "./keyPairMatch.js";
|
|
26
27
|
import { importPrivateKeyPkcs8 } from "./keys.js";
|
|
27
28
|
import { KeysLockedError, buildAad, openWithKey } from "./masterKey.js";
|
|
28
29
|
import { deriveFromPassword, parseArgon2idKdfLabel } from "./passwordUnlock.js";
|
|
@@ -33,6 +34,8 @@ import { RECOVERY_KDF_LABEL, deriveFromRecoveryCode } from "./recoveryCode.js";
|
|
|
33
34
|
export const MASTER_KEY_AAD_PURPOSE = "master-key";
|
|
34
35
|
export const SIGNING_PRIVATE_KEY_AAD_PURPOSE = "sign-private-key";
|
|
35
36
|
export const ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE = "encrypt-private-key";
|
|
37
|
+
export const ZERO_ACCESS_PRIVATE_KEY_AAD_PURPOSE = "zero-access-private-key";
|
|
38
|
+
export const OPENPGP_PRIVATE_KEY_AAD_PURPOSE = "openpgp-private-key";
|
|
36
39
|
export { KeysLockedError };
|
|
37
40
|
/** How many retained (non-active) encryption keys an unlock opens at most: the most recently issued ones by
|
|
38
41
|
* `notBefore`. Each costs an AEAD open and a key import at unlock, and `smime.ts`'s `decryptEnvelopedDataWithKeys()`
|
|
@@ -89,6 +92,57 @@ export function subscribeKeySession(listener) {
|
|
|
89
92
|
listeners.delete(listener);
|
|
90
93
|
};
|
|
91
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Makes a zero-access key this device has just enrolled (`zeroAccessKey.ts`'s `provisionZeroAccessKey()`) the active one
|
|
97
|
+
* of `unlocked` - the session object the key's private half was sealed under - so zero-access mail encrypted to it reads
|
|
98
|
+
* without another unlock. Does nothing when `unlocked` is no longer this mailbox's session (it was locked or replaced
|
|
99
|
+
* meanwhile; the next unlock opens the key from the vault anyway).
|
|
100
|
+
*/
|
|
101
|
+
export function addZeroAccessKey(mailboxUid, unlocked, key) {
|
|
102
|
+
if (unlocked.destroyed || sessions.get(mailboxUid) !== unlocked) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
unlocked.zeroAccessKeys = [key, ...(unlocked.zeroAccessKeys ?? []).filter((k) => k.fingerprint !== key.fingerprint)];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Makes an OpenPGP key this device has just enrolled (`openPgpKey.ts`'s `ensureOpenPgpKey()`) the active one of `unlocked`, so
|
|
109
|
+
* OpenPGP mail to it reads without another unlock - the same rules as `addZeroAccessKey()`.
|
|
110
|
+
*/
|
|
111
|
+
export function addOpenPgpKey(mailboxUid, unlocked, key) {
|
|
112
|
+
if (unlocked.destroyed || sessions.get(mailboxUid) !== unlocked) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
unlocked.openPgpKeys = [key, ...(unlocked.openPgpKeys ?? []).filter((k) => k.fingerprint !== key.fingerprint)];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Stores the keys of a shared mailbox opened through member key wraps (`sharedMailboxKeys.ts`'s `unlockSharedMailbox()`) as that mailbox's
|
|
119
|
+
* session, exactly as an unlock would - tracked for `destroyUnlockedKeys()`, and notified - unless the mailbox was locked since `generation`
|
|
120
|
+
* was taken (`sharedUnlockGeneration()`), when they are destroyed instead and `KeysLockedError` is thrown.
|
|
121
|
+
*/
|
|
122
|
+
export function installSharedSession(mailboxUid, unlocked, generation) {
|
|
123
|
+
if (lockGeneration(mailboxUid) !== generation) {
|
|
124
|
+
destroyObject(unlocked);
|
|
125
|
+
throw new KeysLockedError();
|
|
126
|
+
}
|
|
127
|
+
unlocked.shared = true;
|
|
128
|
+
sessions.set(mailboxUid, unlocked);
|
|
129
|
+
const objects = issued.get(mailboxUid) ?? new Set();
|
|
130
|
+
objects.add(weakHandle(unlocked));
|
|
131
|
+
issued.set(mailboxUid, objects);
|
|
132
|
+
notify({ mailboxUid, state: "unlocked" });
|
|
133
|
+
}
|
|
134
|
+
/** The lock generation to pass `installSharedSession()`, taken before a shared unlock starts its requests. */
|
|
135
|
+
export function sharedUnlockGeneration(mailboxUid) {
|
|
136
|
+
return lockGeneration(mailboxUid);
|
|
137
|
+
}
|
|
138
|
+
/** Every mailbox unlocked this session with its own keys (not a shared one opened through member key wraps). */
|
|
139
|
+
export function personallyUnlockedKeys() {
|
|
140
|
+
return personalSessions().map((session) => session.unlocked);
|
|
141
|
+
}
|
|
142
|
+
/** `personallyUnlockedKeys()` with each mailbox's uid. */
|
|
143
|
+
export function personalSessions() {
|
|
144
|
+
return [...sessions].filter(([, unlocked]) => !unlocked.shared).map(([mailboxUid, unlocked]) => ({ mailboxUid, unlocked }));
|
|
145
|
+
}
|
|
92
146
|
/** Reads back a mailbox's already-unlocked keys this session, or `undefined` if it hasn't been
|
|
93
147
|
* unlocked yet (or was destroyed). Never triggers an unlock itself. */
|
|
94
148
|
export function getUnlockedKeys(mailboxUid) {
|
|
@@ -132,6 +186,14 @@ export function destroyUnlockedKeys(mailboxUid) {
|
|
|
132
186
|
sessions.delete(uid);
|
|
133
187
|
notify({ mailboxUid: uid, state: "locked" });
|
|
134
188
|
}
|
|
189
|
+
// The shared mailboxes opened with this mailbox's keys go with it.
|
|
190
|
+
if (mailboxUid) {
|
|
191
|
+
for (const [uid, unlocked] of [...sessions]) {
|
|
192
|
+
if (unlocked.shared && unlocked.openedWith?.includes(mailboxUid)) {
|
|
193
|
+
destroyUnlockedKeys(uid);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
135
197
|
}
|
|
136
198
|
function destroyObject(unlocked) {
|
|
137
199
|
unlocked.masterKey.fill(0);
|
|
@@ -143,6 +205,19 @@ function destroyObject(unlocked) {
|
|
|
143
205
|
unlocked.retainedEncryptionKeys.length = 0;
|
|
144
206
|
delete unlocked.retainedEncryptionKeys;
|
|
145
207
|
}
|
|
208
|
+
if (unlocked.zeroAccessKeys) {
|
|
209
|
+
unlocked.zeroAccessKeys.length = 0;
|
|
210
|
+
delete unlocked.zeroAccessKeys;
|
|
211
|
+
}
|
|
212
|
+
if (unlocked.openPgpKeys) {
|
|
213
|
+
// OpenPGP.js keeps a decrypted secret key's material in its own objects: wipe it there too.
|
|
214
|
+
for (const { privateKey } of unlocked.openPgpKeys) {
|
|
215
|
+
// Present at runtime on every `PrivateKey`, missing from OpenPGP.js's type declarations.
|
|
216
|
+
privateKey.clearPrivateParams();
|
|
217
|
+
}
|
|
218
|
+
unlocked.openPgpKeys.length = 0;
|
|
219
|
+
delete unlocked.openPgpKeys;
|
|
220
|
+
}
|
|
146
221
|
}
|
|
147
222
|
/** Finds the wrapped private key whose fingerprint matches a given published public key. */
|
|
148
223
|
function findWrappedPrivateKey(vault, fingerprint) {
|
|
@@ -163,10 +238,15 @@ export class UnopenableEncryptionKeyError extends Error {
|
|
|
163
238
|
}
|
|
164
239
|
}
|
|
165
240
|
/** Opens one wrapped PKCS#8 private key under `masterKey` and imports it (non-extractable by default, like
|
|
166
|
-
* `importPrivateKeyPkcs8()` itself), zeroing the transient plaintext whether or not the import succeeds.
|
|
167
|
-
|
|
241
|
+
* `importPrivateKeyPkcs8()` itself), zeroing the transient plaintext whether or not the import succeeds. With `certDer` - the server's
|
|
242
|
+
* certificate for it, unauthenticated - it rejects a key that certificate isn't the one of (`certificateHoldsKey()`), so a swapped
|
|
243
|
+
* certificate never pairs with the opened key. */
|
|
244
|
+
async function openPrivateKey(masterKey, wrapped, aad, algorithm, usages, extractable = false, certDer) {
|
|
168
245
|
const raw = await openWithKey(masterKey, wrapped, aad);
|
|
169
246
|
try {
|
|
247
|
+
if (certDer && !(await certificateHoldsKey(certDer, raw))) {
|
|
248
|
+
throw new Error("The certificate isn't the key's.");
|
|
249
|
+
}
|
|
170
250
|
return await importPrivateKeyPkcs8(raw, algorithm, usages, extractable);
|
|
171
251
|
}
|
|
172
252
|
finally {
|
|
@@ -177,7 +257,7 @@ async function openPrivateKey(masterKey, wrapped, aad, algorithm, usages, extrac
|
|
|
177
257
|
* Opens the retained encryption keys onto `unlocked.retainedEncryptionKeys`: every published `encrypt` key in
|
|
178
258
|
* `mailboxKeys` other than the active one (`activeFingerprint`), whatever its revocation or expiry, that has a wrapped
|
|
179
259
|
* private key in the vault - de-duplicated by fingerprint, newest `notBefore` first, at most
|
|
180
|
-
* `MAX_RETAINED_ENCRYPTION_KEYS`. A key that won't open or import, or whose certificate won't decode, is skipped and
|
|
260
|
+
* `MAX_RETAINED_ENCRYPTION_KEYS`. A key that won't open or import, or whose certificate won't decode or isn't the key's, is skipped and
|
|
181
261
|
* added to `unopenableKeys`. Retained keys are imported non-extractable. Each key is attached as it opens, so a failed
|
|
182
262
|
* unlock's `destroyObject()` drops those too.
|
|
183
263
|
*/
|
|
@@ -185,7 +265,7 @@ async function openRetainedEncryptionKeys(mailboxUid, mailboxKeys, vault, unlock
|
|
|
185
265
|
const seen = new Set(activeFingerprint !== undefined ? [activeFingerprint] : []);
|
|
186
266
|
const candidates = [];
|
|
187
267
|
for (const publicKey of [...mailboxKeys].sort((a, b) => b.notBefore - a.notBefore)) {
|
|
188
|
-
const wrapped = publicKey.useType === "encrypt" && !seen.has(publicKey.fingerprint) ? findWrappedPrivateKey(vault, publicKey.fingerprint) : undefined;
|
|
268
|
+
const wrapped = publicKey.useType === "encrypt" && !isOpenPgpKey(publicKey) && !seen.has(publicKey.fingerprint) ? findWrappedPrivateKey(vault, publicKey.fingerprint) : undefined;
|
|
189
269
|
if (!wrapped) {
|
|
190
270
|
continue;
|
|
191
271
|
}
|
|
@@ -199,7 +279,7 @@ async function openRetainedEncryptionKeys(mailboxUid, mailboxKeys, vault, unlock
|
|
|
199
279
|
for (const { publicKey, wrapped } of candidates) {
|
|
200
280
|
try {
|
|
201
281
|
const certDer = fromBase64(publicKey.publicKey);
|
|
202
|
-
const privateKey = await openPrivateKey(unlocked.masterKey, wrapped, aad, { name: "ECDH", namedCurve: "P-256" }, ["deriveBits"], false);
|
|
282
|
+
const privateKey = await openPrivateKey(unlocked.masterKey, wrapped, aad, { name: "ECDH", namedCurve: "P-256" }, ["deriveBits"], false, certDer);
|
|
203
283
|
(unlocked.retainedEncryptionKeys ?? (unlocked.retainedEncryptionKeys = [])).push({ fingerprint: publicKey.fingerprint, certDer, privateKey });
|
|
204
284
|
}
|
|
205
285
|
catch {
|
|
@@ -207,6 +287,78 @@ async function openRetainedEncryptionKeys(mailboxUid, mailboxKeys, vault, unlock
|
|
|
207
287
|
}
|
|
208
288
|
}
|
|
209
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Opens the mailbox's zero-access private keys into `unlocked.zeroAccessKeys`: the vault's `zeroAccessKeys` that have a
|
|
292
|
+
* wrapped private key, the unrevoked one with the latest `notBefore` first, then the rest newest first, at most
|
|
293
|
+
* `MAX_RETAINED_ENCRYPTION_KEYS + 1`. A key that won't open is listed in `unopenableKeys` and skipped - zero-access mail
|
|
294
|
+
* encrypted to it doesn't decrypt on this device, but nothing else about the unlock depends on it. So is one whose certificate (the
|
|
295
|
+
* server's, unauthenticated) isn't the private key's: this device seals to the active key's certificate - search index segments of
|
|
296
|
+
* end-to-end mail, protected meetings' keys - so a certificate swapped by the server must never become it.
|
|
297
|
+
*/
|
|
298
|
+
async function openZeroAccessKeys(mailboxUid, vault, unlocked, unopenableKeys) {
|
|
299
|
+
const ordered = [...(vault.zeroAccessKeys ?? [])].sort((a, b) => Number(!!a.revokedAt) - Number(!!b.revokedAt) || b.notBefore - a.notBefore);
|
|
300
|
+
const aad = buildAad(mailboxUid, ZERO_ACCESS_PRIVATE_KEY_AAD_PURPOSE);
|
|
301
|
+
let opened = 0;
|
|
302
|
+
for (const publicKey of ordered) {
|
|
303
|
+
const wrapped = findWrappedPrivateKey(vault, publicKey.fingerprint);
|
|
304
|
+
if (!wrapped || wrapped.useType !== "zero-access") {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const certDer = fromBase64(publicKey.publicKey);
|
|
309
|
+
const raw = await openWithKey(unlocked.masterKey, wrapped, aad);
|
|
310
|
+
try {
|
|
311
|
+
if (!(await certificateHoldsKey(certDer, raw))) {
|
|
312
|
+
throw new Error("The zero-access certificate isn't the key's.");
|
|
313
|
+
}
|
|
314
|
+
const privateKey = await importPrivateKeyPkcs8(raw, { name: "ECDH", namedCurve: "P-256" }, ["deriveBits"], false);
|
|
315
|
+
(unlocked.zeroAccessKeys ?? (unlocked.zeroAccessKeys = [])).push({ fingerprint: publicKey.fingerprint, certDer, privateKey });
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
raw.fill(0);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
unopenableKeys.push(publicKey.fingerprint);
|
|
323
|
+
}
|
|
324
|
+
if (++opened === MAX_RETAINED_ENCRYPTION_KEYS + 1) {
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Opens the mailbox's OpenPGP secret keys into `unlocked.openPgpKeys`: its published OpenPGP certificates (`type: "openpgp"`) that
|
|
331
|
+
* have a wrapped secret key, the active one first (unrevoked, unexpired, latest `notBefore`), then the rest newest first, at most
|
|
332
|
+
* `MAX_RETAINED_ENCRYPTION_KEYS + 1`. OpenPGP.js is loaded only for a mailbox that has one. A key that won't open is listed in
|
|
333
|
+
* `unopenableKeys` and skipped.
|
|
334
|
+
*/
|
|
335
|
+
async function openOpenPgpKeys(mailboxUid, mailboxKeys, vault, unlocked, unopenableKeys) {
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
const active = (key) => !key.revokedAt && key.notAfter > now;
|
|
338
|
+
const ordered = mailboxKeys
|
|
339
|
+
.filter((key) => isOpenPgpKey(key) && findWrappedPrivateKey(vault, key.fingerprint))
|
|
340
|
+
.sort((a, b) => Number(active(b)) - Number(active(a)) || b.notBefore - a.notBefore)
|
|
341
|
+
.slice(0, MAX_RETAINED_ENCRYPTION_KEYS + 1);
|
|
342
|
+
if (ordered.length === 0) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const { readSecretKey } = await import("./openpgp.js");
|
|
346
|
+
const aad = buildAad(mailboxUid, OPENPGP_PRIVATE_KEY_AAD_PURPOSE);
|
|
347
|
+
for (const publicKey of ordered) {
|
|
348
|
+
try {
|
|
349
|
+
const raw = await openWithKey(unlocked.masterKey, findWrappedPrivateKey(vault, publicKey.fingerprint), aad);
|
|
350
|
+
try {
|
|
351
|
+
(unlocked.openPgpKeys ?? (unlocked.openPgpKeys = [])).push({ fingerprint: publicKey.fingerprint, privateKey: await readSecretKey(raw) });
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
raw.fill(0);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
unopenableKeys.push(publicKey.fingerprint);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
210
362
|
/**
|
|
211
363
|
* Unlocks a mailbox's key vault with its password unlock method: fetches the vault, derives the
|
|
212
364
|
* wrapping key from `password` using the *exact* KDF parameters that wrap was created with, unwraps
|
|
@@ -219,13 +371,22 @@ async function openRetainedEncryptionKeys(mailboxUid, mailboxKeys, vault, unlock
|
|
|
219
371
|
* "incorrect password," not a generic error, but this module doesn't presume a specific UI's error copy.
|
|
220
372
|
* Once the master key has opened, the password is known to be right: an active signing key that then
|
|
221
373
|
* won't open is skipped and listed in `UnlockResult.unopenableKeys` (so is a retained encryption key); an active
|
|
222
|
-
* encryption key that won't
|
|
223
|
-
*
|
|
374
|
+
* encryption key that won't open, or whose certificate isn't its own,
|
|
375
|
+
* still fails the unlock, with `UnopenableEncryptionKeyError`. Throws `KeysLockedError` when
|
|
224
376
|
* `destroyUnlockedKeys()` locked this mailbox (or all mailboxes) while the unlock was in flight.
|
|
225
377
|
*/
|
|
226
378
|
export async function unlockWithPassword(mailboxUid, mailboxKeys, password) {
|
|
227
379
|
const generation = lockGeneration(mailboxUid);
|
|
228
|
-
const vault = await
|
|
380
|
+
const { vault, masterKey } = await openMasterKeyWithPassword(mailboxUid, password);
|
|
381
|
+
return openSession(mailboxUid, mailboxKeys, vault, masterKey, generation);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Fetches `mailboxUid`'s vault and opens its master key with the password wrap, touching no session - for an operation that
|
|
385
|
+
* must re-prove the password even while the mailbox is unlocked (`keyExport.ts`). Rejects exactly as `unlockWithPassword()`
|
|
386
|
+
* does on a missing wrap, an unknown KDF or a wrong password. The caller owns the returned master key and must zero it.
|
|
387
|
+
*/
|
|
388
|
+
export async function openMasterKeyWithPassword(mailboxUid, password, client) {
|
|
389
|
+
const vault = await getKeyVault(mailboxUid, client);
|
|
229
390
|
const passwordWrap = vault.masterKeyWraps.find((w) => w.method === "password");
|
|
230
391
|
if (!passwordWrap) {
|
|
231
392
|
throw new Error("This mailbox has no password unlock method enrolled.");
|
|
@@ -237,7 +398,7 @@ export async function unlockWithPassword(mailboxUid, mailboxKeys, password) {
|
|
|
237
398
|
const salt = fromBase64(passwordWrap.salt);
|
|
238
399
|
const { wrappingKey } = await deriveFromPassword(password, salt, kdfParams);
|
|
239
400
|
const masterKey = await openWithKey(wrappingKey, { ciphertext: passwordWrap.ciphertext, nonce: passwordWrap.nonce }, buildAad(mailboxUid, MASTER_KEY_AAD_PURPOSE));
|
|
240
|
-
return
|
|
401
|
+
return { vault, masterKey };
|
|
241
402
|
}
|
|
242
403
|
/** The one rejection for a recovery code that opens nothing - a wrong code and a vault with no recovery wraps
|
|
243
404
|
* alike, so a UI can say "that recovery code didn't work" without learning (or revealing) which it was. */
|
|
@@ -326,17 +487,23 @@ async function openSession(mailboxUid, mailboxKeys, vault, masterKey, generation
|
|
|
326
487
|
const encryptionPublicKey = findActivePublicKey(mailboxKeys, "encrypt");
|
|
327
488
|
const wrappedEncryptionKey = encryptionPublicKey && findWrappedPrivateKey(vault, encryptionPublicKey.fingerprint);
|
|
328
489
|
if (encryptionPublicKey && wrappedEncryptionKey) {
|
|
490
|
+
let encryptionCertDer;
|
|
329
491
|
try {
|
|
492
|
+
// The certificate is the server's: one that isn't this key's - what this device encrypts its own copies to, and what it
|
|
493
|
+
// hands out as its S/MIME certificate - makes the key unopenable, exactly as a wrap that won't open.
|
|
494
|
+
encryptionCertDer = fromBase64(encryptionPublicKey.publicKey);
|
|
330
495
|
const aad = buildAad(mailboxUid, ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE);
|
|
331
|
-
unlocked.encryptionPrivateKey = await openPrivateKey(masterKey, wrappedEncryptionKey, aad, { name: "ECDH", namedCurve: "P-256" }, ["deriveBits"]);
|
|
496
|
+
unlocked.encryptionPrivateKey = await openPrivateKey(masterKey, wrappedEncryptionKey, aad, { name: "ECDH", namedCurve: "P-256" }, ["deriveBits"], false, encryptionCertDer);
|
|
332
497
|
}
|
|
333
498
|
catch (err) {
|
|
334
499
|
throw new UnopenableEncryptionKeyError(encryptionPublicKey.fingerprint, err);
|
|
335
500
|
}
|
|
336
|
-
unlocked.encryptionCertDer =
|
|
501
|
+
unlocked.encryptionCertDer = encryptionCertDer;
|
|
337
502
|
unlocked.encryptionFingerprint = encryptionPublicKey.fingerprint;
|
|
338
503
|
}
|
|
339
504
|
await openRetainedEncryptionKeys(mailboxUid, mailboxKeys, vault, unlocked, encryptionPublicKey?.fingerprint, unopenableKeys);
|
|
505
|
+
await openZeroAccessKeys(mailboxUid, vault, unlocked, unopenableKeys);
|
|
506
|
+
await openOpenPgpKeys(mailboxUid, mailboxKeys, vault, unlocked, unopenableKeys);
|
|
340
507
|
if (lockGeneration(mailboxUid) !== generation) {
|
|
341
508
|
// Locked (e.g. logout) while this unlock was awaiting - never restore keys after that.
|
|
342
509
|
throw new KeysLockedError();
|
|
@@ -10,9 +10,10 @@ import { ApiClient, ApiRequestError } from "../util/api.js";
|
|
|
10
10
|
/** A cryptographic public key used to sign or encrypt messages — safe to expose publicly. Mirrors
|
|
11
11
|
* `@rapidmx/restapi`'s `PublicKey` type exactly. */
|
|
12
12
|
export interface PublicKey {
|
|
13
|
-
/** Base64-encoded DER X.509 certificate. */
|
|
13
|
+
/** Base64-encoded DER X.509 certificate - or, for `type: "openpgp"`, the binary OpenPGP transferable public key. */
|
|
14
14
|
publicKey: string;
|
|
15
|
-
/** The key's type/format
|
|
15
|
+
/** The key's type/format: `x509` (S/MIME), or `openpgp` - an OpenPGP certificate, which both encrypts and signs and is listed
|
|
16
|
+
* once, with `useType: "encrypt"` (restapi `specs/end-to-end_encryption.md` *OpenPGP Interoperability*). */
|
|
16
17
|
type: string;
|
|
17
18
|
useType: "sign" | "encrypt";
|
|
18
19
|
/** SHA-256 fingerprint of the certificate, hex encoded. */
|
|
@@ -41,7 +42,29 @@ export interface WrappedPrivateKey {
|
|
|
41
42
|
/** AEAD algorithm identifier (e.g. `AES-256-GCM`). */
|
|
42
43
|
algorithm: string;
|
|
43
44
|
fingerprint: string;
|
|
44
|
-
useType:
|
|
45
|
+
useType: KeyUseType;
|
|
46
|
+
/** `"openpgp"` for an OpenPGP secret key (sealed with `OPENPGP_PRIVATE_KEY_AAD_PURPOSE`); absent for a PKCS#8 key. */
|
|
47
|
+
type?: string;
|
|
48
|
+
}
|
|
49
|
+
/** `PublicKey.type` of an OpenPGP certificate. */
|
|
50
|
+
export declare const OPENPGP_KEY_TYPE = "openpgp";
|
|
51
|
+
/** Whether `key` is an OpenPGP certificate (or an OpenPGP secret key's wrap). */
|
|
52
|
+
export declare function isOpenPgpKey(key: {
|
|
53
|
+
type?: string;
|
|
54
|
+
} | undefined): boolean;
|
|
55
|
+
/** A key's format: `"openpgp"`, or `"x509"` for every other key (S/MIME). */
|
|
56
|
+
export type KeyFormat = "x509" | "openpgp";
|
|
57
|
+
export declare function keyFormat(key: {
|
|
58
|
+
type?: string;
|
|
59
|
+
}): KeyFormat;
|
|
60
|
+
/** What a mailbox's own key is used for. `sign`/`encrypt` keys are published (`Mailbox.keys`); `zero-access` keys never are.
|
|
61
|
+
* Mirrors `@rapidmx/restapi`'s `KeyUseType`. */
|
|
62
|
+
export type KeyUseType = "sign" | "encrypt" | "zero-access";
|
|
63
|
+
/** A mailbox's zero-access key (restapi `specs/search.md` §3): the certificate the server encrypts mail at rest to on
|
|
64
|
+
* arrival. Same shape as `PublicKey`, but never published - only the vault's reader gets it (`KeyVault.zeroAccessKeys`).
|
|
65
|
+
* Mirrors `@rapidmx/restapi`'s `ZeroAccessKey`. */
|
|
66
|
+
export interface ZeroAccessKey extends Omit<PublicKey, "useType" | "issuerCertificate"> {
|
|
67
|
+
useType: "zero-access";
|
|
45
68
|
}
|
|
46
69
|
/** One wrapped copy of the mailbox master key (MK), per unlock method. Mirrors `@rapidmx/restapi`'s
|
|
47
70
|
* `MasterKeyWrap` exactly. */
|
|
@@ -64,7 +87,12 @@ export interface MasterKeyWrap {
|
|
|
64
87
|
export interface EncryptionPreference {
|
|
65
88
|
lastSeen?: number;
|
|
66
89
|
preferEncrypt: "mutual" | "nopreference";
|
|
90
|
+
/** Which end-to-end format the address would rather receive when a sender can use either (restapi
|
|
91
|
+
* `specs/end-to-end_encryption.md` *Choosing the format*). Absent: no preference. */
|
|
92
|
+
preferFormat?: EndToEndFormat;
|
|
67
93
|
}
|
|
94
|
+
/** The two end-to-end encryption formats. */
|
|
95
|
+
export type EndToEndFormat = "smime" | "openpgp";
|
|
68
96
|
/** A newly observed key that differs from a contact's pinned key of the same `useType` - restapi records it instead of
|
|
69
97
|
* silently replacing the pin (the spec's Key Conflict Handling) until the user resolves it with `resolveKeyConflict()`.
|
|
70
98
|
* A contact holds at most one per `useType`. Mirrors `@rapidmx/restapi`'s `KeyConflict`. */
|
|
@@ -102,6 +130,9 @@ export interface KeyVault {
|
|
|
102
130
|
* (`enrollKey()`, `startSignEnrollment()`, `addMasterKeyWrap()`, `rekey()`), so restapi refuses it with `409` if
|
|
103
131
|
* another device rotated the master key meanwhile. Absent from servers that don't track it yet. */
|
|
104
132
|
masterKeyGeneration?: number;
|
|
133
|
+
/** The mailbox's zero-access certificates, newest last - returned with the vault because they are never published and
|
|
134
|
+
* an unlock needs them to decrypt zero-access mail. Absent from servers without zero-access encryption. */
|
|
135
|
+
zeroAccessKeys?: ZeroAccessKey[];
|
|
105
136
|
}
|
|
106
137
|
/** The optional optimistic check restapi applies to vault writes carrying key material sealed under the master key. */
|
|
107
138
|
export interface ExpectedMasterKeyGeneration {
|
|
@@ -121,7 +152,7 @@ export declare function getKeyVault(mailboxUid: string, client?: ApiClient): Pro
|
|
|
121
152
|
* `revocationReason` - a superseded key may still verify old mail (`isTrustedForVerification()`) but is never used for
|
|
122
153
|
* new mail. Shared by `crypto/keySession.ts` (the
|
|
123
154
|
* mailbox's own keys) and `crypto/composeSecurity.ts` (a recipient's discovered keys). */
|
|
124
|
-
export declare function findActivePublicKey(keys: PublicKey[], useType: "sign" | "encrypt"): PublicKey | undefined;
|
|
155
|
+
export declare function findActivePublicKey(keys: PublicKey[], useType: "sign" | "encrypt", format?: KeyFormat): PublicKey | undefined;
|
|
125
156
|
/**
|
|
126
157
|
* Whether `key` may still be trusted to *verify* existing signatures - never whether to sign or encrypt with it (that is
|
|
127
158
|
* `findActivePublicKey()`, which skips every revoked key). Expired keys are trusted: mail signed while a key was valid
|
|
@@ -147,13 +178,17 @@ export declare function isTrustedForVerification(key: PublicKey): boolean;
|
|
|
147
178
|
*/
|
|
148
179
|
export declare function signingKeyFingerprints(keys: PublicKey[] | undefined, previousKeys?: PreviousKey[]): string[];
|
|
149
180
|
export interface EnrollKeyInput extends ExpectedMasterKeyGeneration {
|
|
150
|
-
useType:
|
|
151
|
-
/** PEM-encoded PKCS#10 CSR — required (and only meaningful) for `useType: "encrypt"`; the server
|
|
181
|
+
useType: KeyUseType;
|
|
182
|
+
/** PEM-encoded PKCS#10 CSR — required (and only meaningful) for `useType: "encrypt"` and `"zero-access"`; the server
|
|
152
183
|
* calls its own internal CA against this CSR. */
|
|
153
184
|
csr?: string;
|
|
154
185
|
/** An already-issued PEM certificate — required (and only meaningful) for `useType: "sign"`. */
|
|
155
186
|
certificate?: string;
|
|
156
|
-
|
|
187
|
+
/** `"openpgp"` to enroll the mailbox's OpenPGP key (with `useType: "encrypt"`): self-certified, no CA. */
|
|
188
|
+
type?: string;
|
|
189
|
+
/** The OpenPGP public key (armored or base64 binary) - required, and only meaningful, with `type: "openpgp"`. */
|
|
190
|
+
publicKey?: string;
|
|
191
|
+
wrappedKey: Omit<WrappedPrivateKey, "fingerprint" | "useType" | "type">;
|
|
157
192
|
/** Only meaningful the very first time a mailbox enrolls a key at all (bootstraps its master key). */
|
|
158
193
|
masterKeyWraps?: MasterKeyWrap[];
|
|
159
194
|
}
|
|
@@ -298,6 +333,75 @@ export declare function addMasterKeyWrap(mailboxUid: string, wrap: MasterKeyWrap
|
|
|
298
333
|
* than one wrap could share the same `method` (e.g. multiple passkeys). This alone does NOT revoke
|
|
299
334
|
* access for anyone who already captured the wrapped blob — see `rekey()`. */
|
|
300
335
|
export declare function removeMasterKeyWrap(mailboxUid: string, method: string, methodId?: string, client?: ApiClient): Promise<KeyVault>;
|
|
336
|
+
/** One member's copy of a shared mailbox's zero-access private key: base64 CMS of its PKCS#8, to the member's own zero-access certificate. */
|
|
337
|
+
export interface MemberKeyWrap {
|
|
338
|
+
memberUserUid: string;
|
|
339
|
+
recipientFingerprint: string;
|
|
340
|
+
zeroAccessKeyFingerprint: string;
|
|
341
|
+
cms: string;
|
|
342
|
+
createdAt?: number;
|
|
343
|
+
}
|
|
344
|
+
/** One member in `SharedKeyStatus`. */
|
|
345
|
+
export interface SharedKeyMember {
|
|
346
|
+
userUid: string;
|
|
347
|
+
full: boolean;
|
|
348
|
+
/** Their own zero-access certificate - absent until they first sign in and get one. */
|
|
349
|
+
certificate?: ZeroAccessKey;
|
|
350
|
+
/** The mailbox's zero-access keys they hold a wrap of. */
|
|
351
|
+
wrapped: string[];
|
|
352
|
+
}
|
|
353
|
+
/** Where a shared mailbox's zero-access key stands with its members - restapi `GET /mail/mailboxes/:id/keyvault/members`. */
|
|
354
|
+
export interface SharedKeyStatus {
|
|
355
|
+
ownerless: boolean;
|
|
356
|
+
/** The caller: the mailbox's owner, one of its members, or neither (an administrator acting through a role). */
|
|
357
|
+
role?: "owner" | "member" | "other";
|
|
358
|
+
activeKeyFingerprint?: string;
|
|
359
|
+
zeroAccessKeys: ZeroAccessKey[];
|
|
360
|
+
members: SharedKeyMember[];
|
|
361
|
+
/** Members with a certificate but no wrap of the active key. */
|
|
362
|
+
pending: string[];
|
|
363
|
+
/** Users holding wraps who are no longer members. */
|
|
364
|
+
departed: string[];
|
|
365
|
+
/** A departed user holds the active key: a new key must be provisioned. */
|
|
366
|
+
rotationPending: boolean;
|
|
367
|
+
/** The caller may change the wraps. */
|
|
368
|
+
manager: boolean;
|
|
369
|
+
/** A mailbox without an owner assigned to an escrow scope: that scope. */
|
|
370
|
+
escrowScopeId?: string;
|
|
371
|
+
/** Its zero-access keys (fingerprints) with no escrow copy for `escrowScopeId` yet. Absent from servers that predate it. */
|
|
372
|
+
escrowPending?: string[];
|
|
373
|
+
}
|
|
374
|
+
/** An escrow copy of a mailbox-without-an-owner's zero-access private key, as `putZeroAccessEscrowWraps()` sends it: CMS of its PKCS#8 to
|
|
375
|
+
* the escrow scope's certificate (`getEscrowInfo()`). */
|
|
376
|
+
export interface ZeroAccessEscrowWrapInput {
|
|
377
|
+
escrowScopeId: string;
|
|
378
|
+
zeroAccessKeyFingerprint: string;
|
|
379
|
+
/** Base64 DER CMS `EnvelopedData`. */
|
|
380
|
+
cms: string;
|
|
381
|
+
}
|
|
382
|
+
export declare function getSharedKeyStatus(mailboxUid: string, client?: ApiClient): Promise<SharedKeyStatus>;
|
|
383
|
+
/** The caller's own member key wraps of `mailboxUid` (`[]` for its owner and for a member still waiting). */
|
|
384
|
+
export declare function getMyMemberKeyWraps(mailboxUid: string, client?: ApiClient): Promise<MemberKeyWrap[]>;
|
|
385
|
+
export declare function putMemberKeyWraps(mailboxUid: string, wraps: MemberKeyWrap[], client?: ApiClient): Promise<SharedKeyStatus>;
|
|
386
|
+
/** Removes every wrap of a user who is no longer a member - the last step of a rotation. */
|
|
387
|
+
export declare function removeMemberKeyWraps(mailboxUid: string, memberUserUid: string, client?: ApiClient): Promise<SharedKeyStatus>;
|
|
388
|
+
/** Provisions (or rotates) a mailbox-without-an-owner's zero-access key with its member wraps, in one write - and, for a mailbox assigned to
|
|
389
|
+
* an escrow scope, its escrow copy (required then). */
|
|
390
|
+
export declare function enrollSharedZeroAccessKey(mailboxUid: string, input: {
|
|
391
|
+
csr: string;
|
|
392
|
+
wraps: Omit<MemberKeyWrap, "zeroAccessKeyFingerprint">[];
|
|
393
|
+
escrowWrap?: Omit<ZeroAccessEscrowWrapInput, "zeroAccessKeyFingerprint">;
|
|
394
|
+
}, client?: ApiClient): Promise<SharedKeyStatus>;
|
|
395
|
+
/** Adds the escrow copies of a mailbox-without-an-owner's zero-access keys its escrow scope is missing (`SharedKeyStatus.escrowPending`). */
|
|
396
|
+
export declare function putZeroAccessEscrowWraps(mailboxUid: string, wraps: ZeroAccessEscrowWrapInput[], client?: ApiClient): Promise<SharedKeyStatus>;
|
|
397
|
+
/** One key a "Export keys for Outlook/phone" file held, as `recordKeyExport()` reports it. */
|
|
398
|
+
export interface ExportedKeyRecord {
|
|
399
|
+
kind: "zero-access" | "encrypt" | "sign";
|
|
400
|
+
fingerprint: string;
|
|
401
|
+
}
|
|
402
|
+
/** Records with restapi that the owner exported these private keys (`key_vault.export` in the audit log). `keyExport.ts`
|
|
403
|
+
* calls it before handing over the file, so an export the server refuses to record isn't handed over. Owner-only. */
|
|
404
|
+
export declare function recordKeyExport(mailboxUid: string, keys: ExportedKeyRecord[], client?: ApiClient): Promise<void>;
|
|
301
405
|
export interface RekeyInput extends ExpectedMasterKeyGeneration {
|
|
302
406
|
wrappedKeys: WrappedPrivateKey[];
|
|
303
407
|
/** Every wrap of the new master key. For a mailbox assigned an escrow scope, this must include a fresh escrow wrap
|
|
@@ -320,6 +424,11 @@ export interface KeyLookupResult {
|
|
|
320
424
|
keyConflicts?: KeyConflict[];
|
|
321
425
|
/** Keys that were pinned before, newest first - see `PreviousKey`. */
|
|
322
426
|
previousKeys?: PreviousKey[];
|
|
427
|
+
/** The format to use for a recipient reachable in both (restapi *Choosing the format*): the user's choice for the contact, else what
|
|
428
|
+
* the address advertises, else what it was last seen using. Absent when nothing says - S/MIME then. */
|
|
429
|
+
preferredFormat?: EndToEndFormat;
|
|
430
|
+
/** Where `preferredFormat` came from. */
|
|
431
|
+
preferredFormatSource?: "contact" | "advertised" | "observed";
|
|
323
432
|
}
|
|
324
433
|
/** Server-side Discovery: the server itself performs the `_rapidmx` DNS lookup and remote key-endpoint
|
|
325
434
|
* fetch (browsers can't do DNS TXT lookups, and a direct cross-origin fetch would hit CORS), persisting
|
|
@@ -329,8 +438,11 @@ export declare function lookupKeys(mailboxUid: string, addr: string, client?: Ap
|
|
|
329
438
|
/** What `trustSigner()` pins: the sender's address and the certificate that signed their message. */
|
|
330
439
|
export interface TrustSignerInput {
|
|
331
440
|
address: string;
|
|
332
|
-
/** Base64 DER of the signer certificate - `MessageSecurityResult.signerCertificate
|
|
441
|
+
/** Base64 DER of the signer certificate - `MessageSecurityResult.signerCertificate` - or, with `type: "openpgp"`, an OpenPGP
|
|
442
|
+
* public key (armored or base64 binary): a signer's, a key attached to a message, or one the user pasted. */
|
|
333
443
|
certificate: string;
|
|
444
|
+
/** `"openpgp"` to pin an OpenPGP key (it encrypts and signs); omitted for an X.509 signing certificate. */
|
|
445
|
+
type?: string;
|
|
334
446
|
}
|
|
335
447
|
/**
|
|
336
448
|
* Thrown by `trustSigner()` when restapi answers `409`: a different signing key is already pinned for that address, so
|
|
@@ -363,6 +475,8 @@ export interface ResolveKeyConflictInput {
|
|
|
363
475
|
/** For `"accept"`: base64 DER of the certificate to pin (e.g. `MessageSecurityResult.signerCertificate` of a
|
|
364
476
|
* `signer_key_changed` result). Omitted, restapi pins the recorded conflict's `observedKey`. */
|
|
365
477
|
certificate?: string;
|
|
478
|
+
/** `"openpgp"` for the contact's OpenPGP key (always `useType: "encrypt"`); omitted for its S/MIME certificate. */
|
|
479
|
+
type?: string;
|
|
366
480
|
}
|
|
367
481
|
/** The contact's resulting key state after `resolveKeyConflict()`. */
|
|
368
482
|
export type ResolveKeyConflictResult = KeyLookupResult;
|
|
@@ -386,6 +500,13 @@ export interface EncryptionPolicy {
|
|
|
386
500
|
encryptSameOrg: PolicyState;
|
|
387
501
|
encryptFederated: PolicyState;
|
|
388
502
|
encryptExternal: PolicyState;
|
|
503
|
+
/** "Allow zero-access encryption": whether mailboxes may store mail encrypted at rest. Always present from a server that
|
|
504
|
+
* supports zero-access encryption (its stored value or the server default); absent from one that doesn't, which a
|
|
505
|
+
* caller treats as not allowed. */
|
|
506
|
+
allowZeroAccess?: boolean;
|
|
507
|
+
/** "Allow OpenPGP": whether mailboxes may enroll and publish OpenPGP keys, for mail with OpenPGP users. Absent from a server
|
|
508
|
+
* that predates it, which a caller treats as not allowed. */
|
|
509
|
+
allowOpenPgp?: boolean;
|
|
389
510
|
}
|
|
390
511
|
/** The system-wide encryption policy (readable by any authenticated user, used to decide what encryption
|
|
391
512
|
* controls a compose UI should offer). */
|
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
* and `crypto/keys.ts` for the client-side cryptography that produces the values passed here.
|
|
12
12
|
*/
|
|
13
13
|
import { ApiRequestError, withClient } from "../util/api.js";
|
|
14
|
+
/** `PublicKey.type` of an OpenPGP certificate. */
|
|
15
|
+
export const OPENPGP_KEY_TYPE = "openpgp";
|
|
16
|
+
/** Whether `key` is an OpenPGP certificate (or an OpenPGP secret key's wrap). */
|
|
17
|
+
export function isOpenPgpKey(key) {
|
|
18
|
+
return key?.type === OPENPGP_KEY_TYPE;
|
|
19
|
+
}
|
|
20
|
+
export function keyFormat(key) {
|
|
21
|
+
return isOpenPgpKey(key) ? "openpgp" : "x509";
|
|
22
|
+
}
|
|
14
23
|
/** `client`, given by every function below that calls the network, is an explicit `ApiClient` from `createApiClient()`
|
|
15
24
|
* (e.g. one account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
16
25
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
@@ -24,10 +33,10 @@ export function getKeyVault(mailboxUid, client) {
|
|
|
24
33
|
* `revocationReason` - a superseded key may still verify old mail (`isTrustedForVerification()`) but is never used for
|
|
25
34
|
* new mail. Shared by `crypto/keySession.ts` (the
|
|
26
35
|
* mailbox's own keys) and `crypto/composeSecurity.ts` (a recipient's discovered keys). */
|
|
27
|
-
export function findActivePublicKey(keys, useType) {
|
|
36
|
+
export function findActivePublicKey(keys, useType, format = "x509") {
|
|
28
37
|
const now = Date.now();
|
|
29
38
|
return keys
|
|
30
|
-
.filter((k) => k.useType === useType && !k.revokedAt && k.notAfter > now)
|
|
39
|
+
.filter((k) => k.useType === useType && keyFormat(k) === format && !k.revokedAt && k.notAfter > now)
|
|
31
40
|
.sort((a, b) => b.notBefore - a.notBefore)[0];
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
@@ -248,6 +257,38 @@ export function removeMasterKeyWrap(mailboxUid, method, methodId, client) {
|
|
|
248
257
|
method: "DELETE",
|
|
249
258
|
});
|
|
250
259
|
}
|
|
260
|
+
const vaultPath = (mailboxUid) => `/mail/mailboxes/${encodeURIComponent(mailboxUid)}/keyvault`;
|
|
261
|
+
export function getSharedKeyStatus(mailboxUid, client) {
|
|
262
|
+
return withClient(client, `${vaultPath(mailboxUid)}/members`);
|
|
263
|
+
}
|
|
264
|
+
/** The caller's own member key wraps of `mailboxUid` (`[]` for its owner and for a member still waiting). */
|
|
265
|
+
export function getMyMemberKeyWraps(mailboxUid, client) {
|
|
266
|
+
return withClient(client, `${vaultPath(mailboxUid)}/member-wraps`);
|
|
267
|
+
}
|
|
268
|
+
export function putMemberKeyWraps(mailboxUid, wraps, client) {
|
|
269
|
+
return withClient(client, `${vaultPath(mailboxUid)}/member-wraps`, { method: "PUT", body: JSON.stringify({ wraps }) });
|
|
270
|
+
}
|
|
271
|
+
/** Removes every wrap of a user who is no longer a member - the last step of a rotation. */
|
|
272
|
+
export function removeMemberKeyWraps(mailboxUid, memberUserUid, client) {
|
|
273
|
+
return withClient(client, `${vaultPath(mailboxUid)}/member-wraps/${encodeURIComponent(memberUserUid)}`, { method: "DELETE" });
|
|
274
|
+
}
|
|
275
|
+
/** Provisions (or rotates) a mailbox-without-an-owner's zero-access key with its member wraps, in one write - and, for a mailbox assigned to
|
|
276
|
+
* an escrow scope, its escrow copy (required then). */
|
|
277
|
+
export function enrollSharedZeroAccessKey(mailboxUid, input, client) {
|
|
278
|
+
return withClient(client, `${vaultPath(mailboxUid)}/shared-zero-access-key`, { method: "POST", body: JSON.stringify(input) });
|
|
279
|
+
}
|
|
280
|
+
/** Adds the escrow copies of a mailbox-without-an-owner's zero-access keys its escrow scope is missing (`SharedKeyStatus.escrowPending`). */
|
|
281
|
+
export function putZeroAccessEscrowWraps(mailboxUid, wraps, client) {
|
|
282
|
+
return withClient(client, `${vaultPath(mailboxUid)}/escrow-wraps`, { method: "PUT", body: JSON.stringify({ wraps }) });
|
|
283
|
+
}
|
|
284
|
+
/** Records with restapi that the owner exported these private keys (`key_vault.export` in the audit log). `keyExport.ts`
|
|
285
|
+
* calls it before handing over the file, so an export the server refuses to record isn't handed over. Owner-only. */
|
|
286
|
+
export async function recordKeyExport(mailboxUid, keys, client) {
|
|
287
|
+
await withClient(client, `/mail/mailboxes/${encodeURIComponent(mailboxUid)}/keyvault/exports`, {
|
|
288
|
+
method: "POST",
|
|
289
|
+
body: JSON.stringify({ keys }),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
251
292
|
/** Full, atomic replacement of the mailbox's key-vault contents — the only real revocation mechanism
|
|
252
293
|
* for a captured wrap. Restricted server-side to the mailbox's actual owner. Refused (409) while a signing enrollment
|
|
253
294
|
* holding a wrapped key is in flight (see `cancelSignEnrollment()`), or when an escrowed mailbox's request carries no
|
|
@@ -288,7 +329,7 @@ export async function trustSigner(mailboxUid, input, client) {
|
|
|
288
329
|
try {
|
|
289
330
|
return await withClient(client, `/mail/mailboxes/${encodeURIComponent(mailboxUid)}/keys/trust`, {
|
|
290
331
|
method: "POST",
|
|
291
|
-
body: JSON.stringify({ address: input.address, certificate: input.certificate }),
|
|
332
|
+
body: JSON.stringify({ address: input.address, certificate: input.certificate, ...(input.type ? { type: input.type } : {}) }),
|
|
292
333
|
});
|
|
293
334
|
}
|
|
294
335
|
catch (err) {
|
|
@@ -322,6 +363,7 @@ export async function resolveKeyConflict(mailboxUid, input, client) {
|
|
|
322
363
|
action: input.action,
|
|
323
364
|
expectedPinnedFingerprint: input.expectedPinnedFingerprint,
|
|
324
365
|
...(input.certificate !== undefined ? { certificate: input.certificate } : {}),
|
|
366
|
+
...(input.type ? { type: input.type } : {}),
|
|
325
367
|
};
|
|
326
368
|
try {
|
|
327
369
|
return await withClient(client, `/mail/mailboxes/${encodeURIComponent(mailboxUid)}/keys/resolve`, {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DecryptionKey } from "./smime.js";
|
|
2
|
+
/** The `#` parameter of a link that carries a meeting key. */
|
|
3
|
+
export declare const MEETING_KEY_FRAGMENT_PARAM = "k";
|
|
4
|
+
/** A meeting key as its creator holds it. */
|
|
5
|
+
export interface GeneratedMeetingKey {
|
|
6
|
+
/** PKCS#8 DER of the private key - what a link carries and a wrap seals. */
|
|
7
|
+
pkcs8: Uint8Array;
|
|
8
|
+
/** The self-signed certificate's DER - what the server stores and data is sealed to. */
|
|
9
|
+
certificateDer: Uint8Array;
|
|
10
|
+
/** SHA-256 of `certificateDer`, lowercase hex. */
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
}
|
|
13
|
+
/** A new meeting key valid until `expiresAt` (its certificate's `notAfter`). */
|
|
14
|
+
export declare function generateMeetingKey(expiresAt: Date): Promise<GeneratedMeetingKey>;
|
|
15
|
+
/** The `#` part a guest's link carries for `pkcs8`: `k=<base64url>`. */
|
|
16
|
+
export declare function meetingKeyFragment(pkcs8: Uint8Array): string;
|
|
17
|
+
/** The meeting key in `hash` (a `location.hash`, with or without its `#`), or `undefined` when it carries none or it is malformed. */
|
|
18
|
+
export declare function readMeetingKeyFragment(hash: string): Uint8Array | undefined;
|
|
19
|
+
/** The meeting key `pkcs8` as the CMS code opens with it, and its live key for meeting `meetingUid`. Rejects a key that isn't P-256. */
|
|
20
|
+
export declare function importMeetingKey(pkcs8: Uint8Array, certificateDer: Uint8Array, meetingUid: string): Promise<{
|
|
21
|
+
decryptionKey: DecryptionKey;
|
|
22
|
+
liveKey: CryptoKey;
|
|
23
|
+
}>;
|
|
24
|
+
/** The AES-256-GCM live key of meeting `meetingUid` from `ikm`, the meeting key's private scalar. Non-extractable. */
|
|
25
|
+
export declare function deriveLiveKey(ikm: Uint8Array, meetingUid: string): Promise<CryptoKey>;
|