@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.
Files changed (268) hide show
  1. package/apps/escrow/matters/[uid].tsx +4 -3
  2. package/apps/shared/calendar/calendarReminders.ts +35 -0
  3. package/apps/shared/calendar/sealedEvents.ts +113 -0
  4. package/apps/shared/calendar/useCalendarReminders.ts +28 -9
  5. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -0
  6. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +89 -3
  7. package/apps/shared/components/calendar/EventDetails.tsx +13 -2
  8. package/apps/shared/components/calendar/EventEditor.tsx +124 -17
  9. package/apps/shared/components/calendar/EventFormParts.tsx +1 -0
  10. package/apps/shared/components/calendar/EventModal.tsx +19 -2
  11. package/apps/shared/components/calendar/RequestChangeForm.tsx +4 -1
  12. package/apps/shared/components/calendar/eventForm.ts +2 -0
  13. package/apps/shared/components/contacts/ContactCard.tsx +13 -1
  14. package/apps/shared/components/contacts/ContactDetailPane.tsx +189 -9
  15. package/apps/shared/components/contacts/ContactForm.tsx +56 -7
  16. package/apps/shared/components/contacts/KeyChangeReview.tsx +5 -1
  17. package/apps/shared/components/contacts/contactCardData.ts +8 -4
  18. package/apps/shared/components/contacts/contactKeys.ts +3 -3
  19. package/apps/shared/components/layout/AppShell.tsx +34 -5
  20. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +30 -1
  21. package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +122 -0
  22. package/apps/shared/components/layout/UnlockPromptProvider.tsx +34 -0
  23. package/apps/shared/components/mail/ConversationList.tsx +1 -1
  24. package/apps/shared/components/mail/InviteCard.tsx +11 -1
  25. package/apps/shared/components/mail/MessageDetailPane.tsx +59 -26
  26. package/apps/shared/components/mail/compose/ComposeWindow.tsx +9 -2
  27. package/apps/shared/components/mail/compose/RecipientInput.tsx +2 -1
  28. package/apps/shared/components/mail/compose/draftResume.ts +28 -3
  29. package/apps/shared/components/mail/compose/protectedContactSuggestions.ts +121 -0
  30. package/apps/shared/components/mail/compose/quotedBody.ts +4 -3
  31. package/apps/shared/components/mail/invite/inviteStore.ts +17 -1
  32. package/apps/shared/components/mail/layout/MailShell.tsx +2 -0
  33. package/apps/shared/components/mail/pinnedSigners.ts +14 -0
  34. package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
  35. package/apps/shared/components/mail/reading/useMessageActions.ts +4 -1
  36. package/apps/shared/components/settings/KeyExportSetting.tsx +173 -0
  37. package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
  38. package/apps/shared/components/settings/PolicyLockedNotice.tsx +23 -0
  39. package/apps/shared/components/settings/ZeroAccessSetting.tsx +128 -0
  40. package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
  41. package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
  42. package/apps/shared/mail/decryptedMessages.ts +11 -3
  43. package/apps/shared/mail/outbox/sendJob.ts +349 -50
  44. package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
  45. package/apps/shared/mail/reportNotices.ts +3 -1
  46. package/apps/shared/mail/zeroAccessSource.ts +27 -0
  47. package/apps/shared/search/LocalIndexLifecycle.tsx +84 -24
  48. package/apps/shared/search/localIndexBuilder.ts +57 -20
  49. package/apps/shared/search/localIndexRpcClient.ts +96 -12
  50. package/apps/shared/search/localIndexSchema.ts +20 -4
  51. package/apps/shared/search/localIndexTransport.ts +6 -2
  52. package/apps/shared/search/localIndexWorker.ts +37 -18
  53. package/apps/shared/search/searchTier2.ts +5 -0
  54. package/apps/shared/search/zeroAccessIndexSync.ts +679 -0
  55. package/apps/shared/search/zeroAccessSyncState.ts +70 -0
  56. package/apps/shared/zeroAccess/ProtectedMark.tsx +19 -0
  57. package/apps/shared/zeroAccess/useKeysLocked.ts +25 -0
  58. package/apps/shared/zeroAccess/useOpenedRecord.ts +71 -0
  59. package/apps/www/_shell.tsx +2 -0
  60. package/apps/www/calendar/index.tsx +225 -61
  61. package/apps/www/contacts/index.tsx +67 -11
  62. package/apps/www/index.tsx +19 -3
  63. package/apps/www/settings/encryption/index.tsx +21 -1
  64. package/apps/www/settings/privacy/index.tsx +89 -39
  65. package/apps/www/tasks/index.tsx +24 -6
  66. package/dist/apps/escrow/matters/[uid].js +2 -2
  67. package/dist/apps/shared/calendar/calendarReminders.d.ts +14 -0
  68. package/dist/apps/shared/calendar/calendarReminders.js +26 -0
  69. package/dist/apps/shared/calendar/sealedEvents.d.ts +14 -0
  70. package/dist/apps/shared/calendar/sealedEvents.js +85 -0
  71. package/dist/apps/shared/calendar/useCalendarReminders.d.ts +2 -1
  72. package/dist/apps/shared/calendar/useCalendarReminders.js +27 -9
  73. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -0
  74. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +25 -2
  75. package/dist/apps/shared/components/calendar/EventDetails.js +13 -2
  76. package/dist/apps/shared/components/calendar/EventEditor.js +115 -18
  77. package/dist/apps/shared/components/calendar/EventFormParts.js +1 -1
  78. package/dist/apps/shared/components/calendar/EventModal.d.ts +1 -1
  79. package/dist/apps/shared/components/calendar/EventModal.js +15 -2
  80. package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +3 -0
  81. package/dist/apps/shared/components/calendar/RequestChangeForm.js +1 -1
  82. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -0
  83. package/dist/apps/shared/components/contacts/ContactCard.js +12 -1
  84. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +2 -1
  85. package/dist/apps/shared/components/contacts/ContactDetailPane.js +103 -9
  86. package/dist/apps/shared/components/contacts/ContactForm.d.ts +4 -1
  87. package/dist/apps/shared/components/contacts/ContactForm.js +37 -7
  88. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +3 -1
  89. package/dist/apps/shared/components/contacts/KeyChangeReview.js +3 -2
  90. package/dist/apps/shared/components/contacts/contactCardData.js +8 -4
  91. package/dist/apps/shared/components/contacts/contactKeys.d.ts +1 -1
  92. package/dist/apps/shared/components/contacts/contactKeys.js +3 -3
  93. package/dist/apps/shared/components/layout/AppShell.d.ts +18 -1
  94. package/dist/apps/shared/components/layout/AppShell.js +17 -6
  95. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +27 -2
  96. package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +17 -0
  97. package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +98 -0
  98. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +3 -0
  99. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +32 -0
  100. package/dist/apps/shared/components/mail/ConversationList.js +1 -1
  101. package/dist/apps/shared/components/mail/InviteCard.js +11 -1
  102. package/dist/apps/shared/components/mail/MessageDetailPane.js +46 -21
  103. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +2 -2
  104. package/dist/apps/shared/components/mail/compose/RecipientInput.js +3 -2
  105. package/dist/apps/shared/components/mail/compose/draftResume.d.ts +2 -0
  106. package/dist/apps/shared/components/mail/compose/draftResume.js +28 -3
  107. package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.d.ts +14 -0
  108. package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.js +96 -0
  109. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +1 -1
  110. package/dist/apps/shared/components/mail/compose/quotedBody.js +4 -3
  111. package/dist/apps/shared/components/mail/invite/inviteStore.js +16 -1
  112. package/dist/apps/shared/components/mail/layout/MailShell.js +2 -1
  113. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +6 -0
  114. package/dist/apps/shared/components/mail/pinnedSigners.js +10 -1
  115. package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +2 -2
  116. package/dist/apps/shared/components/mail/reading/conversationPrint.js +3 -3
  117. package/dist/apps/shared/components/mail/reading/useMessageActions.js +4 -1
  118. package/dist/apps/shared/components/settings/KeyExportSetting.d.ts +13 -0
  119. package/dist/apps/shared/components/settings/KeyExportSetting.js +90 -0
  120. package/dist/apps/shared/components/settings/OpenPgpKeySetting.d.ts +13 -0
  121. package/dist/apps/shared/components/settings/OpenPgpKeySetting.js +143 -0
  122. package/dist/apps/shared/components/settings/PolicyLockedNotice.d.ts +11 -0
  123. package/dist/apps/shared/components/settings/PolicyLockedNotice.js +9 -0
  124. package/dist/apps/shared/components/settings/ZeroAccessSetting.d.ts +16 -0
  125. package/dist/apps/shared/components/settings/ZeroAccessSetting.js +80 -0
  126. package/dist/apps/shared/components/settings/ZeroAccessTypes.d.ts +29 -0
  127. package/dist/apps/shared/components/settings/ZeroAccessTypes.js +107 -0
  128. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +3 -0
  129. package/dist/apps/shared/components/settings/layout/SettingsShell.js +3 -2
  130. package/dist/apps/shared/mail/decryptedMessages.d.ts +1 -1
  131. package/dist/apps/shared/mail/decryptedMessages.js +11 -3
  132. package/dist/apps/shared/mail/outbox/sendJob.d.ts +37 -4
  133. package/dist/apps/shared/mail/outbox/sendJob.js +287 -42
  134. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +12 -0
  135. package/dist/apps/shared/mail/outbox/sendOutcomes.js +76 -20
  136. package/dist/apps/shared/mail/reportNotices.js +3 -1
  137. package/dist/apps/shared/mail/zeroAccessSource.d.ts +8 -0
  138. package/dist/apps/shared/mail/zeroAccessSource.js +26 -0
  139. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +2 -0
  140. package/dist/apps/shared/search/LocalIndexLifecycle.js +74 -24
  141. package/dist/apps/shared/search/localIndexBuilder.d.ts +9 -1
  142. package/dist/apps/shared/search/localIndexBuilder.js +50 -17
  143. package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
  144. package/dist/apps/shared/search/localIndexRpcClient.js +129 -11
  145. package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
  146. package/dist/apps/shared/search/localIndexSchema.js +18 -4
  147. package/dist/apps/shared/search/localIndexTransport.d.ts +6 -2
  148. package/dist/apps/shared/search/localIndexWorker.d.ts +11 -0
  149. package/dist/apps/shared/search/localIndexWorker.js +22 -15
  150. package/dist/apps/shared/search/searchTier2.js +5 -0
  151. package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +102 -0
  152. package/dist/apps/shared/search/zeroAccessIndexSync.js +544 -0
  153. package/dist/apps/shared/search/zeroAccessSyncState.d.ts +20 -0
  154. package/dist/apps/shared/search/zeroAccessSyncState.js +65 -0
  155. package/dist/apps/shared/zeroAccess/ProtectedMark.d.ts +7 -0
  156. package/dist/apps/shared/zeroAccess/ProtectedMark.js +10 -0
  157. package/dist/apps/shared/zeroAccess/useKeysLocked.d.ts +6 -0
  158. package/dist/apps/shared/zeroAccess/useKeysLocked.js +20 -0
  159. package/dist/apps/shared/zeroAccess/useOpenedRecord.d.ts +18 -0
  160. package/dist/apps/shared/zeroAccess/useOpenedRecord.js +66 -0
  161. package/dist/apps/www/_shell.d.ts +1 -1
  162. package/dist/apps/www/_shell.js +2 -2
  163. package/dist/apps/www/calendar/index.js +88 -20
  164. package/dist/apps/www/contacts/index.js +64 -11
  165. package/dist/apps/www/index.js +19 -3
  166. package/dist/apps/www/settings/encryption/index.js +16 -4
  167. package/dist/apps/www/settings/privacy/index.js +47 -6
  168. package/dist/apps/www/tasks/index.js +21 -7
  169. package/dist/lib/admin/domainsApi.d.ts +1 -1
  170. package/dist/lib/admin/escrowAccessRequestsApi.d.ts +6 -0
  171. package/dist/lib/calendar/calendarApi.d.ts +52 -6
  172. package/dist/lib/calendar/calendarApi.js +110 -17
  173. package/dist/lib/calendar/calendarMutations.d.ts +20 -7
  174. package/dist/lib/calendar/calendarMutations.js +73 -14
  175. package/dist/lib/calendar/inviteApi.d.ts +17 -3
  176. package/dist/lib/calendar/inviteApi.js +86 -7
  177. package/dist/lib/calendar/protectedEvents.d.ts +31 -0
  178. package/dist/lib/calendar/protectedEvents.js +113 -0
  179. package/dist/lib/calendar/recurrence.d.ts +2 -0
  180. package/dist/lib/calendar/recurrence.js +16 -0
  181. package/dist/lib/calendar/sealedInvites.d.ts +9 -0
  182. package/dist/lib/calendar/sealedInvites.js +65 -0
  183. package/dist/lib/contacts/contactsApi.d.ts +29 -3
  184. package/dist/lib/contacts/contactsApi.js +23 -7
  185. package/dist/lib/contacts/useContactPhotoSrc.d.ts +5 -2
  186. package/dist/lib/contacts/useContactPhotoSrc.js +91 -11
  187. package/dist/lib/crypto/composeSecurity.d.ts +11 -3
  188. package/dist/lib/crypto/composeSecurity.js +13 -5
  189. package/dist/lib/crypto/keyExport.d.ts +54 -0
  190. package/dist/lib/crypto/keyExport.js +161 -0
  191. package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
  192. package/dist/lib/crypto/keyPairMatch.js +65 -0
  193. package/dist/lib/crypto/keySession.d.ts +64 -3
  194. package/dist/lib/crypto/keySession.js +179 -12
  195. package/dist/lib/crypto/keyvaultApi.d.ts +129 -8
  196. package/dist/lib/crypto/keyvaultApi.js +45 -3
  197. package/dist/lib/crypto/meetingKeys.d.ts +25 -0
  198. package/dist/lib/crypto/meetingKeys.js +87 -0
  199. package/dist/lib/crypto/messageSecurity.d.ts +56 -3
  200. package/dist/lib/crypto/messageSecurity.js +201 -13
  201. package/dist/lib/crypto/mime.d.ts +5 -0
  202. package/dist/lib/crypto/mime.js +25 -0
  203. package/dist/lib/crypto/openPgpKey.d.ts +39 -0
  204. package/dist/lib/crypto/openPgpKey.js +89 -0
  205. package/dist/lib/crypto/openpgp.d.ts +47 -0
  206. package/dist/lib/crypto/openpgp.js +88 -0
  207. package/dist/lib/crypto/pgpMessage.d.ts +51 -0
  208. package/dist/lib/crypto/pgpMessage.js +271 -0
  209. package/dist/lib/crypto/sharedMailboxKeys.d.ts +62 -0
  210. package/dist/lib/crypto/sharedMailboxKeys.js +337 -0
  211. package/dist/lib/crypto/smimeMessage.d.ts +21 -0
  212. package/dist/lib/crypto/smimeMessage.js +11 -7
  213. package/dist/lib/crypto/zeroAccessKey.d.ts +32 -0
  214. package/dist/lib/crypto/zeroAccessKey.js +71 -0
  215. package/dist/lib/mail/conversationsApi.d.ts +6 -0
  216. package/dist/lib/mail/mailApi.d.ts +63 -2
  217. package/dist/lib/mail/mailApi.js +5 -1
  218. package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
  219. package/dist/lib/mail/mailboxImportApi.js +15 -0
  220. package/dist/lib/search/zeroAccessIndexApi.d.ts +85 -0
  221. package/dist/lib/search/zeroAccessIndexApi.js +35 -0
  222. package/dist/lib/tasks/tasksApi.d.ts +11 -1
  223. package/dist/lib/tasks/tasksApi.js +10 -4
  224. package/dist/lib/util/clientPlatform.d.ts +14 -0
  225. package/dist/lib/util/clientPlatform.js +26 -0
  226. package/dist/lib/videoconf/protectedMeetings.d.ts +74 -0
  227. package/dist/lib/videoconf/protectedMeetings.js +181 -0
  228. package/dist/lib/videoconf/videoMeetingsApi.d.ts +25 -4
  229. package/dist/lib/videoconf/videoMeetingsApi.js +1 -1
  230. package/dist/lib/zeroAccess/sealedMoves.d.ts +41 -0
  231. package/dist/lib/zeroAccess/sealedMoves.js +63 -0
  232. package/dist/lib/zeroAccess/sealedRecords.d.ts +29 -0
  233. package/dist/lib/zeroAccess/sealedRecords.js +136 -0
  234. package/lib/admin/domainsApi.ts +1 -1
  235. package/lib/admin/escrowAccessRequestsApi.ts +6 -0
  236. package/lib/calendar/calendarApi.ts +143 -17
  237. package/lib/calendar/calendarMutations.ts +79 -12
  238. package/lib/calendar/inviteApi.ts +101 -8
  239. package/lib/calendar/protectedEvents.ts +143 -0
  240. package/lib/calendar/recurrence.ts +17 -0
  241. package/lib/calendar/sealedInvites.ts +73 -0
  242. package/lib/contacts/contactsApi.ts +43 -17
  243. package/lib/contacts/useContactPhotoSrc.ts +103 -13
  244. package/lib/crypto/composeSecurity.ts +20 -5
  245. package/lib/crypto/keyExport.ts +229 -0
  246. package/lib/crypto/keyPairMatch.ts +69 -0
  247. package/lib/crypto/keySession.ts +216 -12
  248. package/lib/crypto/keyvaultApi.ts +174 -10
  249. package/lib/crypto/meetingKeys.ts +118 -0
  250. package/lib/crypto/messageSecurity.ts +284 -16
  251. package/lib/crypto/mime.ts +26 -0
  252. package/lib/crypto/openPgpKey.ts +118 -0
  253. package/lib/crypto/openpgp.ts +115 -0
  254. package/lib/crypto/pgpMessage.ts +336 -0
  255. package/lib/crypto/sharedMailboxKeys.ts +402 -0
  256. package/lib/crypto/smimeMessage.ts +11 -7
  257. package/lib/crypto/zeroAccessKey.ts +102 -0
  258. package/lib/mail/conversationsApi.ts +3 -0
  259. package/lib/mail/mailApi.ts +60 -3
  260. package/lib/mail/mailboxImportApi.ts +24 -0
  261. package/lib/search/zeroAccessIndexApi.ts +123 -0
  262. package/lib/tasks/tasksApi.ts +19 -5
  263. package/lib/util/clientPlatform.ts +50 -0
  264. package/lib/videoconf/protectedMeetings.ts +221 -0
  265. package/lib/videoconf/videoMeetingsApi.ts +15 -4
  266. package/lib/zeroAccess/sealedMoves.ts +79 -0
  267. package/lib/zeroAccess/sealedRecords.ts +153 -0
  268. package/package.json +14 -5
