1 import { CryptoProxy } from '@proton/crypto';
2 import { authStore } from '@proton/pass/lib/auth/store';
4 PassCryptoManagerContext,
6 SerializedCryptoContext,
12 } from '@proton/pass/types';
13 import { ShareType } from '@proton/pass/types';
14 import { unwrap } from '@proton/pass/utils/fp/promises';
15 import { logId, logger } from '@proton/pass/utils/logger';
16 import { entriesMap } from '@proton/pass/utils/object/map';
17 import type { DecryptedAddressKey } from '@proton/shared/lib/interfaces';
18 import { getDecryptedAddressKeysHelper, getDecryptedUserKeysHelper } from '@proton/shared/lib/keys';
20 import * as processes from './processes';
21 import { createShareManager } from './share-manager';
22 import { getSupportedAddresses } from './utils/addresses';
25 PassCryptoHydrationError,
26 PassCryptoNotHydratedError,
29 } from './utils/errors';
31 function assertHydrated(ctx: PassCryptoManagerContext): asserts ctx is Required<PassCryptoManagerContext> {
33 ctx.user === undefined ||
34 ctx.addresses === undefined ||
35 ctx.primaryAddress === undefined ||
36 ctx.userKeys === undefined ||
37 ctx.primaryUserKey === undefined
39 throw new PassCryptoNotHydratedError('Pass crypto manager incorrectly hydrated');
43 export const createPassCrypto = (): PassCryptoWorker => {
44 const context: PassCryptoManagerContext = {
48 primaryUserKey: undefined,
49 primaryAddress: undefined,
50 shareManagers: new Map(),
53 const hasShareManager = (shareId: string): boolean => context.shareManagers.has(shareId);
55 const getShareManager = (shareId: string): ShareManager => {
56 if (!hasShareManager(shareId)) throw new PassCryptoShareError(`Unknown shareId : cannot resolve share manager`);
57 return context.shareManagers.get(shareId)!;
60 const unregisterInactiveShares = () => {
61 context.shareManagers.forEach((shareManager, shareId) => {
62 if (!shareManager.isActive(context.userKeys)) {
63 logger.info(`[PassCrypto] Unregistering share ${logId(shareId)} (inactive)`);
64 context.shareManagers.delete(shareId);
69 const getDecryptedAddressKeys = async (addressId: string): Promise<DecryptedAddressKey[]> => {
70 assertHydrated(context);
72 const address = context.addresses.find((address) => address.ID === addressId);
73 if (address === undefined) throw new PassCryptoError(`Could not find address with ID ${logId(addressId)}`);
74 return getDecryptedAddressKeysHelper(address.Keys, context.user, context.userKeys, authStore.getPassword()!);
77 /** Resolves the decrypted address key reference */
78 const getPrimaryAddressKeyById = async (addressId: string): Promise<DecryptedAddressKey> => {
79 const [primaryAddressKey] = await getDecryptedAddressKeys(addressId);
80 return primaryAddressKey;
83 const worker: PassCryptoWorker = {
86 assertHydrated(context);
93 getContext: () => context,
95 async hydrate({ user, addresses, keyPassword, snapshot, clear }) {
96 logger.info('[PassCrypto] Hydrating crypto state');
98 if (clear) worker.clear();
101 const userKeys = await getDecryptedUserKeysHelper(user, keyPassword);
102 const activeAddresses = addresses.filter(getSupportedAddresses);
104 if (userKeys.length === 0) throw new PassCryptoHydrationError('No user keys found');
105 if (activeAddresses.length === 0) throw new PassCryptoHydrationError('No active user addresses found');
108 context.addresses = addresses;
109 context.primaryAddress = activeAddresses[0];
110 context.userKeys = userKeys;
111 context.primaryUserKey = userKeys[0];
114 const entries = snapshot.shareManagers as [string, SerializedCryptoContext<ShareContext>][];
115 const shareManagers = await unwrap(entriesMap(entries)(createShareManager.fromSnapshot));
116 context.shareManagers = new Map(shareManagers);
117 logger.info('[PassCrypto] Hydrated from local snapshot');
120 logger.warn('[PassCrypto] Hydration failed', err);
121 const message = err instanceof Error ? err.message : 'unknown error';
122 throw new PassCryptoHydrationError(`Hydration failure (${message})`);
125 unregisterInactiveShares();
129 logger.info('[PassCrypto] Clearing state');
130 context.user = undefined;
131 context.userKeys = [];
132 context.addresses = [];
133 context.primaryAddress = undefined;
134 context.primaryUserKey = undefined;
135 context.shareManagers = new Map();
140 getDecryptedAddressKeys,
142 /* Creating a vault does not register a share manager :
143 * call PassCrypto::openShare to register it */
144 async createVault(content) {
145 assertHydrated(context);
147 return processes.createVault({
149 userKey: context.primaryUserKey,
150 addressId: context.primaryAddress.ID,
154 /* Updating a vault does not register a share manager :
155 * call PassCrypto::openShare to register it.
157 * ⚠️ Key rotation : We're assuming the shareManager will
158 * always hold the latest rotation keys - In order to future
159 * -proof this, each vault update should be preceded by a
160 * call to retrieve the latest shareKeys */
161 async updateVault({ shareId, content }) {
162 assertHydrated(context);
164 const shareManager = getShareManager(shareId);
165 const latestRotation = shareManager.getLatestRotation();
166 const vaultKey = shareManager.getVaultKey(latestRotation);
168 return processes.updateVault({ vaultKey, content });
171 canOpenShare(shareId) {
173 return worker.getShareManager(shareId).isActive(context.userKeys);
179 /* Opening a new share has the side-effect of registering a
180 * shareManager for this share. When opening a pre-registered
181 * share (most likely hydrated from a snapshot) - filter the
182 * vault keys to only open those we haven't already processed.
183 * This can happen during a vault share content update or during
184 * a full data sync. */
185 openShare: async <T extends ShareType = ShareType>(data: {
186 encryptedShare: ShareGetResponse;
187 shareKeys: ShareKeyResponse[];
189 assertHydrated(context);
192 const { encryptedShare, shareKeys } = data;
194 if (shareKeys.length === 0) throw new PassCryptoShareError('Empty share keys');
196 /* before processing the current encryptedShare - ensure the
197 * latest rotation key can be decrypted with an active userKey */
198 const latestKey = shareKeys.reduce((acc, curr) => (curr.KeyRotation > acc.KeyRotation ? curr : acc));
199 const canOpenShare = context.userKeys.some(({ ID }) => ID === latestKey.UserKeyID);
201 if (!canOpenShare) return null;
203 const shareId = encryptedShare.ShareID;
204 const maybeShareManager = hasShareManager(shareId) ? getShareManager(shareId) : undefined;
206 const share = await (async () => {
207 switch (encryptedShare.TargetType) {
208 case ShareType.Vault: {
209 const vaultKeys = await Promise.all(
210 shareKeys.map((shareKey) =>
211 maybeShareManager?.hasVaultKey(shareKey.KeyRotation)
212 ? maybeShareManager.getVaultKey(shareKey.KeyRotation)
213 : processes.openVaultKey({ shareKey, userKeys: context.userKeys })
217 const rotation = encryptedShare.ContentKeyRotation!;
218 const vaultKey = vaultKeys.find((key) => key.rotation === rotation);
219 if (vaultKey === undefined) {
220 throw new PassCryptoShareError(`Missing vault key for rotation ${rotation}`);
222 return processes.openShare({ type: ShareType.Vault, encryptedShare, vaultKey });
226 return processes.openShare({ type: ShareType.Item, encryptedShare });
229 throw new PassCryptoShareError('Unsupported share type');
233 const shareManager = maybeShareManager ?? createShareManager(share);
235 context.shareManagers.set(shareId, shareManager);
236 shareManager.setShare(share); /* handle update when recyling */
237 await worker.updateShareKeys({ shareId, shareKeys });
239 return shareManager.getShare() as TypedOpenedShare<T>;
241 throw isPassCryptoError(err) ? err : new PassCryptoError(err);
245 /* FIXME: add support for itemKeys when we
246 * support ItemShares */
247 async updateShareKeys({ shareId, shareKeys }) {
248 assertHydrated(context);
250 const shareManager = getShareManager(shareId);
251 const newVaultKeys = await Promise.all(
253 .filter(({ KeyRotation }) => !shareManager.hasVaultKey(KeyRotation))
254 .map((shareKey) => processes.openVaultKey({ shareKey, userKeys: context.userKeys }))
257 newVaultKeys.forEach((vaultKey) => shareManager.addVaultKey(vaultKey));
260 removeShare: (shareId) => context.shareManagers.delete(shareId),
262 /* Resolve the latest rotation for this share
263 * and use the vault key for that rotation */
264 async createItem({ shareId, content }) {
265 assertHydrated(context);
267 const shareManager = getShareManager(shareId);
268 const latestRotation = shareManager.getLatestRotation();
269 const vaultKey = shareManager.getVaultKey(latestRotation);
271 return processes.createItem({ content, vaultKey });
274 async openItem({ shareId, encryptedItem }) {
275 assertHydrated(context);
277 const shareManager = getShareManager(shareId);
278 const vaultKey = shareManager.getVaultKey(encryptedItem.KeyRotation!);
280 return processes.openItem({ encryptedItem, vaultKey });
283 /* We're assuming that every call to PassCrypto::updateItem will
284 * be preceded by a request to resolve the latest encrypted item
285 * key for future-proofing */
286 async updateItem({ shareId, content, latestItemKey, lastRevision }) {
287 assertHydrated(context);
289 const shareManager = getShareManager(shareId);
290 const vaultKey = shareManager.getVaultKey(latestItemKey.KeyRotation);
291 const itemKey = await processes.openItemKey({ encryptedItemKey: latestItemKey, vaultKey });
293 return processes.updateItem({ itemKey, content, lastRevision });
296 async moveItem({ destinationShareId, content }) {
297 assertHydrated(context);
299 const shareManager = getShareManager(destinationShareId);
300 const latestRotation = shareManager.getLatestRotation();
301 const destinationVaultKey = shareManager.getVaultKey(latestRotation);
303 return processes.moveItem({ destinationShareId, destinationVaultKey, content });
306 async createVaultInvite({ shareId, invitedPublicKey, email, role }) {
307 assertHydrated(context);
309 const shareManager = getShareManager(shareId);
310 const share = shareManager.getShare();
311 const inviteKeys = await processes.createInviteKeys({
312 targetKeys: shareManager.getVaultKeys(),
313 invitedPublicKey: await CryptoProxy.importPublicKey({ armoredKey: invitedPublicKey }),
314 inviterPrivateKey: (await getPrimaryAddressKeyById(share.addressId)).privateKey,
317 return { Keys: inviteKeys, Email: email, ShareRoleID: role, TargetType: ShareType.Vault };
320 async createNewUserVaultInvite({ shareId, email, role }) {
321 assertHydrated(context);
323 const shareManager = getShareManager(shareId);
324 const share = shareManager.getShare();
326 const signature = await processes.createNewUserSignature({
327 inviterPrivateKey: (await getPrimaryAddressKeyById(share.addressId)).privateKey,
329 vaultKey: shareManager.getVaultKey(shareManager.getLatestRotation()),
335 Signature: signature,
336 TargetType: ShareType.Vault,
340 async promoteInvite({ shareId, invitedPublicKey }) {
341 assertHydrated(context);
343 const shareManager = getShareManager(shareId);
344 const share = shareManager.getShare();
345 const inviteKeys = await processes.createInviteKeys({
346 targetKeys: shareManager.getVaultKeys(),
347 invitedPublicKey: await CryptoProxy.importPublicKey({ armoredKey: invitedPublicKey }),
348 inviterPrivateKey: (await getPrimaryAddressKeyById(share.addressId)).privateKey,
351 return { Keys: inviteKeys };
354 async acceptVaultInvite({ inviteKeys, invitedAddressId, inviterPublicKeys }) {
355 assertHydrated(context);
357 const vaultKeys = await processes.reencryptInviteKeys({
358 userKey: context.primaryUserKey,
360 invitedPrivateKey: (await getPrimaryAddressKeyById(invitedAddressId)).privateKey,
361 inviterPublicKeys: await Promise.all(
362 inviterPublicKeys.map((armoredKey) => CryptoProxy.importPublicKey({ armoredKey }))
366 return { Keys: vaultKeys };
369 async readVaultInvite({ inviteKey, invitedAddressId, encryptedVaultContent, inviterPublicKeys }) {
370 assertHydrated(context);
372 return processes.readVaultInviteContent({
374 encryptedVaultContent,
375 invitedPrivateKey: (await getPrimaryAddressKeyById(invitedAddressId)).privateKey,
376 inviterPublicKeys: await Promise.all(
377 inviterPublicKeys.map((armoredKey) => CryptoProxy.importPublicKey({ armoredKey }))
382 async createSecureLink({ shareId, latestItemKey }) {
383 assertHydrated(context);
385 const vaultKey = getShareManager(shareId).getVaultKey(latestItemKey.KeyRotation);
386 const itemKey = await processes.openItemKey({ encryptedItemKey: latestItemKey, vaultKey });
387 return processes.createSecureLink({ itemKey, vaultKey });
390 async openSecureLink({ linkKey, publicLinkContent }) {
391 if (!publicLinkContent.ItemKey || !publicLinkContent.Contents) {
392 throw new Error('Missing data when retrieving secure link content');
395 return processes.openSecureLink({
396 encryptedItemKey: publicLinkContent.ItemKey,
397 content: publicLinkContent.Contents,
402 async openLinkKey({ encryptedLinkKey, linkKeyShareKeyRotation, shareId }) {
403 assertHydrated(context);
405 const vaultKey = getShareManager(shareId).getVaultKey(linkKeyShareKeyRotation);
406 return processes.openLinkKey({ encryptedLinkKey, shareKey: vaultKey.key });
410 shareManagers: [...context.shareManagers.entries()].map(([shareId, shareManager]) => [
412 shareManager.serialize(),