Merge branch 'IDTEAM-1.26.0' into 'main'
[ProtonMail-WebClient.git] / packages / pass / lib / crypto / share-manager.ts
blob3c283940ff6cbac33ac937b5918298c5e47a18aa
1 import type {
2     OpenedShare,
3     Rotation,
4     SerializedCryptoContext,
5     ShareContext,
6     ShareManager,
7     TypedOpenedShare,
8     VaultKey,
9 } from '@proton/pass/types';
10 import { ShareType } from '@proton/pass/types';
11 import { base64StringToUint8Array, uint8ArrayToBase64String } from '@proton/shared/lib/helpers/encoding';
13 import { importSymmetricKey } from './utils/crypto-helpers';
14 import { PassCryptoShareError, PassCryptoVaultError } from './utils/errors';
16 export const createShareManager = <T extends ShareType = ShareType>(
17     share: TypedOpenedShare<T>,
18     context?: ShareContext
19 ): ShareManager<T> => {
20     const shareContext: ShareContext = context ?? {
21         share,
22         latestRotation: -1,
23         vaultKeys: new Map(),
24         itemKeys: new Map(),
25     };
27     const shareManager: ShareManager<T> = {
28         getShare: () => shareContext.share as TypedOpenedShare<T>,
29         setShare: (share) => (shareContext.share = share),
31         setLatestRotation: (rotation) => (shareContext.latestRotation = rotation),
33         getLatestRotation() {
34             if (shareContext.latestRotation === -1) {
35                 throw new PassCryptoShareError(`Share has not been hydrated`);
36             }
38             return shareContext.latestRotation;
39         },
41         hasVaultKey: (rotation) =>
42             shareManager.getShare().targetType === ShareType.Vault && shareContext.vaultKeys.has(rotation),
44         getVaultKey(rotation) {
45             if (shareManager.getShare().targetType !== ShareType.Vault) {
46                 throw new PassCryptoVaultError(`Cannot resolve vault keys for non-vault share`);
47             }
49             if (!shareManager.hasVaultKey(rotation)) {
50                 throw new PassCryptoVaultError(`Cannot find vault key for rotation ${rotation}`);
51             }
53             return shareContext.vaultKeys.get(rotation)!;
54         },
56         getVaultKeys: () => Array.from(shareContext.vaultKeys.values()),
58         addVaultKey(vaultKey) {
59             if (shareManager.getShare().targetType !== ShareType.Vault) {
60                 throw new PassCryptoVaultError(`Cannot add vault key to non-vault share`);
61             }
63             const rotation = vaultKey.rotation;
64             shareContext.vaultKeys.set(rotation, vaultKey);
66             if (rotation > shareContext.latestRotation) {
67                 shareManager.setLatestRotation(rotation);
68             }
69         },
71         /* a share is considered `active` if its latest rotation key
72          * was encrypted with an active user key - if the user key is
73          * inactive, then share should be ignored until it becomes
74          * active again */
75         isActive(userKeys = []) {
76             try {
77                 const { targetType } = shareManager.getShare();
78                 const latestRotation = shareManager.getLatestRotation();
80                 if (targetType === ShareType.Vault) {
81                     const vaultKey = shareManager.getVaultKey(latestRotation);
82                     return userKeys.some(({ ID }) => ID === vaultKey.userKeyId);
83                 }
85                 return false;
86             } catch (_) {
87                 return false;
88             }
89         },
91         serialize: () => ({
92             ...shareContext,
93             share: (() => {
94                 switch (shareContext.share.targetType) {
95                     case ShareType.Vault: {
96                         return {
97                             ...shareContext.share,
98                             content: uint8ArrayToBase64String(shareContext.share.content),
99                         };
100                     }
101                     case ShareType.Item:
102                         return shareContext.share;
103                 }
104             })(),
105             vaultKeys: [...shareContext.vaultKeys.entries()].map(([rotation, vaultKey]) => [
106                 rotation,
107                 {
108                     rotation: vaultKey.rotation,
109                     raw: uint8ArrayToBase64String(vaultKey.raw),
110                     userKeyId: vaultKey.userKeyId,
111                 },
112             ]),
113             itemKeys: [],
114         }),
115     };
117     return shareManager as ShareManager<T>;
120 createShareManager.fromSnapshot = async (snapshot: SerializedCryptoContext<ShareContext>): Promise<ShareManager> => {
121     /**
122      * Item shares do not have a content property
123      * Only encode the vault content when dealing
124      * with VaultShares
125      */
126     const share: OpenedShare = (() => {
127         switch (snapshot.share.targetType) {
128             case ShareType.Vault: {
129                 return { ...snapshot.share, content: base64StringToUint8Array(snapshot.share.content) };
130             }
131             case ShareType.Item: {
132                 return snapshot.share;
133             }
134         }
135     })();
137     const vaultKeys: [Rotation, VaultKey][] = await Promise.all(
138         snapshot.vaultKeys.map(async ([rotation, vaultKey]) => {
139             const rawKey = base64StringToUint8Array(vaultKey.raw);
140             return [
141                 rotation,
142                 {
143                     rotation: vaultKey.rotation,
144                     raw: rawKey,
145                     key: await importSymmetricKey(rawKey),
146                     userKeyId: vaultKey.userKeyId,
147                 },
148             ];
149         })
150     );
152     return createShareManager(share, {
153         ...snapshot,
154         share,
155         vaultKeys: new Map(vaultKeys),
156         itemKeys: new Map(),
157     });