@@ -0,0 +1,54 @@
1
+ /**
2
+ * "Export keys for Outlook/phone" (restapi `specs/search.md` §3, *Clients that don't run RapidMX*): a passphrase-protected
3
+ * PKCS#12 file holding the private keys a mail client needs to read this mailbox's mail on its own - every zero-access key
4
+ * (mail at rest is S/MIME `enveloped-data` to these) and every end-to-end encryption key (current and retained), plus, only
5
+ * when asked, the active signing key. Built entirely here from a fresh master-password unlock: the session's own private
6
+ * keys are non-extractable, and the server never sees the file or the passphrase. restapi only records that an export
7
+ * happened (`key_vault.export`), with the fingerprints exported and nothing else.
8
+ */
9
+ import "reflect-metadata";
10
+ import type { ApiClient } from "../util/api.js";
11
+ import { type PublicKey } from "./keyvaultApi.js";
12
+ /** PBKDF2 iterations for the shrouded key bags and the PKCS#12 MAC. Windows, macOS and iOS all accept this. */
13
+ export declare const KEY_EXPORT_ITERATIONS = 100000;
14
+ /** The shortest export passphrase accepted. The file grants read access to all of this mailbox's mail. */
15
+ export declare const MIN_EXPORT_PASSPHRASE_LENGTH = 12;
16
+ /** Which key a bag holds, for its friendly name and the export's audit record. */
17
+ export type ExportedKeyKind = "zero-access" | "encrypt" | "sign";
18
+ /** What `exportKeysAsPkcs12()` takes. */
19
+ export interface KeyExportInput {
20
+ mailboxUid: string;
21
+ /** The mailbox's address, used in each key's friendly name. */
22
+ mailboxAddress: string;
23
+ /** The mailbox's published keys (`Mailbox.keys`): the encryption and signing certificates. */
24
+ mailboxKeys: PublicKey[];
25
+ /** The master password, for the fresh unlock every export needs. */
26
+ masterPassword: string;
27
+ /** Protects the PKCS#12 file. At least `MIN_EXPORT_PASSPHRASE_LENGTH` characters. */
28
+ exportPassphrase: string;
29
+ /** Whether to include the active signing key, for clients that will sign. Off unless the user opts in. */
30
+ includeSigningKey?: boolean;
31
+ }
32
+ /** One key in the export. */
33
+ export interface ExportedKey {
34
+ kind: ExportedKeyKind;
35
+ fingerprint: string;
36
+ }
37
+ /** What `exportKeysAsPkcs12()` resolves with. */
38
+ export interface KeyExport {
39
+ /** The DER-encoded PKCS#12 (`.p12`) file. */
40
+ pkcs12: Uint8Array;
41
+ /** The keys it holds, zero-access keys first. */
42
+ keys: ExportedKey[];
43
+ }
44
+ /** The export has nothing to hold - the vault has no zero-access or encryption private key this password opens. */
45
+ export declare class NothingToExportError extends Error {
46
+ constructor();
47
+ }
48
+ /**
49
+ * Builds the "Export keys for Outlook/phone" PKCS#12 for `input.mailboxUid` (see this module's comment) and records the
50
+ * export with restapi. Re-opens the vault with `masterPassword` - rejecting as `unlockWithPassword()` does on a wrong
51
+ * one - and never touches the in-memory session. Every opened private key and the master key are zeroed before this
52
+ * returns, whether or not it succeeds. Throws `NothingToExportError` when no key opens.
53
+ */
54
+ export declare function exportKeysAsPkcs12(input: KeyExportInput, client?: ApiClient): Promise<KeyExport>;
@@ -0,0 +1,161 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * "Export keys for Outlook/phone" (restapi `specs/search.md` §3, *Clients that don't run RapidMX*): a passphrase-protected
7
+ * PKCS#12 file holding the private keys a mail client needs to read this mailbox's mail on its own - every zero-access key
8
+ * (mail at rest is S/MIME `enveloped-data` to these) and every end-to-end encryption key (current and retained), plus, only
9
+ * when asked, the active signing key. Built entirely here from a fresh master-password unlock: the session's own private
10
+ * keys are non-extractable, and the server never sees the file or the passphrase. restapi only records that an export
11
+ * happened (`key_vault.export`), with the fingerprints exported and nothing else.
12
+ */
13
+ import "reflect-metadata";
14
+ import * as asn1js from "asn1js";
15
+ import * as pkijs from "pkijs";
16
+ import { fromBase64 } from "./encoding.js";
17
+ import { findActivePublicKey, isOpenPgpKey, recordKeyExport } from "./keyvaultApi.js";
18
+ import { ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE, MAX_RETAINED_ENCRYPTION_KEYS, openMasterKeyWithPassword, SIGNING_PRIVATE_KEY_AAD_PURPOSE, ZERO_ACCESS_PRIVATE_KEY_AAD_PURPOSE, } from "./keySession.js";
19
+ import { buildAad, openWithKey } from "./masterKey.js";
20
+ /** PBKDF2 iterations for the shrouded key bags and the PKCS#12 MAC. Windows, macOS and iOS all accept this. */
21
+ export const KEY_EXPORT_ITERATIONS = 100000;
22
+ /** The shortest export passphrase accepted. The file grants read access to all of this mailbox's mail. */
23
+ export const MIN_EXPORT_PASSPHRASE_LENGTH = 12;
24
+ const PKCS8_SHROUDED_KEY_BAG = "1.2.840.113549.1.12.10.1.2";
25
+ const CERT_BAG = "1.2.840.113549.1.12.10.1.3";
26
+ const X509_CERTIFICATE = "1.2.840.113549.1.9.22.1";
27
+ const FRIENDLY_NAME = "1.2.840.113549.1.9.20";
28
+ const LOCAL_KEY_ID = "1.2.840.113549.1.9.21";
29
+ /** The shrouded key bags' cipher. pkijs's type wants an `iv`, but it generates a fresh one for every bag and ignores this one. */
30
+ const AES_256_CBC = { name: "AES-CBC", length: 256, iv: new Uint8Array(16) };
31
+ /** The export has nothing to hold - the vault has no zero-access or encryption private key this password opens. */
32
+ export class NothingToExportError extends Error {
33
+ constructor() {
34
+ super("This mailbox has no keys to export yet.");
35
+ this.name = "NothingToExportError";
36
+ }
37
+ }
38
+ function toArrayBuffer(bytes) {
39
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
40
+ }
41
+ function hexBytes(hex) {
42
+ const out = new Uint8Array(hex.length >> 1);
43
+ for (let i = 0; i < out.length; i++) {
44
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
45
+ }
46
+ return out;
47
+ }
48
+ function bagAttributes(friendlyName, localKeyId) {
49
+ return [
50
+ new pkijs.Attribute({ type: FRIENDLY_NAME, values: [new asn1js.BmpString({ value: friendlyName })] }),
51
+ new pkijs.Attribute({ type: LOCAL_KEY_ID, values: [new asn1js.OctetString({ valueHex: toArrayBuffer(localKeyId) })] }),
52
+ ];
53
+ }
54
+ function friendlyName(kind, address, index) {
55
+ const label = kind === "zero-access" ? "zero-access key" : kind === "encrypt" ? "encryption key" : "signing key";
56
+ return `RapidMX ${label} (${address})${index > 0 ? ` ${index + 1}` : ""}`;
57
+ }
58
+ /** Opens one wrapped private key to its raw PKCS#8, or `undefined` when it won't open under this master key - an export
59
+ * carries what it can rather than failing on one stale wrap. */
60
+ async function openPkcs8(masterKey, wrapped, aad) {
61
+ try {
62
+ return await openWithKey(masterKey, wrapped, aad);
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ }
68
+ /** Opens every key the export holds, in order: zero-access keys (unrevoked newest first), the encryption keys (active
69
+ * first, then retained newest first), then the active signing key if asked. Each list is capped like the session's. */
70
+ async function openExportedKeys(input, vault, masterKey) {
71
+ const opened = [];
72
+ const wrappedFor = (fingerprint) => vault.wrappedKeys.find((k) => k.fingerprint === fingerprint);
73
+ const take = async (kind, keys, purpose, max) => {
74
+ const aad = buildAad(input.mailboxUid, purpose);
75
+ const seen = new Set();
76
+ for (const key of keys) {
77
+ const wrapped = wrappedFor(key.fingerprint);
78
+ if (!wrapped || seen.has(key.fingerprint) || seen.size === max) {
79
+ continue;
80
+ }
81
+ const pkcs8 = await openPkcs8(masterKey, wrapped, aad);
82
+ if (pkcs8) {
83
+ seen.add(key.fingerprint);
84
+ opened.push({ kind, fingerprint: key.fingerprint, certDer: fromBase64(key.publicKey), pkcs8 });
85
+ }
86
+ }
87
+ };
88
+ const zeroAccess = [...(vault.zeroAccessKeys ?? [])].sort((a, b) => Number(!!a.revokedAt) - Number(!!b.revokedAt) || b.notBefore - a.notBefore);
89
+ await take("zero-access", zeroAccess, ZERO_ACCESS_PRIVATE_KEY_AAD_PURPOSE, MAX_RETAINED_ENCRYPTION_KEYS + 1);
90
+ const active = findActivePublicKey(input.mailboxKeys, "encrypt");
91
+ const encryption = [
92
+ ...(active ? [active] : []),
93
+ ...input.mailboxKeys.filter((k) => k.useType === "encrypt" && !isOpenPgpKey(k) && k !== active).sort((a, b) => b.notBefore - a.notBefore),
94
+ ];
95
+ await take("encrypt", encryption, ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE, MAX_RETAINED_ENCRYPTION_KEYS + 1);
96
+ if (input.includeSigningKey) {
97
+ const signing = findActivePublicKey(input.mailboxKeys, "sign");
98
+ await take("sign", signing ? [signing] : [], SIGNING_PRIVATE_KEY_AAD_PURPOSE, 1);
99
+ }
100
+ return opened;
101
+ }
102
+ /** Builds the PKCS#12 file: one PBES2 (PBKDF2-SHA256, AES-256-CBC) shrouded key bag and one certificate bag per key,
103
+ * paired by `localKeyID`, in an unencrypted SafeContents, under a SHA-256 MAC. */
104
+ async function buildPkcs12(keys, address, passphrase) {
105
+ const crypto = pkijs.getCrypto(true);
106
+ // UTF-8 for both: pkijs re-encodes it as the BMPString the PKCS#12 MAC derivation (RFC 7292 Appendix B) takes.
107
+ const utf8 = toArrayBuffer(new TextEncoder().encode(passphrase));
108
+ const bags = [];
109
+ const shrouded = [];
110
+ const counts = { "zero-access": 0, encrypt: 0, sign: 0 };
111
+ for (const key of keys) {
112
+ const name = friendlyName(key.kind, address, counts[key.kind]++);
113
+ const localKeyId = hexBytes(key.fingerprint).slice(0, 20);
114
+ const keyBag = new pkijs.PKCS8ShroudedKeyBag({ parsedValue: pkijs.PrivateKeyInfo.fromBER(toArrayBuffer(key.pkcs8)) });
115
+ shrouded.push(keyBag);
116
+ bags.push(new pkijs.SafeBag({ bagId: PKCS8_SHROUDED_KEY_BAG, bagValue: keyBag, bagAttributes: bagAttributes(name, localKeyId) }));
117
+ const certBag = new pkijs.CertBag({ certId: X509_CERTIFICATE, parsedValue: pkijs.Certificate.fromBER(toArrayBuffer(key.certDer)) });
118
+ bags.push(new pkijs.SafeBag({ bagId: CERT_BAG, bagValue: certBag, bagAttributes: bagAttributes(name, localKeyId) }));
119
+ }
120
+ for (const keyBag of shrouded) {
121
+ await keyBag.makeInternalValues({ password: utf8, contentEncryptionAlgorithm: AES_256_CBC, hmacHashAlgorithm: "SHA-256", iterationCount: KEY_EXPORT_ITERATIONS }, crypto);
122
+ }
123
+ const pfx = new pkijs.PFX({
124
+ parsedValue: {
125
+ integrityMode: 0,
126
+ authenticatedSafe: new pkijs.AuthenticatedSafe({ parsedValue: { safeContents: [{ privacyMode: 0, value: new pkijs.SafeContents({ safeBags: bags }) }] } }),
127
+ },
128
+ });
129
+ await pfx.parsedValue.authenticatedSafe.makeInternalValues({ safeContents: [{}] }, crypto);
130
+ await pfx.makeInternalValues({ password: utf8, iterations: KEY_EXPORT_ITERATIONS, pbkdf2HashAlgorithm: { name: "SHA-256" }, hmacHashAlgorithm: "SHA-256" }, crypto);
131
+ return new Uint8Array(pfx.toSchema().toBER(false));
132
+ }
133
+ /**
134
+ * Builds the "Export keys for Outlook/phone" PKCS#12 for `input.mailboxUid` (see this module's comment) and records the
135
+ * export with restapi. Re-opens the vault with `masterPassword` - rejecting as `unlockWithPassword()` does on a wrong
136
+ * one - and never touches the in-memory session. Every opened private key and the master key are zeroed before this
137
+ * returns, whether or not it succeeds. Throws `NothingToExportError` when no key opens.
138
+ */
139
+ export async function exportKeysAsPkcs12(input, client) {
140
+ if (input.exportPassphrase.length < MIN_EXPORT_PASSPHRASE_LENGTH) {
141
+ throw new Error(`The export passphrase must be at least ${MIN_EXPORT_PASSPHRASE_LENGTH} characters.`);
142
+ }
143
+ const { vault, masterKey } = await openMasterKeyWithPassword(input.mailboxUid, input.masterPassword, client);
144
+ let opened = [];
145
+ try {
146
+ opened = await openExportedKeys(input, vault, masterKey);
147
+ if (!opened.some((k) => k.kind !== "sign")) {
148
+ throw new NothingToExportError();
149
+ }
150
+ const pkcs12 = await buildPkcs12(opened, input.mailboxAddress, input.exportPassphrase);
151
+ const keys = opened.map(({ kind, fingerprint }) => ({ kind, fingerprint }));
152
+ await recordKeyExport(input.mailboxUid, keys, client);
153
+ return { pkcs12, keys };
154
+ }
155
+ finally {
156
+ masterKey.fill(0);
157
+ for (const key of opened) {
158
+ key.pkcs8.fill(0);
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Whether a certificate the server lists is really the one of a private key this device opened. A zero-access key's private half comes
3
+ * from an authenticated seal (or a member's wrap), but its certificate comes from the server as it is: what this device then encrypts
4
+ * to that certificate - the search index segments it builds from end-to-end mail, a protected meeting's key - must not go to a key the
5
+ * server swapped in. Plain WebCrypto and a few lines of DER, so the unlock path loads no certificate library.
6
+ */
7
+ /** Whether `certDer` (a P-256 certificate) carries the public half of `pkcs8` (a P-256 private key). Never rejects: anything that won't
8
+ * parse or import is `false`. */
9
+ export declare function certificateHoldsKey(certDer: Uint8Array, pkcs8: Uint8Array): Promise<boolean>;
@@ -0,0 +1,65 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * Whether a certificate the server lists is really the one of a private key this device opened. A zero-access key's private half comes
7
+ * from an authenticated seal (or a member's wrap), but its certificate comes from the server as it is: what this device then encrypts
8
+ * to that certificate - the search index segments it builds from end-to-end mail, a protected meeting's key - must not go to a key the
9
+ * server swapped in. Plain WebCrypto and a few lines of DER, so the unlock path loads no certificate library.
10
+ */
11
+ /** One DER element of `der` at `offset`: where its header starts, where its content starts, where it ends. */
12
+ function element(der, offset) {
13
+ const tag = der[offset];
14
+ let length = der[offset + 1];
15
+ let content = offset + 2;
16
+ if (length & 0x80) {
17
+ const bytes = length & 0x7f;
18
+ if (bytes === 0 || bytes > 4) {
19
+ throw new Error("Unsupported DER length.");
20
+ }
21
+ length = 0;
22
+ for (let i = 0; i < bytes; i++) {
23
+ length = length * 256 + der[content + i];
24
+ }
25
+ content += bytes;
26
+ }
27
+ const end = content + length;
28
+ if (tag === undefined || end > der.length) {
29
+ throw new Error("Truncated DER.");
30
+ }
31
+ return { tag, header: offset, content, end };
32
+ }
33
+ /** The children of the constructed element `parent`. */
34
+ function children(der, parent) {
35
+ const found = [];
36
+ for (let offset = parent.content; offset < parent.end; offset = found[found.length - 1].end) {
37
+ found.push(element(der, offset));
38
+ }
39
+ return found;
40
+ }
41
+ /** The SubjectPublicKeyInfo of X.509 certificate `certDer` (RFC 5280: the seventh field of `tbsCertificate`, the sixth when it has no
42
+ * explicit version). */
43
+ function subjectPublicKeyInfo(certDer) {
44
+ const [tbs] = children(certDer, element(certDer, 0));
45
+ const fields = children(certDer, tbs);
46
+ const spki = fields[fields[0].tag === 0xa0 ? 6 : 5];
47
+ if (!spki || spki.tag !== 0x30) {
48
+ throw new Error("No subject public key.");
49
+ }
50
+ return certDer.subarray(spki.header, spki.end);
51
+ }
52
+ const P256_ECDH = { name: "ECDH", namedCurve: "P-256" };
53
+ /** Whether `certDer` (a P-256 certificate) carries the public half of `pkcs8` (a P-256 private key). Never rejects: anything that won't
54
+ * parse or import is `false`. */
55
+ export async function certificateHoldsKey(certDer, pkcs8) {
56
+ try {
57
+ const publicKey = await crypto.subtle.importKey("spki", subjectPublicKeyInfo(certDer), P256_ECDH, true, []);
58
+ const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8, P256_ECDH, true, ["deriveBits"]);
59
+ const [certified, held] = await Promise.all([crypto.subtle.exportKey("jwk", publicKey), crypto.subtle.exportKey("jwk", privateKey)]);
60
+ return !!certified.x && certified.x === held.x && certified.y === held.y;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
@@ -17,14 +17,18 @@
17
17
  * follow-up, not a silent gap: a mailbox enrolled *only* with a passkey has no way to unlock through this
18
18
  * module today.
19
19
  */
20
- import { type PublicKey } from "./keyvaultApi.js";
20
+ import { type KeyVault, type PublicKey } from "./keyvaultApi.js";
21
+ import type { ApiClient } from "../util/api.js";
21
22
  import { KeysLockedError } from "./masterKey.js";
23
+ import type { OpenPgpPrivateKey } from "./openpgp.js";
22
24
  /** AAD purpose labels — MUST exactly match what `KeyEnrollmentGate.tsx` used when it originally
23
25
  * sealed each of these values, or `openWithKey()` fails (GCM authenticates the AAD, not just the
24
26
  * ciphertext). Centralized here as the one place both wrapping and unwrapping refer to. */
25
27
  export declare const MASTER_KEY_AAD_PURPOSE = "master-key";
26
28
  export declare const SIGNING_PRIVATE_KEY_AAD_PURPOSE = "sign-private-key";
27
29
  export declare const ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE = "encrypt-private-key";
30
+ export declare const ZERO_ACCESS_PRIVATE_KEY_AAD_PURPOSE = "zero-access-private-key";
31
+ export declare const OPENPGP_PRIVATE_KEY_AAD_PURPOSE = "openpgp-private-key";
28
32
  export { KeysLockedError };
29
33
  /** How many retained (non-active) encryption keys an unlock opens at most: the most recently issued ones by
30
34
  * `notBefore`. Each costs an AEAD open and a key import at unlock, and `smime.ts`'s `decryptEnvelopedDataWithKeys()`
@@ -60,6 +64,24 @@ export interface UnlockedKeys {
60
64
  * that). A compromised key is kept because reading one's own mail encrypted to it is still needed. Absent when there
61
65
  * are none. */
62
66
  retainedEncryptionKeys?: RetainedEncryptionKey[];
67
+ /** The mailbox's zero-access private keys (restapi `specs/search.md` §3) - the keys the server encrypts mail at rest,
68
+ * index entries and sealed summaries to - the active one first, then retained ones newest first, at most
69
+ * `MAX_RETAINED_ENCRYPTION_KEYS + 1`. Decryption only. Absent when the mailbox has none (or none opened). */
70
+ zeroAccessKeys?: RetainedEncryptionKey[];
71
+ /** The mailbox's OpenPGP secret keys (restapi `specs/end-to-end_encryption.md` *OpenPGP Interoperability*) - the active one
72
+ * first, then retained ones newest first, at most `MAX_RETAINED_ENCRYPTION_KEYS + 1`: what opens mail OpenPGP users encrypt to
73
+ * the mailbox, and signs what it sends them. Absent when the mailbox has none (or none opened). */
74
+ openPgpKeys?: {
75
+ fingerprint: string;
76
+ privateKey: OpenPgpPrivateKey;
77
+ }[];
78
+ /** A shared mailbox opened through this user's member key wraps (`sharedMailboxKeys.ts`), not with its own master key: it holds
79
+ * only `zeroAccessKeys`, and `masterKey` is a per-key secret derived from its zero-access private key (what the local search
80
+ * index's key is derived from), never a key that opens the mailbox's vault. */
81
+ shared?: boolean;
82
+ /** A shared session only: the user's own mailboxes whose zero-access keys opened its member key wraps. Locking any of them locks it too -
83
+ * it was opened with their keys, so it doesn't outlive them. */
84
+ openedWith?: string[];
63
85
  }
64
86
  /** What changed in the session store — passed to every `subscribeKeySession()` listener. */
65
87
  export interface KeySessionEvent {
@@ -76,6 +98,36 @@ export type KeySessionListener = (event: KeySessionEvent) => void;
76
98
  * nothing. Returns an unsubscribe function.
77
99
  */
78
100
  export declare function subscribeKeySession(listener: KeySessionListener): () => void;
101
+ /**
102
+ * Makes a zero-access key this device has just enrolled (`zeroAccessKey.ts`'s `provisionZeroAccessKey()`) the active one
103
+ * of `unlocked` - the session object the key's private half was sealed under - so zero-access mail encrypted to it reads
104
+ * without another unlock. Does nothing when `unlocked` is no longer this mailbox's session (it was locked or replaced
105
+ * meanwhile; the next unlock opens the key from the vault anyway).
106
+ */
107
+ export declare function addZeroAccessKey(mailboxUid: string, unlocked: UnlockedKeys, key: RetainedEncryptionKey): void;
108
+ /**
109
+ * Makes an OpenPGP key this device has just enrolled (`openPgpKey.ts`'s `ensureOpenPgpKey()`) the active one of `unlocked`, so
110
+ * OpenPGP mail to it reads without another unlock - the same rules as `addZeroAccessKey()`.
111
+ */
112
+ export declare function addOpenPgpKey(mailboxUid: string, unlocked: UnlockedKeys, key: {
113
+ fingerprint: string;
114
+ privateKey: OpenPgpPrivateKey;
115
+ }): void;
116
+ /**
117
+ * Stores the keys of a shared mailbox opened through member key wraps (`sharedMailboxKeys.ts`'s `unlockSharedMailbox()`) as that mailbox's
118
+ * session, exactly as an unlock would - tracked for `destroyUnlockedKeys()`, and notified - unless the mailbox was locked since `generation`
119
+ * was taken (`sharedUnlockGeneration()`), when they are destroyed instead and `KeysLockedError` is thrown.
120
+ */
121
+ export declare function installSharedSession(mailboxUid: string, unlocked: UnlockedKeys, generation: string): void;
122
+ /** The lock generation to pass `installSharedSession()`, taken before a shared unlock starts its requests. */
123
+ export declare function sharedUnlockGeneration(mailboxUid: string): string;
124
+ /** Every mailbox unlocked this session with its own keys (not a shared one opened through member key wraps). */
125
+ export declare function personallyUnlockedKeys(): UnlockedKeys[];
126
+ /** `personallyUnlockedKeys()` with each mailbox's uid. */
127
+ export declare function personalSessions(): {
128
+ mailboxUid: string;
129
+ unlocked: UnlockedKeys;
130
+ }[];
79
131
  /** Reads back a mailbox's already-unlocked keys this session, or `undefined` if it hasn't been
80
132
  * unlocked yet (or was destroyed). Never triggers an unlock itself. */
81
133
  export declare function getUnlockedKeys(mailboxUid: string): UnlockedKeys | undefined;
@@ -128,11 +180,20 @@ export declare class UnopenableEncryptionKeyError extends Error {
128
180
  * "incorrect password," not a generic error, but this module doesn't presume a specific UI's error copy.
129
181
  * Once the master key has opened, the password is known to be right: an active signing key that then
130
182
  * won't open is skipped and listed in `UnlockResult.unopenableKeys` (so is a retained encryption key); an active
131
- * encryption key that won't
132
- * open still fails the unlock, with `UnopenableEncryptionKeyError`. Throws `KeysLockedError` when
183
+ * encryption key that won't open, or whose certificate isn't its own,
184
+ * still fails the unlock, with `UnopenableEncryptionKeyError`. Throws `KeysLockedError` when
133
185
  * `destroyUnlockedKeys()` locked this mailbox (or all mailboxes) while the unlock was in flight.
134
186
  */
135
187
  export declare function unlockWithPassword(mailboxUid: string, mailboxKeys: PublicKey[], password: string): Promise<UnlockResult>;
188
+ /**
189
+ * Fetches `mailboxUid`'s vault and opens its master key with the password wrap, touching no session - for an operation that
190
+ * must re-prove the password even while the mailbox is unlocked (`keyExport.ts`). Rejects exactly as `unlockWithPassword()`
191
+ * does on a missing wrap, an unknown KDF or a wrong password. The caller owns the returned master key and must zero it.
192
+ */
193
+ export declare function openMasterKeyWithPassword(mailboxUid: string, password: string, client?: ApiClient): Promise<{
194
+ vault: KeyVault;
195
+ masterKey: Uint8Array;
196
+ }>;
136
197
  /** What `unlockWithRecoveryCode()` resolves with. */
137
198
  export interface RecoveryUnlockResult extends UnlockResult {
138
199
  /** The `methodId` of the recovery wrap the code opened (e.g. `"recovery-3"`). Recovery codes are single-use: