@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenPGP keys (restapi `specs/end-to-end_encryption.md` *OpenPGP Interoperability*): the mailbox's own key, generated here, and the
|
|
3
|
+
* keys of correspondents who use OpenPGP (Proton Mail, Thunderbird, GnuPG). Built on OpenPGP.js - which runs on WebCrypto and audited
|
|
4
|
+
* curve code, never OpenSSL - and loaded only when OpenPGP is used: this module is only ever imported dynamically.
|
|
5
|
+
*
|
|
6
|
+
* S/MIME stays RapidMX's own format; an OpenPGP key exists so OpenPGP users can reach RapidMX users, and RapidMX users them.
|
|
7
|
+
*/
|
|
8
|
+
import * as openpgp from "openpgp";
|
|
9
|
+
export type OpenPgpPrivateKey = openpgp.PrivateKey;
|
|
10
|
+
export type OpenPgpPublicKey = openpgp.PublicKey;
|
|
11
|
+
/** A freshly generated key: the public certificate to publish and the secret key to seal under the master key. */
|
|
12
|
+
export interface GeneratedOpenPgpKey {
|
|
13
|
+
/** Base64 of the binary transferable public key (`PublicKey.publicKey`). */
|
|
14
|
+
publicKey: string;
|
|
15
|
+
/** The same certificate, ASCII armored (what enrollment sends and the user can export). */
|
|
16
|
+
armoredPublicKey: string;
|
|
17
|
+
/** The binary transferable secret key, unencrypted - the caller seals it under the master key and zeroes this copy. */
|
|
18
|
+
secretKey: Uint8Array;
|
|
19
|
+
/** The primary key's fingerprint (40 hex characters for a v4 key, 64 for v6). */
|
|
20
|
+
fingerprint: string;
|
|
21
|
+
}
|
|
22
|
+
/** Which OpenPGP key version to generate: 4 (RFC 4880, the default - what every OpenPGP app reads, GnuPG included) or 6 (RFC 9580). */
|
|
23
|
+
export type OpenPgpKeyVersion = 4 | 6;
|
|
24
|
+
/**
|
|
25
|
+
* Generates the mailbox's OpenPGP key: an Ed25519 primary key with a Curve25519 encryption subkey and one user ID,
|
|
26
|
+
* `displayName <address>`, never expiring (rotation replaces it). A v4 key uses the long-established EdDSA/ECDH Curve25519
|
|
27
|
+
* algorithms; a v6 key RFC 9580's Ed25519/X25519. It carries no passphrase: like every other private key, its secret half is
|
|
28
|
+
* sealed under the master key before it leaves this device.
|
|
29
|
+
*/
|
|
30
|
+
export declare function generateOpenPgpKey(address: string, displayName?: string, version?: OpenPgpKeyVersion): Promise<GeneratedOpenPgpKey>;
|
|
31
|
+
/** A rotation's new key with its user ID certified by the key it replaces, so correspondents who pinned the old one accept the
|
|
32
|
+
* new one without asking (restapi `OpenPgpKeyUtils.canReplaceOpenPgpKeyAutomatically()`). */
|
|
33
|
+
export declare function certifyWithPredecessor(successor: GeneratedOpenPgpKey, predecessor: OpenPgpPrivateKey): Promise<GeneratedOpenPgpKey>;
|
|
34
|
+
/** The secret key in `bytes` (a sealed `WrappedPrivateKey`'s plaintext), ready to decrypt and sign with. */
|
|
35
|
+
export declare function readSecretKey(bytes: Uint8Array): Promise<OpenPgpPrivateKey>;
|
|
36
|
+
/** The certificate in `base64` (`PublicKey.publicKey`), or `undefined` when it isn't one. */
|
|
37
|
+
export declare function readPublicKey(base64: string): Promise<OpenPgpPublicKey | undefined>;
|
|
38
|
+
/** Every certificate in an armored or binary key block (e.g. an attached `application/pgp-keys` part, or a pasted one). */
|
|
39
|
+
export declare function readPublicKeys(block: string | Uint8Array): Promise<OpenPgpPublicKey[]>;
|
|
40
|
+
/** The email addresses a certificate's user IDs name with a verifying self-signature, lowercased. */
|
|
41
|
+
export declare function keyEmails(key: OpenPgpPublicKey): Promise<string[]>;
|
|
42
|
+
/** A certificate's armored form, for export or for attaching to a message. */
|
|
43
|
+
export declare function armorPublicKey(key: OpenPgpPublicKey): string;
|
|
44
|
+
/** A secret key armored and protected with `passphrase` - the export a user imports into another OpenPGP app. */
|
|
45
|
+
export declare function exportProtectedSecretKey(key: OpenPgpPrivateKey, passphrase: string): Promise<string>;
|
|
46
|
+
/** `key`'s public certificate as base64 (`PublicKey.publicKey`). */
|
|
47
|
+
export declare function publicKeyBase64(key: OpenPgpPrivateKey | OpenPgpPublicKey): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* OpenPGP keys (restapi `specs/end-to-end_encryption.md` *OpenPGP Interoperability*): the mailbox's own key, generated here, and the
|
|
7
|
+
* keys of correspondents who use OpenPGP (Proton Mail, Thunderbird, GnuPG). Built on OpenPGP.js - which runs on WebCrypto and audited
|
|
8
|
+
* curve code, never OpenSSL - and loaded only when OpenPGP is used: this module is only ever imported dynamically.
|
|
9
|
+
*
|
|
10
|
+
* S/MIME stays RapidMX's own format; an OpenPGP key exists so OpenPGP users can reach RapidMX users, and RapidMX users them.
|
|
11
|
+
*/
|
|
12
|
+
import * as openpgp from "openpgp";
|
|
13
|
+
import { fromBase64, toBase64 } from "./encoding.js";
|
|
14
|
+
/**
|
|
15
|
+
* Generates the mailbox's OpenPGP key: an Ed25519 primary key with a Curve25519 encryption subkey and one user ID,
|
|
16
|
+
* `displayName <address>`, never expiring (rotation replaces it). A v4 key uses the long-established EdDSA/ECDH Curve25519
|
|
17
|
+
* algorithms; a v6 key RFC 9580's Ed25519/X25519. It carries no passphrase: like every other private key, its secret half is
|
|
18
|
+
* sealed under the master key before it leaves this device.
|
|
19
|
+
*/
|
|
20
|
+
export async function generateOpenPgpKey(address, displayName, version = 4) {
|
|
21
|
+
const userIDs = [{ ...(displayName ? { name: displayName } : {}), email: address }];
|
|
22
|
+
const { privateKey, publicKey } = version === 6
|
|
23
|
+
? await openpgp.generateKey({ type: "curve25519", userIDs, format: "object", config: { v6Keys: true } })
|
|
24
|
+
: await openpgp.generateKey({ type: "ecc", curve: "curve25519Legacy", userIDs, format: "object" });
|
|
25
|
+
return {
|
|
26
|
+
publicKey: toBase64(publicKey.write()),
|
|
27
|
+
armoredPublicKey: publicKey.armor(),
|
|
28
|
+
secretKey: privateKey.write(),
|
|
29
|
+
fingerprint: publicKey.getFingerprint(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** A rotation's new key with its user ID certified by the key it replaces, so correspondents who pinned the old one accept the
|
|
33
|
+
* new one without asking (restapi `OpenPgpKeyUtils.canReplaceOpenPgpKeyAutomatically()`). */
|
|
34
|
+
export async function certifyWithPredecessor(successor, predecessor) {
|
|
35
|
+
const secret = await openpgp.readPrivateKey({ binaryKey: successor.secretKey });
|
|
36
|
+
const certified = await secret.signPrimaryUser([predecessor]);
|
|
37
|
+
const publicKey = certified.toPublic();
|
|
38
|
+
return { ...successor, publicKey: toBase64(publicKey.write()), armoredPublicKey: publicKey.armor(), secretKey: certified.write() };
|
|
39
|
+
}
|
|
40
|
+
/** The secret key in `bytes` (a sealed `WrappedPrivateKey`'s plaintext), ready to decrypt and sign with. */
|
|
41
|
+
export async function readSecretKey(bytes) {
|
|
42
|
+
return await openpgp.readPrivateKey({ binaryKey: bytes });
|
|
43
|
+
}
|
|
44
|
+
/** The certificate in `base64` (`PublicKey.publicKey`), or `undefined` when it isn't one. */
|
|
45
|
+
export async function readPublicKey(base64) {
|
|
46
|
+
try {
|
|
47
|
+
return await openpgp.readKey({ binaryKey: fromBase64(base64) });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Every certificate in an armored or binary key block (e.g. an attached `application/pgp-keys` part, or a pasted one). */
|
|
54
|
+
export async function readPublicKeys(block) {
|
|
55
|
+
try {
|
|
56
|
+
const keys = typeof block === "string" && block.includes("-----BEGIN PGP PUBLIC KEY BLOCK-----")
|
|
57
|
+
? await openpgp.readKeys({ armoredKeys: block })
|
|
58
|
+
: await openpgp.readKeys({ binaryKeys: typeof block === "string" ? fromBase64(block.replace(/\s+/g, "")) : block });
|
|
59
|
+
return keys.filter((key) => !key.isPrivate());
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** The email addresses a certificate's user IDs name with a verifying self-signature, lowercased. */
|
|
66
|
+
export async function keyEmails(key) {
|
|
67
|
+
const emails = [];
|
|
68
|
+
for (const user of key.users) {
|
|
69
|
+
const email = user.userID?.email;
|
|
70
|
+
if (email && (await user.verify().then(() => true, () => false))) {
|
|
71
|
+
emails.push(email.toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return emails;
|
|
75
|
+
}
|
|
76
|
+
/** A certificate's armored form, for export or for attaching to a message. */
|
|
77
|
+
export function armorPublicKey(key) {
|
|
78
|
+
return key.armor();
|
|
79
|
+
}
|
|
80
|
+
/** A secret key armored and protected with `passphrase` - the export a user imports into another OpenPGP app. */
|
|
81
|
+
export async function exportProtectedSecretKey(key, passphrase) {
|
|
82
|
+
const copy = await openpgp.readPrivateKey({ binaryKey: key.write() });
|
|
83
|
+
return (await openpgp.encryptKey({ privateKey: copy, passphrase })).armor();
|
|
84
|
+
}
|
|
85
|
+
/** `key`'s public certificate as base64 (`PublicKey.publicKey`). */
|
|
86
|
+
export function publicKeyBase64(key) {
|
|
87
|
+
return toBase64(key.toPublic().write());
|
|
88
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type OpenPgpPrivateKey, type OpenPgpPublicKey } from "./openpgp.js";
|
|
2
|
+
import { type MimePart, type ProtectedHeaders } from "./smimeMessage.js";
|
|
3
|
+
/**
|
|
4
|
+
* An RFC 3156 `multipart/encrypted` message: the inner entity (protected headers, the body and - with `attachKey` - our public
|
|
5
|
+
* key) signed with `signingKey` when given and encrypted to every key in `recipientKeys` in one OpenPGP message. Per the spec's
|
|
6
|
+
* "Encrypt to Self", `recipientKeys` MUST include the sender's own key.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildPgpEncryptedMessage(bodyContentType: string, bodyText: string, protectedHeaders: ProtectedHeaders, outerHeaders: ProtectedHeaders, recipientKeys: OpenPgpPublicKey[], signingKey?: OpenPgpPrivateKey, attachKey?: string): Promise<MimePart>;
|
|
9
|
+
/** An RFC 3156 detached `multipart/signed` message: the inner entity (protected headers, the body and - with `attachKey` - our
|
|
10
|
+
* public key), signed with `signingKey`. */
|
|
11
|
+
export declare function buildPgpSignedOnlyMessage(bodyContentType: string, bodyText: string, protectedHeaders: ProtectedHeaders, signingKey: OpenPgpPrivateKey, attachKey?: string): Promise<MimePart>;
|
|
12
|
+
/** A key that may have made a signature: a pinned one is trusted, one only attached to the message is not. */
|
|
13
|
+
export interface PgpSignerCandidate {
|
|
14
|
+
key: OpenPgpPublicKey;
|
|
15
|
+
trusted: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** What a signature proved. `"unknown_key"`: no candidate key has the signing key's ID, so it can't be checked at all. */
|
|
18
|
+
export type PgpSignatureOutcome = {
|
|
19
|
+
status: "verified";
|
|
20
|
+
candidate: PgpSignerCandidate;
|
|
21
|
+
fingerprint: string;
|
|
22
|
+
emails: string[];
|
|
23
|
+
} | {
|
|
24
|
+
status: "invalid";
|
|
25
|
+
} | {
|
|
26
|
+
status: "unknown_key";
|
|
27
|
+
};
|
|
28
|
+
/** A recovered OpenPGP message: the inner entity (a binary string, as `mime.ts` parses it) and what its signature proved. */
|
|
29
|
+
export interface ParsedPgpMessage {
|
|
30
|
+
/** For an encrypted message: whether one of the keys opened it. Always `true` for a signed-only one. */
|
|
31
|
+
decrypted: boolean;
|
|
32
|
+
/** The recovered MIME entity (PGP/MIME), or for an inline message a `text/plain` entity built around its text. */
|
|
33
|
+
entityText?: string;
|
|
34
|
+
/** `undefined` when the message carried no signature. */
|
|
35
|
+
signature?: PgpSignatureOutcome;
|
|
36
|
+
}
|
|
37
|
+
/** Verifies an RFC 3156 `multipart/signed` message (`protocol="application/pgp-signature"`). Never throws. */
|
|
38
|
+
export declare function parsePgpSignedMessage(contentType: string, body: string, candidates: PgpSignerCandidate[]): Promise<ParsedPgpMessage>;
|
|
39
|
+
/**
|
|
40
|
+
* Decrypts an RFC 3156 `multipart/encrypted` message with `decryptionKeys`, verifying the signature it carries (a combined signed
|
|
41
|
+
* and encrypted message, RFC 3156 §6.2, or an inner `multipart/signed`, §6.1) against `candidates` and the keys attached inside.
|
|
42
|
+
* Never throws.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parsePgpEncryptedMessage(contentType: string, body: string, decryptionKeys: OpenPgpPrivateKey[], candidates: PgpSignerCandidate[]): Promise<ParsedPgpMessage>;
|
|
45
|
+
/** Decrypts an inline OpenPGP message (an armored `PGP MESSAGE` as the whole text body), verifying a signature it carries. Never
|
|
46
|
+
* throws. */
|
|
47
|
+
export declare function parsePgpInlineEncrypted(armored: string, decryptionKeys: OpenPgpPrivateKey[], candidates: PgpSignerCandidate[]): Promise<ParsedPgpMessage>;
|
|
48
|
+
/** Verifies an inline cleartext-signed message (`BEGIN PGP SIGNED MESSAGE`). Never throws. */
|
|
49
|
+
export declare function parsePgpCleartextSigned(text: string, candidates: PgpSignerCandidate[]): Promise<ParsedPgpMessage>;
|
|
50
|
+
/** The certificates in a list of base64 keys (`PublicKey.publicKey` of OpenPGP entries) - unreadable ones dropped. */
|
|
51
|
+
export declare function readKeysBase64(keys: string[]): Promise<OpenPgpPublicKey[]>;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* OpenPGP messages (RFC 3156 PGP/MIME, and the inline forms some clients still send) - the OpenPGP counterpart of
|
|
7
|
+
* `smimeMessage.ts`, for mail with OpenPGP users (restapi `specs/end-to-end_encryption.md` *OpenPGP Interoperability*). Only ever
|
|
8
|
+
* imported dynamically: OpenPGP.js is loaded when an OpenPGP message is read or sent.
|
|
9
|
+
*
|
|
10
|
+
* Sending writes PGP/MIME only: `multipart/encrypted` (signed and encrypted in one OpenPGP message, RFC 3156 §6.2) and
|
|
11
|
+
* detached `multipart/signed`. The inner entity carries the protected headers exactly as an S/MIME message does
|
|
12
|
+
* (`smimeMessage.ts`'s `protectedHeaderLines()`, RFC 9788), marked both `hp=` and `protected-headers="v1"` (the form OpenPGP
|
|
13
|
+
* mail clients recognise), and its body is base64 so the signed bytes survive transport.
|
|
14
|
+
*
|
|
15
|
+
* Reading recovers the inner entity and reports what the signature proved; the trust decision (pins, identity binding,
|
|
16
|
+
* header checks) is `messageSecurity.ts`'s, the same as for S/MIME.
|
|
17
|
+
*/
|
|
18
|
+
import * as openpgp from "openpgp";
|
|
19
|
+
import { fromBase64 } from "./encoding.js";
|
|
20
|
+
import { bytesToBinaryString, binaryStringToBytes, decodeBodyText, parseMimeEntity, parseParameterizedHeader, splitMultipart } from "./mime.js";
|
|
21
|
+
import { keyEmails, readPublicKeys } from "./openpgp.js";
|
|
22
|
+
import { base64Wrapped, flattenHeaderValue, generateBoundary, protectedHeaderLines, } from "./smimeMessage.js";
|
|
23
|
+
const CRLF = "\r\n";
|
|
24
|
+
/** Builds the inner entity: protected headers, then the body (base64) - wrapped with `attachKey` (an armored public key) in a
|
|
25
|
+
* `multipart/mixed` when one is given, so a correspondent without our key can import it. */
|
|
26
|
+
function innerEntity(bodyContentType, bodyText, headerBlock, hp, attachKey) {
|
|
27
|
+
const marks = `hp="${hp}"; protected-headers="v1"`;
|
|
28
|
+
const bodyPart = [
|
|
29
|
+
`Content-Type: ${flattenHeaderValue(bodyContentType)}`,
|
|
30
|
+
`Content-Transfer-Encoding: base64`,
|
|
31
|
+
"",
|
|
32
|
+
base64Wrapped(new TextEncoder().encode(bodyText)),
|
|
33
|
+
].join(CRLF);
|
|
34
|
+
if (!attachKey) {
|
|
35
|
+
return [headerBlock, bodyPart.replace(/^Content-Type: ([^\r\n]*)/, `Content-Type: $1; ${marks}`)].join(CRLF);
|
|
36
|
+
}
|
|
37
|
+
const boundary = generateBoundary();
|
|
38
|
+
return [
|
|
39
|
+
headerBlock,
|
|
40
|
+
`Content-Type: multipart/mixed; boundary="${boundary}"; ${marks}`,
|
|
41
|
+
"",
|
|
42
|
+
`--${boundary}`,
|
|
43
|
+
bodyPart,
|
|
44
|
+
`--${boundary}`,
|
|
45
|
+
`Content-Type: application/pgp-keys; name="OpenPGP_public_key.asc"`,
|
|
46
|
+
`Content-Disposition: attachment; filename="OpenPGP_public_key.asc"`,
|
|
47
|
+
"",
|
|
48
|
+
attachKey.replace(/\r?\n/g, CRLF).trim(),
|
|
49
|
+
`--${boundary}--`,
|
|
50
|
+
].join(CRLF);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* An RFC 3156 `multipart/encrypted` message: the inner entity (protected headers, the body and - with `attachKey` - our public
|
|
54
|
+
* key) signed with `signingKey` when given and encrypted to every key in `recipientKeys` in one OpenPGP message. Per the spec's
|
|
55
|
+
* "Encrypt to Self", `recipientKeys` MUST include the sender's own key.
|
|
56
|
+
*/
|
|
57
|
+
export async function buildPgpEncryptedMessage(bodyContentType, bodyText, protectedHeaders, outerHeaders, recipientKeys, signingKey, attachKey) {
|
|
58
|
+
const entity = innerEntity(bodyContentType, bodyText, protectedHeaderLines(protectedHeaders, outerHeaders), "cipher", attachKey);
|
|
59
|
+
const armored = await openpgp.encrypt({
|
|
60
|
+
message: await openpgp.createMessage({ binary: new TextEncoder().encode(entity) }),
|
|
61
|
+
encryptionKeys: recipientKeys,
|
|
62
|
+
...(signingKey ? { signingKeys: signingKey } : {}),
|
|
63
|
+
format: "armored",
|
|
64
|
+
});
|
|
65
|
+
const boundary = generateBoundary();
|
|
66
|
+
const body = [
|
|
67
|
+
`--${boundary}`,
|
|
68
|
+
`Content-Type: application/pgp-encrypted`,
|
|
69
|
+
`Content-Description: PGP/MIME version identification`,
|
|
70
|
+
"",
|
|
71
|
+
"Version: 1",
|
|
72
|
+
"",
|
|
73
|
+
`--${boundary}`,
|
|
74
|
+
`Content-Type: application/octet-stream; name="encrypted.asc"`,
|
|
75
|
+
`Content-Description: OpenPGP encrypted message`,
|
|
76
|
+
`Content-Disposition: inline; filename="encrypted.asc"`,
|
|
77
|
+
"",
|
|
78
|
+
armored.replace(/\r?\n/g, CRLF).trim(),
|
|
79
|
+
`--${boundary}--`,
|
|
80
|
+
].join(CRLF);
|
|
81
|
+
return { contentType: `multipart/encrypted; protocol="application/pgp-encrypted"; boundary="${boundary}"`, body };
|
|
82
|
+
}
|
|
83
|
+
/** The `micalg` of a detached signature: `pgp-` and its hash algorithm's name (RFC 3156 §5). */
|
|
84
|
+
async function micalg(armoredSignature) {
|
|
85
|
+
const signature = await openpgp.readSignature({ armoredSignature });
|
|
86
|
+
const id = signature.packets[0]?.hashAlgorithm;
|
|
87
|
+
const name = id !== undefined && id !== null ? openpgp.enums.read(openpgp.enums.hash, id) : "sha512";
|
|
88
|
+
return `pgp-${String(name).toLowerCase()}`;
|
|
89
|
+
}
|
|
90
|
+
/** An RFC 3156 detached `multipart/signed` message: the inner entity (protected headers, the body and - with `attachKey` - our
|
|
91
|
+
* public key), signed with `signingKey`. */
|
|
92
|
+
export async function buildPgpSignedOnlyMessage(bodyContentType, bodyText, protectedHeaders, signingKey, attachKey) {
|
|
93
|
+
const entity = innerEntity(bodyContentType, bodyText, protectedHeaderLines(protectedHeaders), "clear", attachKey);
|
|
94
|
+
const signature = await openpgp.sign({
|
|
95
|
+
message: await openpgp.createMessage({ binary: new TextEncoder().encode(entity) }),
|
|
96
|
+
signingKeys: signingKey,
|
|
97
|
+
detached: true,
|
|
98
|
+
format: "armored",
|
|
99
|
+
});
|
|
100
|
+
const boundary = generateBoundary();
|
|
101
|
+
const body = [
|
|
102
|
+
`--${boundary}`,
|
|
103
|
+
entity,
|
|
104
|
+
`--${boundary}`,
|
|
105
|
+
`Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"`,
|
|
106
|
+
`Content-Description: OpenPGP digital signature`,
|
|
107
|
+
`Content-Disposition: attachment; filename="OpenPGP_signature.asc"`,
|
|
108
|
+
"",
|
|
109
|
+
signature.replace(/\r?\n/g, CRLF).trim(),
|
|
110
|
+
`--${boundary}--`,
|
|
111
|
+
].join(CRLF);
|
|
112
|
+
return {
|
|
113
|
+
contentType: `multipart/signed; protocol="application/pgp-signature"; micalg=${await micalg(signature)}; boundary="${boundary}"`,
|
|
114
|
+
body,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function signatureOutcome(signatures, candidates) {
|
|
118
|
+
let known = false;
|
|
119
|
+
for (const signature of signatures) {
|
|
120
|
+
const candidate = candidates.find((c) => c.key.getKeyIDs().some((id) => id.equals(signature.keyID)));
|
|
121
|
+
if (!candidate) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
known = true;
|
|
125
|
+
const valid = await signature.verified.then(() => true, () => false);
|
|
126
|
+
if (valid) {
|
|
127
|
+
return { status: "verified", candidate, fingerprint: candidate.key.getFingerprint().toLowerCase(), emails: await keyEmails(candidate.key) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return known ? { status: "invalid" } : { status: "unknown_key" };
|
|
131
|
+
}
|
|
132
|
+
/** The certificates attached to `entityText` (`application/pgp-keys` parts) - untrusted signer candidates. */
|
|
133
|
+
async function attachedKeys(entityText) {
|
|
134
|
+
const keys = [];
|
|
135
|
+
const visit = async (text, depth) => {
|
|
136
|
+
if (depth > 4) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const entity = parseMimeEntity(text);
|
|
140
|
+
const type = parseParameterizedHeader(entity.headers["content-type"]);
|
|
141
|
+
if (type.value === "application/pgp-keys") {
|
|
142
|
+
keys.push(...(await readPublicKeys(decodeBodyText(entity))));
|
|
143
|
+
}
|
|
144
|
+
else if (type.value.startsWith("multipart/") && type.params["boundary"]) {
|
|
145
|
+
for (const part of splitMultipart(entity.body, type.params["boundary"]).slice(0, 16)) {
|
|
146
|
+
await visit(part, depth + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
await visit(entityText, 0);
|
|
151
|
+
return keys;
|
|
152
|
+
}
|
|
153
|
+
/** Candidates for `entityText`: the given ones, then any key attached to the message (untrusted). */
|
|
154
|
+
async function withAttached(candidates, entityText) {
|
|
155
|
+
return [...candidates, ...(await attachedKeys(entityText)).map((key) => ({ key, trusted: false }))];
|
|
156
|
+
}
|
|
157
|
+
/** Verifies an RFC 3156 `multipart/signed` message (`protocol="application/pgp-signature"`). Never throws. */
|
|
158
|
+
export async function parsePgpSignedMessage(contentType, body, candidates) {
|
|
159
|
+
const boundary = parseParameterizedHeader(contentType).params["boundary"];
|
|
160
|
+
const parts = boundary ? splitMultipart(body, boundary) : [];
|
|
161
|
+
if (parts.length !== 2) {
|
|
162
|
+
return { decrypted: true, signature: { status: "invalid" } };
|
|
163
|
+
}
|
|
164
|
+
const [signed, signaturePart] = parts;
|
|
165
|
+
try {
|
|
166
|
+
const signature = await openpgp.readSignature({ armoredSignature: decodeBodyText(parseMimeEntity(signaturePart)) });
|
|
167
|
+
const all = await withAttached(candidates, signed);
|
|
168
|
+
// What was signed is the part in canonical CRLF form (RFC 3156 §5).
|
|
169
|
+
const canonical = signed.replace(/\r?\n/g, CRLF);
|
|
170
|
+
const verification = await openpgp.verify({
|
|
171
|
+
message: await openpgp.createMessage({ binary: binaryStringToBytes(canonical) }),
|
|
172
|
+
signature,
|
|
173
|
+
verificationKeys: all.map((c) => c.key),
|
|
174
|
+
expectSigned: false,
|
|
175
|
+
});
|
|
176
|
+
return { decrypted: true, entityText: signed, signature: await signatureOutcome(verification.signatures, all) };
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return { decrypted: true, signature: { status: "invalid" } };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** The armored OpenPGP message in an RFC 3156 `multipart/encrypted` body (its second part), or `undefined`. */
|
|
183
|
+
function encryptedPayload(contentType, body) {
|
|
184
|
+
const boundary = parseParameterizedHeader(contentType).params["boundary"];
|
|
185
|
+
const parts = boundary ? splitMultipart(body, boundary) : [];
|
|
186
|
+
return parts.length === 2 ? decodeBodyText(parseMimeEntity(parts[1])) : undefined;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Decrypts an RFC 3156 `multipart/encrypted` message with `decryptionKeys`, verifying the signature it carries (a combined signed
|
|
190
|
+
* and encrypted message, RFC 3156 §6.2, or an inner `multipart/signed`, §6.1) against `candidates` and the keys attached inside.
|
|
191
|
+
* Never throws.
|
|
192
|
+
*/
|
|
193
|
+
export async function parsePgpEncryptedMessage(contentType, body, decryptionKeys, candidates) {
|
|
194
|
+
const payload = encryptedPayload(contentType, body);
|
|
195
|
+
if (!payload || decryptionKeys.length === 0) {
|
|
196
|
+
return { decrypted: false };
|
|
197
|
+
}
|
|
198
|
+
let data;
|
|
199
|
+
let signed;
|
|
200
|
+
try {
|
|
201
|
+
const result = await openpgp.decrypt({ message: await openpgp.readMessage({ armoredMessage: payload }), decryptionKeys, format: "binary", expectSigned: false });
|
|
202
|
+
data = result.data;
|
|
203
|
+
signed = result.signatures.length > 0;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return { decrypted: false };
|
|
207
|
+
}
|
|
208
|
+
const entityText = bytesToBinaryString(data);
|
|
209
|
+
const inner = parseMimeEntity(entityText);
|
|
210
|
+
const innerType = parseParameterizedHeader(inner.headers["content-type"]);
|
|
211
|
+
if (!signed && innerType.value === "multipart/signed" && innerType.params["protocol"]?.toLowerCase() === "application/pgp-signature") {
|
|
212
|
+
const innerSigned = await parsePgpSignedMessage(inner.headers["content-type"], inner.body, candidates);
|
|
213
|
+
return { decrypted: true, entityText: innerSigned.entityText ?? entityText, signature: innerSigned.signature };
|
|
214
|
+
}
|
|
215
|
+
if (!signed) {
|
|
216
|
+
return { decrypted: true, entityText };
|
|
217
|
+
}
|
|
218
|
+
// The keys attached inside are only known now: verify the signature against them and the given ones together.
|
|
219
|
+
const all = await withAttached(candidates, entityText);
|
|
220
|
+
if (all.length === 0) {
|
|
221
|
+
return { decrypted: true, entityText, signature: { status: "unknown_key" } };
|
|
222
|
+
}
|
|
223
|
+
const verified = await openpgp.decrypt({
|
|
224
|
+
message: await openpgp.readMessage({ armoredMessage: payload }),
|
|
225
|
+
decryptionKeys,
|
|
226
|
+
verificationKeys: all.map((c) => c.key),
|
|
227
|
+
format: "binary",
|
|
228
|
+
expectSigned: false,
|
|
229
|
+
});
|
|
230
|
+
return { decrypted: true, entityText, signature: await signatureOutcome(verified.signatures, all) };
|
|
231
|
+
}
|
|
232
|
+
/** A `text/plain` entity for an inline message's recovered text. */
|
|
233
|
+
function plainEntity(text) {
|
|
234
|
+
return bytesToBinaryString(new TextEncoder().encode(`Content-Type: text/plain; charset=utf-8${CRLF}${CRLF}${text}`));
|
|
235
|
+
}
|
|
236
|
+
/** Decrypts an inline OpenPGP message (an armored `PGP MESSAGE` as the whole text body), verifying a signature it carries. Never
|
|
237
|
+
* throws. */
|
|
238
|
+
export async function parsePgpInlineEncrypted(armored, decryptionKeys, candidates) {
|
|
239
|
+
if (decryptionKeys.length === 0) {
|
|
240
|
+
return { decrypted: false };
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const result = await openpgp.decrypt({
|
|
244
|
+
message: await openpgp.readMessage({ armoredMessage: armored }),
|
|
245
|
+
decryptionKeys,
|
|
246
|
+
verificationKeys: candidates.map((c) => c.key),
|
|
247
|
+
expectSigned: false,
|
|
248
|
+
});
|
|
249
|
+
const signature = result.signatures.length > 0 ? await signatureOutcome(result.signatures, candidates) : undefined;
|
|
250
|
+
return { decrypted: true, entityText: plainEntity(String(result.data)), ...(signature ? { signature } : {}) };
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return { decrypted: false };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** Verifies an inline cleartext-signed message (`BEGIN PGP SIGNED MESSAGE`). Never throws. */
|
|
257
|
+
export async function parsePgpCleartextSigned(text, candidates) {
|
|
258
|
+
try {
|
|
259
|
+
const message = await openpgp.readCleartextMessage({ cleartextMessage: text });
|
|
260
|
+
const verification = await openpgp.verify({ message, verificationKeys: candidates.map((c) => c.key), expectSigned: false });
|
|
261
|
+
return { decrypted: true, entityText: plainEntity(message.getText()), signature: await signatureOutcome(verification.signatures, candidates) };
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return { decrypted: true, signature: { status: "invalid" } };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** The certificates in a list of base64 keys (`PublicKey.publicKey` of OpenPGP entries) - unreadable ones dropped. */
|
|
268
|
+
export async function readKeysBase64(keys) {
|
|
269
|
+
const read = await Promise.all(keys.map((key) => readPublicKeys(fromBase64(key))));
|
|
270
|
+
return read.flat();
|
|
271
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared mailboxes' zero-access keys (restapi `specs/search.md` §3 *Shared Mailboxes*). A mailbox someone else owns, or that has no owner,
|
|
3
|
+
* has no master key its members know: its zero-access private key is instead wrapped to each member's own zero-access certificate
|
|
4
|
+
* (member key wraps), and a member opens it with the zero-access key of a mailbox they own - so a member unlocks it without a password,
|
|
5
|
+
* once their own mailbox is unlocked. Members get the ability to read, never the master key, signing key or encryption keys.
|
|
6
|
+
*
|
|
7
|
+
* `unlockSharedMailbox()` opens a shared mailbox for its member. `maintainSharedMailbox()` is what the client of the owner, or of a member
|
|
8
|
+
* with full access, does in the background: provisions a key for a mailbox without an owner, wraps the keys for members still waiting,
|
|
9
|
+
* and - once someone who held the key has left - rotates it, so new mail is encrypted to a key they never had. A mailbox without an owner
|
|
10
|
+
* has no master key for escrow to open, so when it is assigned to an escrow scope each of its keys is also wrapped to the scope's
|
|
11
|
+
* certificate: with a new key in the same write, and for keys the scope is still missing (`escrowPending`) in the background.
|
|
12
|
+
*/
|
|
13
|
+
import { type ApiClient } from "../util/api.js";
|
|
14
|
+
/** What `unlockSharedMailbox()` found. */
|
|
15
|
+
export type SharedUnlockOutcome =
|
|
16
|
+
/** Its keys are open in this session. */
|
|
17
|
+
"unlocked"
|
|
18
|
+
/** The caller is a member but holds no wrap yet: another member's client shares the key with them shortly. */
|
|
19
|
+
| "pending"
|
|
20
|
+
/** The caller is a member, but none of their own mailboxes is unlocked to open their wraps with. */
|
|
21
|
+
| "own_keys_locked"
|
|
22
|
+
/** Not a shared mailbox of the caller's (they own it, or have no member access): its own unlock applies. */
|
|
23
|
+
| "not_shared";
|
|
24
|
+
/**
|
|
25
|
+
* Opens shared mailbox `mailboxUid` for its member, with no password: their member wraps, opened with the zero-access keys of a mailbox they
|
|
26
|
+
* own and have unlocked, become this session's keys for it (`installSharedSession()`). See `SharedUnlockOutcome` for when it can't.
|
|
27
|
+
*
|
|
28
|
+
* `refresh`: a shared session already open is opened again when it lacks the mailbox's active key and the member now holds a wrap of it -
|
|
29
|
+
* after a rotation, new mail is sealed to a key the session doesn't have. Its `masterKey` secret is kept, so the local index stays readable.
|
|
30
|
+
*/
|
|
31
|
+
export declare function unlockSharedMailbox(mailboxUid: string, client?: ApiClient, refresh?: boolean): Promise<SharedUnlockOutcome>;
|
|
32
|
+
/** What `maintainSharedMailbox()` did. */
|
|
33
|
+
export interface SharedMaintenance {
|
|
34
|
+
provisioned: boolean;
|
|
35
|
+
rotated: boolean;
|
|
36
|
+
/** Wraps added for waiting members. */
|
|
37
|
+
wrapped: number;
|
|
38
|
+
/** Wraps of older keys the server refused for waiting members (tried again on the next pass). */
|
|
39
|
+
refused: number;
|
|
40
|
+
/** Departed users whose wraps were removed. */
|
|
41
|
+
removed: number;
|
|
42
|
+
/** Keys wrapped for the mailbox's escrow scope. */
|
|
43
|
+
escrowed: number;
|
|
44
|
+
/**
|
|
45
|
+
* The server's reason when it refused (400) the new key of a pending rotation - e.g. the vault couldn't hold the new key's member wraps
|
|
46
|
+
* next to those it holds - so the current key stays active and members keep reading; the rotation is tried again on the next pass.
|
|
47
|
+
*/
|
|
48
|
+
rotationRefused?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The background care of a shared mailbox, run by the client of its owner or of a member with full access (`status.manager`); nothing for
|
|
52
|
+
* anyone else, or while the keys it needs are locked:
|
|
53
|
+
*
|
|
54
|
+
* A mailbox without an owner and without a key gets one (unless the administrator disallows zero-access), wrapped for every member who has a
|
|
55
|
+
* certificate. Once a user who held the active key has left (`rotationPending`), a new key replaces it - enrolled by the owner with their
|
|
56
|
+
* master key (`provisionZeroAccessKey()`), or for a mailbox without an owner with the members' wraps - and then the departed users' wraps
|
|
57
|
+
* are removed. Members still waiting (`pending`) get wraps of every key this client can open, one write per key with the active key's
|
|
58
|
+
* first (a refused older key doesn't stop the rest), and the escrow scope of a mailbox without an owner gets copies of the keys it is
|
|
59
|
+
* missing (`escrowPending`). A rotation the server refuses (400) is reported in `rotationRefused`, not thrown: the current key stays active
|
|
60
|
+
* and the rest of the pass goes ahead.
|
|
61
|
+
*/
|
|
62
|
+
export declare function maintainSharedMailbox(mailboxUid: string, mailboxAddress: string, client?: ApiClient): Promise<SharedMaintenance>;
|