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