@protontech/drive-sdk 0.2.1 → 0.3.1

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 (165) hide show
  1. package/dist/crypto/interface.d.ts +5 -0
  2. package/dist/diagnostic/httpClient.d.ts +3 -3
  3. package/dist/diagnostic/interface.d.ts +26 -29
  4. package/dist/diagnostic/sdkDiagnostic.js +50 -24
  5. package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
  6. package/dist/errors.d.ts +3 -3
  7. package/dist/errors.js +7 -7
  8. package/dist/errors.js.map +1 -1
  9. package/dist/interface/author.d.ts +1 -1
  10. package/dist/interface/events.d.ts +1 -1
  11. package/dist/interface/events.js.map +1 -1
  12. package/dist/interface/httpClient.d.ts +5 -5
  13. package/dist/interface/index.d.ts +15 -5
  14. package/dist/internal/apiService/apiService.js +12 -4
  15. package/dist/internal/apiService/apiService.js.map +1 -1
  16. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  17. package/dist/internal/apiService/errorCodes.js.map +1 -1
  18. package/dist/internal/apiService/errors.d.ts +4 -3
  19. package/dist/internal/apiService/errors.js +7 -4
  20. package/dist/internal/apiService/errors.js.map +1 -1
  21. package/dist/internal/apiService/errors.test.js +2 -1
  22. package/dist/internal/apiService/errors.test.js.map +1 -1
  23. package/dist/internal/events/index.d.ts +1 -1
  24. package/dist/internal/nodes/apiService.js +3 -0
  25. package/dist/internal/nodes/apiService.js.map +1 -1
  26. package/dist/internal/nodes/apiService.test.js +18 -0
  27. package/dist/internal/nodes/apiService.test.js.map +1 -1
  28. package/dist/internal/nodes/cryptoCache.js +6 -7
  29. package/dist/internal/nodes/cryptoCache.js.map +1 -1
  30. package/dist/internal/nodes/cryptoCache.test.js +4 -7
  31. package/dist/internal/nodes/cryptoCache.test.js.map +1 -1
  32. package/dist/internal/nodes/cryptoService.js +44 -20
  33. package/dist/internal/nodes/cryptoService.js.map +1 -1
  34. package/dist/internal/nodes/nodesAccess.js +2 -2
  35. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  36. package/dist/internal/nodes/nodesManagement.js +0 -2
  37. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  38. package/dist/internal/shares/cryptoCache.d.ts +4 -3
  39. package/dist/internal/shares/cryptoCache.js +23 -6
  40. package/dist/internal/shares/cryptoCache.js.map +1 -1
  41. package/dist/internal/shares/cryptoCache.test.js +3 -2
  42. package/dist/internal/shares/cryptoCache.test.js.map +1 -1
  43. package/dist/internal/shares/index.js +1 -1
  44. package/dist/internal/shares/index.js.map +1 -1
  45. package/dist/internal/sharing/cryptoService.js +8 -6
  46. package/dist/internal/sharing/cryptoService.js.map +1 -1
  47. package/dist/internal/sharing/cryptoService.test.js +13 -0
  48. package/dist/internal/sharing/cryptoService.test.js.map +1 -1
  49. package/dist/internal/sharing/index.js +1 -1
  50. package/dist/internal/sharing/index.js.map +1 -1
  51. package/dist/internal/sharing/interface.d.ts +0 -4
  52. package/dist/internal/sharing/sharingAccess.d.ts +1 -0
  53. package/dist/internal/sharing/sharingAccess.js +6 -1
  54. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  55. package/dist/internal/sharing/sharingAccess.test.js +3 -3
  56. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  57. package/dist/internal/sharing/sharingManagement.d.ts +3 -1
  58. package/dist/internal/sharing/sharingManagement.js +37 -17
  59. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  60. package/dist/internal/sharing/sharingManagement.test.js +61 -14
  61. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  62. package/dist/internal/sharingPublic/apiService.d.ts +19 -0
  63. package/dist/internal/sharingPublic/apiService.js +134 -0
  64. package/dist/internal/sharingPublic/apiService.js.map +1 -0
  65. package/dist/internal/sharingPublic/cryptoCache.d.ts +19 -0
  66. package/dist/internal/sharingPublic/cryptoCache.js +72 -0
  67. package/dist/internal/sharingPublic/cryptoCache.js.map +1 -0
  68. package/dist/internal/sharingPublic/cryptoService.d.ts +23 -0
  69. package/dist/internal/sharingPublic/cryptoService.js +120 -0
  70. package/dist/internal/sharingPublic/cryptoService.js.map +1 -0
  71. package/dist/internal/sharingPublic/index.d.ts +15 -0
  72. package/dist/internal/sharingPublic/index.js +27 -0
  73. package/dist/internal/sharingPublic/index.js.map +1 -0
  74. package/dist/internal/sharingPublic/interface.d.ts +48 -0
  75. package/dist/internal/sharingPublic/interface.js +3 -0
  76. package/dist/internal/sharingPublic/interface.js.map +1 -0
  77. package/dist/internal/sharingPublic/manager.d.ts +19 -0
  78. package/dist/internal/sharingPublic/manager.js +79 -0
  79. package/dist/internal/sharingPublic/manager.js.map +1 -0
  80. package/dist/internal/sharingPublic/session/apiService.d.ts +28 -0
  81. package/dist/internal/sharingPublic/session/apiService.js +55 -0
  82. package/dist/internal/sharingPublic/session/apiService.js.map +1 -0
  83. package/dist/internal/sharingPublic/session/httpClient.d.ts +16 -0
  84. package/dist/internal/sharingPublic/session/httpClient.js +41 -0
  85. package/dist/internal/sharingPublic/session/httpClient.js.map +1 -0
  86. package/dist/internal/sharingPublic/session/index.d.ts +1 -0
  87. package/dist/internal/sharingPublic/session/index.js +6 -0
  88. package/dist/internal/sharingPublic/session/index.js.map +1 -0
  89. package/dist/internal/sharingPublic/session/interface.d.ts +18 -0
  90. package/dist/internal/sharingPublic/session/interface.js +3 -0
  91. package/dist/internal/sharingPublic/session/interface.js.map +1 -0
  92. package/dist/internal/sharingPublic/session/manager.d.ts +49 -0
  93. package/dist/internal/sharingPublic/session/manager.js +75 -0
  94. package/dist/internal/sharingPublic/session/manager.js.map +1 -0
  95. package/dist/internal/sharingPublic/session/session.d.ts +34 -0
  96. package/dist/internal/sharingPublic/session/session.js +67 -0
  97. package/dist/internal/sharingPublic/session/session.js.map +1 -0
  98. package/dist/internal/sharingPublic/session/url.d.ts +12 -0
  99. package/dist/internal/sharingPublic/session/url.js +23 -0
  100. package/dist/internal/sharingPublic/session/url.js.map +1 -0
  101. package/dist/internal/sharingPublic/session/url.test.d.ts +1 -0
  102. package/dist/internal/sharingPublic/session/url.test.js +59 -0
  103. package/dist/internal/sharingPublic/session/url.test.js.map +1 -0
  104. package/dist/internal/upload/manager.js +1 -3
  105. package/dist/internal/upload/manager.js.map +1 -1
  106. package/dist/internal/upload/manager.test.js +2 -2
  107. package/dist/internal/upload/manager.test.js.map +1 -1
  108. package/dist/protonDriveClient.d.ts +25 -10
  109. package/dist/protonDriveClient.js +44 -22
  110. package/dist/protonDriveClient.js.map +1 -1
  111. package/dist/protonDrivePublicLinkClient.d.ts +48 -0
  112. package/dist/protonDrivePublicLinkClient.js +71 -0
  113. package/dist/protonDrivePublicLinkClient.js.map +1 -0
  114. package/package.json +1 -1
  115. package/src/crypto/interface.ts +11 -0
  116. package/src/diagnostic/httpClient.ts +4 -4
  117. package/src/diagnostic/interface.ts +27 -29
  118. package/src/diagnostic/sdkDiagnostic.ts +58 -30
  119. package/src/errors.ts +5 -5
  120. package/src/interface/author.ts +1 -1
  121. package/src/interface/events.ts +1 -7
  122. package/src/interface/httpClient.ts +5 -5
  123. package/src/interface/index.ts +18 -6
  124. package/src/internal/apiService/apiService.ts +13 -4
  125. package/src/internal/apiService/errorCodes.ts +1 -0
  126. package/src/internal/apiService/errors.test.ts +2 -1
  127. package/src/internal/apiService/errors.ts +15 -4
  128. package/src/internal/events/index.ts +1 -1
  129. package/src/internal/nodes/apiService.test.ts +28 -0
  130. package/src/internal/nodes/apiService.ts +3 -0
  131. package/src/internal/nodes/cryptoCache.test.ts +4 -7
  132. package/src/internal/nodes/cryptoCache.ts +6 -7
  133. package/src/internal/nodes/cryptoService.ts +68 -34
  134. package/src/internal/nodes/interface.ts +2 -0
  135. package/src/internal/nodes/nodesAccess.ts +2 -2
  136. package/src/internal/nodes/nodesManagement.ts +0 -3
  137. package/src/internal/shares/cryptoCache.test.ts +3 -2
  138. package/src/internal/shares/cryptoCache.ts +26 -7
  139. package/src/internal/shares/index.ts +1 -1
  140. package/src/internal/sharing/cryptoService.test.ts +22 -1
  141. package/src/internal/sharing/cryptoService.ts +8 -6
  142. package/src/internal/sharing/index.ts +1 -0
  143. package/src/internal/sharing/interface.ts +0 -4
  144. package/src/internal/sharing/sharingAccess.test.ts +4 -4
  145. package/src/internal/sharing/sharingAccess.ts +6 -0
  146. package/src/internal/sharing/sharingManagement.test.ts +87 -24
  147. package/src/internal/sharing/sharingManagement.ts +56 -16
  148. package/src/internal/sharingPublic/apiService.ts +164 -0
  149. package/src/internal/sharingPublic/cryptoCache.ts +79 -0
  150. package/src/internal/sharingPublic/cryptoService.ts +162 -0
  151. package/src/internal/sharingPublic/index.ts +40 -0
  152. package/src/internal/sharingPublic/interface.ts +59 -0
  153. package/src/internal/sharingPublic/manager.ts +85 -0
  154. package/src/internal/sharingPublic/session/apiService.ts +74 -0
  155. package/src/internal/sharingPublic/session/httpClient.ts +48 -0
  156. package/src/internal/sharingPublic/session/index.ts +1 -0
  157. package/src/internal/sharingPublic/session/interface.ts +20 -0
  158. package/src/internal/sharingPublic/session/manager.ts +97 -0
  159. package/src/internal/sharingPublic/session/session.ts +78 -0
  160. package/src/internal/sharingPublic/session/url.test.ts +72 -0
  161. package/src/internal/sharingPublic/session/url.ts +23 -0
  162. package/src/internal/upload/manager.test.ts +2 -2
  163. package/src/internal/upload/manager.ts +2 -4
  164. package/src/protonDriveClient.ts +64 -27
  165. package/src/protonDrivePublicLinkClient.ts +121 -0
@@ -61,6 +61,8 @@ export class NodesCryptoService {
61
61
  node: EncryptedNode,
62
62
  parentKey: PrivateKey,
63
63
  ): Promise<{ node: DecryptedUnparsedNode; keys?: DecryptedNodeKeys }> {
64
+ const start = Date.now();
65
+
64
66
  const commonNodeMetadata = {
65
67
  ...node,
66
68
  encryptedCrypto: undefined,
@@ -89,16 +91,17 @@ export class NodesCryptoService {
89
91
  : nodeParentKeys;
90
92
  }
91
93
 
92
- const { name, author: nameAuthor } = await this.decryptName(node, parentKey, nameVerificationKeys);
93
-
94
- let membership;
95
- if (node.membership) {
96
- membership = await this.decryptMembership(node);
97
- }
94
+ // Start promises early, but await them only when required to do
95
+ // as much work as possible in parallel.
96
+ const [membershipPromise, namePromise, keyPromise] = [
97
+ node.membership ? this.decryptMembership(node) : undefined,
98
+ this.decryptName(node, parentKey, nameVerificationKeys),
99
+ this.decryptKey(node, parentKey, keyVerificationKeys),
100
+ ];
98
101
 
99
102
  let passphrase, key, passphraseSessionKey, keyAuthor;
100
103
  try {
101
- const keyResult = await this.decryptKey(node, parentKey, keyVerificationKeys);
104
+ const keyResult = await keyPromise;
102
105
  passphrase = keyResult.passphrase;
103
106
  key = keyResult.key;
104
107
  passphraseSessionKey = keyResult.passphraseSessionKey;
@@ -107,12 +110,17 @@ export class NodesCryptoService {
107
110
  void this.reportDecryptionError(node, 'nodeKey', error);
108
111
  const message = getErrorMessage(error);
109
112
  const errorMessage = c('Error').t`Failed to decrypt node key: ${message}`;
113
+ const { name, author: nameAuthor } = await namePromise;
114
+ const membership = await membershipPromise;
110
115
  return {
111
116
  node: {
112
117
  ...commonNodeMetadata,
113
118
  name,
114
119
  keyAuthor: resultError({
115
- claimedAuthor: node.encryptedCrypto.signatureEmail,
120
+ claimedAuthor: getClaimedAuthor(
121
+ node.encryptedCrypto.signatureEmail,
122
+ keyVerificationKeys.length === 0,
123
+ ),
116
124
  error: errorMessage,
117
125
  }),
118
126
  nameAuthor,
@@ -131,8 +139,23 @@ export class NodesCryptoService {
131
139
  let folder;
132
140
  let folderExtendedAttributesAuthor;
133
141
  if ('folder' in node.encryptedCrypto) {
142
+ const folderExtendedAttributesVerificationKeys = node.encryptedCrypto.signatureEmail
143
+ ? signatureEmailKeys
144
+ : [key];
145
+
146
+ const [hashKeyPromise, folderExtendedAttributesPromise] = [
147
+ this.decryptHashKey(node, key, signatureEmailKeys),
148
+ this.decryptExtendedAttributes(
149
+ node,
150
+ node.encryptedCrypto.folder.armoredExtendedAttributes,
151
+ key,
152
+ folderExtendedAttributesVerificationKeys,
153
+ node.encryptedCrypto.signatureEmail,
154
+ ),
155
+ ];
156
+
134
157
  try {
135
- const hashKeyResult = await this.decryptHashKey(node, key, signatureEmailKeys);
158
+ const hashKeyResult = await hashKeyPromise;
136
159
  hashKey = hashKeyResult.hashKey;
137
160
  hashKeyAuthor = hashKeyResult.author;
138
161
  } catch (error: unknown) {
@@ -141,16 +164,7 @@ export class NodesCryptoService {
141
164
  }
142
165
 
143
166
  try {
144
- const folderExtendedAttributesVerificationKeys = node.encryptedCrypto.signatureEmail
145
- ? signatureEmailKeys
146
- : [key];
147
- const extendedAttributesResult = await this.decryptExtendedAttributes(
148
- node,
149
- node.encryptedCrypto.folder.armoredExtendedAttributes,
150
- key,
151
- folderExtendedAttributesVerificationKeys,
152
- node.encryptedCrypto.signatureEmail,
153
- );
167
+ const extendedAttributesResult = await folderExtendedAttributesPromise;
154
168
  folder = {
155
169
  extendedAttributes: extendedAttributesResult.extendedAttributes,
156
170
  };
@@ -165,10 +179,20 @@ export class NodesCryptoService {
165
179
  let contentKeyPacketSessionKey;
166
180
  let contentKeyPacketAuthor;
167
181
  if ('file' in node.encryptedCrypto) {
182
+ const [activeRevisionPromise, contentKeyPacketSessionKeyPromise] = [
183
+ this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
184
+ this.driveCrypto.decryptAndVerifySessionKey(
185
+ node.encryptedCrypto.file.base64ContentKeyPacket,
186
+ node.encryptedCrypto.file.armoredContentKeyPacketSignature,
187
+ key,
188
+ // Content key packet is signed with the node key, but
189
+ // in the past some clients signed with the address key.
190
+ [key, ...keyVerificationKeys],
191
+ ),
192
+ ];
193
+
168
194
  try {
169
- activeRevision = resultOk(
170
- await this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
171
- );
195
+ activeRevision = resultOk(await activeRevisionPromise);
172
196
  } catch (error: unknown) {
173
197
  void this.reportDecryptionError(node, 'nodeExtendedAttributes', error);
174
198
  const message = getErrorMessage(error);
@@ -177,18 +201,10 @@ export class NodesCryptoService {
177
201
  }
178
202
 
179
203
  try {
180
- const keySessionKeyResult = await this.driveCrypto.decryptAndVerifySessionKey(
181
- node.encryptedCrypto.file.base64ContentKeyPacket,
182
- node.encryptedCrypto.file.armoredContentKeyPacketSignature,
183
- key,
184
- // Content key packet is signed with the node key, but
185
- // in the past some clients signed with the address key.
186
- [key, ...keyVerificationKeys],
187
- );
188
-
204
+ const keySessionKeyResult = await contentKeyPacketSessionKeyPromise;
189
205
  contentKeyPacketSessionKey = keySessionKeyResult.sessionKey;
190
206
  contentKeyPacketAuthor =
191
- keySessionKeyResult.verified &&
207
+ keySessionKeyResult.verified !== undefined &&
192
208
  (await this.handleClaimedAuthor(
193
209
  node,
194
210
  'nodeContentKey',
@@ -230,6 +246,13 @@ export class NodesCryptoService {
230
246
  finalKeyAuthor = keyAuthor;
231
247
  }
232
248
 
249
+ const { name, author: nameAuthor } = await namePromise;
250
+ const membership = await membershipPromise;
251
+
252
+ const end = Date.now();
253
+ const duration = end - start;
254
+ this.logger.debug(`Node ${node.uid} decrypted in ${duration}ms`);
255
+
233
256
  return {
234
257
  node: {
235
258
  ...commonNodeMetadata,
@@ -319,7 +342,7 @@ export class NodesCryptoService {
319
342
  return {
320
343
  name: resultError(new Error(errorMessage)),
321
344
  author: resultError({
322
- claimedAuthor: nameSignatureEmail,
345
+ claimedAuthor: getClaimedAuthor(nameSignatureEmail, verificationKeys.length === 0),
323
346
  error: errorMessage,
324
347
  }),
325
348
  };
@@ -635,6 +658,7 @@ export class NodesCryptoService {
635
658
  if (this.reportedVerificationErrors.has(node.uid)) {
636
659
  return;
637
660
  }
661
+ this.reportedVerificationErrors.add(node.uid);
638
662
 
639
663
  const fromBefore2024 = node.creationTime < new Date('2024-01-01');
640
664
 
@@ -661,7 +685,6 @@ export class NodesCryptoService {
661
685
  error: verificationErrors?.map((e) => e.message).join(', '),
662
686
  uid: node.uid,
663
687
  });
664
- this.reportedVerificationErrors.add(node.uid);
665
688
  }
666
689
 
667
690
  private async reportDecryptionError(node: EncryptedNode, field: MetricsDecryptionErrorField, error: unknown) {
@@ -716,3 +739,14 @@ function handleClaimedAuthor(
716
739
  error: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
717
740
  });
718
741
  }
742
+
743
+ function getClaimedAuthor(
744
+ claimedAuthor?: string,
745
+ notAvailableVerificationKeys = false,
746
+ ): string | AnonymousUser | undefined {
747
+ if (!claimedAuthor && notAvailableVerificationKeys) {
748
+ return null as AnonymousUser;
749
+ }
750
+
751
+ return claimedAuthor;
752
+ }
@@ -18,6 +18,8 @@ import {
18
18
  interface BaseNode {
19
19
  // Internal metadata
20
20
  hash?: string; // root node doesn't have any hash
21
+ // ecnryptedName should not be needed to keep, nameSessionKey should be enough.
22
+ // We will improve this in the future.
21
23
  encryptedName: string;
22
24
 
23
25
  // Basic node metadata
@@ -24,7 +24,7 @@ const BATCH_LOADING_SIZE = 30;
24
24
  // It is a trade-off between performance and memory usage.
25
25
  // Higher number means more memory usage, but faster decryption.
26
26
  // Lower number means less memory usage, but slower decryption.
27
- const DECRYPTION_CONCURRENCY = 15;
27
+ const DECRYPTION_CONCURRENCY = 30;
28
28
 
29
29
  /**
30
30
  * Provides access to node metadata.
@@ -234,7 +234,7 @@ export class NodesAccess {
234
234
 
235
235
  if (errors.length > 0) {
236
236
  this.logger.error(`Failed to decrypt ${errors.length} nodes`, errors);
237
- throw new ProtonDriveError(c('Error').t`Failed to decrypt some nodes`, { cause: errors });
237
+ throw new DecryptionError(c('Error').t`Failed to decrypt some nodes`, { cause: errors });
238
238
  }
239
239
 
240
240
  const missingNodeUids = nodeUids.filter((nodeUid) => !returnedNodeUids.includes(nodeUid));
@@ -189,11 +189,8 @@ export class NodesManagement {
189
189
  }
190
190
 
191
191
  async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
192
- const deletedNodeUids = [];
193
-
194
192
  for await (const result of this.apiService.deleteNodes(nodeUids, signal)) {
195
193
  if (result.ok) {
196
- deletedNodeUids.push(result.uid);
197
194
  await this.nodesAccess.notifyNodeDeleted(result.uid);
198
195
  }
199
196
  yield result;
@@ -1,6 +1,7 @@
1
1
  import { PrivateKey, SessionKey } from '../../crypto';
2
2
  import { MemoryCache } from '../../cache';
3
3
  import { CachedCryptoMaterial } from '../../interface';
4
+ import { getMockLogger } from '../../tests/logger';
4
5
  import { SharesCryptoCache } from './cryptoCache';
5
6
 
6
7
  describe('sharesCryptoCache', () => {
@@ -17,7 +18,7 @@ describe('sharesCryptoCache', () => {
17
18
 
18
19
  beforeEach(() => {
19
20
  memoryCache = new MemoryCache();
20
- cache = new SharesCryptoCache(memoryCache);
21
+ cache = new SharesCryptoCache(getMockLogger(), memoryCache);
21
22
  });
22
23
 
23
24
  it('should store and retrieve keys', async () => {
@@ -53,7 +54,7 @@ describe('sharesCryptoCache', () => {
53
54
  const keys = { key: generatePrivateKey('privateKey'), passphraseSessionKey: generateSessionKey('sessionKey') };
54
55
 
55
56
  await cache.setShareKey(shareId, keys);
56
- await cache.removeShareKey([shareId]);
57
+ await cache.removeShareKeys([shareId]);
57
58
 
58
59
  try {
59
60
  await cache.getShareKey(shareId);
@@ -1,4 +1,4 @@
1
- import { ProtonDriveCryptoCache } from '../../interface';
1
+ import { Logger, ProtonDriveCryptoCache } from '../../interface';
2
2
  import { DecryptedShareKey } from './interface';
3
3
 
4
4
  /**
@@ -13,23 +13,42 @@ import { DecryptedShareKey } from './interface';
13
13
  * only the root node, thus share cache is not needed.
14
14
  */
15
15
  export class SharesCryptoCache {
16
- constructor(private driveCache: ProtonDriveCryptoCache) {
16
+ constructor(
17
+ private logger: Logger,
18
+ private driveCache: ProtonDriveCryptoCache,
19
+ ) {
20
+ this.logger = logger;
17
21
  this.driveCache = driveCache;
18
22
  }
19
23
 
20
24
  async setShareKey(shareId: string, key: DecryptedShareKey): Promise<void> {
21
- await this.driveCache.setEntity(getCacheUid(shareId), key);
25
+ await this.driveCache.setEntity(getCacheKey(shareId), {
26
+ shareKey: key,
27
+ });
22
28
  }
23
29
 
24
30
  async getShareKey(shareId: string): Promise<DecryptedShareKey> {
25
- return this.driveCache.getEntity(getCacheUid(shareId));
31
+ const nodeKeysData = await this.driveCache.getEntity(getCacheKey(shareId));
32
+ if (!nodeKeysData.shareKey) {
33
+ try {
34
+ await this.removeShareKeys([shareId]);
35
+ } catch (removingError: unknown) {
36
+ // The node keys will not be returned, thus SDK will re-fetch
37
+ // and re-cache it. Setting it again should then fix the problem.
38
+ this.logger.warn(
39
+ `Failed to remove corrupted node keys from the cache: ${removingError instanceof Error ? removingError.message : removingError}`,
40
+ );
41
+ }
42
+ throw new Error(`Failed to deserialize node keys`);
43
+ }
44
+ return nodeKeysData.shareKey;
26
45
  }
27
46
 
28
- async removeShareKey(shareIds: string[]): Promise<void> {
29
- await this.driveCache.removeEntities(shareIds.map(getCacheUid));
47
+ async removeShareKeys(shareIds: string[]): Promise<void> {
48
+ await this.driveCache.removeEntities(shareIds.map(getCacheKey));
30
49
  }
31
50
  }
32
51
 
33
- function getCacheUid(shareId: string) {
52
+ function getCacheKey(shareId: string) {
34
53
  return `shareKey-${shareId}`;
35
54
  }
@@ -33,7 +33,7 @@ export function initSharesModule(
33
33
  ) {
34
34
  const api = new SharesAPIService(apiService);
35
35
  const cache = new SharesCache(telemetry.getLogger('shares-cache'), driveEntitiesCache);
36
- const cryptoCache = new SharesCryptoCache(driveCryptoCache);
36
+ const cryptoCache = new SharesCryptoCache(telemetry.getLogger('shares-cache'), driveCryptoCache);
37
37
  const cryptoService = new SharesCryptoService(telemetry, crypto, account);
38
38
  const sharesManager = new SharesManager(
39
39
  telemetry.getLogger('shares'),
@@ -9,7 +9,7 @@ import {
9
9
  } from '../../interface';
10
10
  import { getMockTelemetry } from '../../tests/telemetry';
11
11
  import { SharesService } from './interface';
12
- import { SharingCryptoService } from './cryptoService';
12
+ import { PUBLIC_LINK_GENERATED_PASSWORD_LENGTH, SharingCryptoService } from './cryptoService';
13
13
 
14
14
  describe('SharingCryptoService', () => {
15
15
  let telemetry: ProtonDriveTelemetry;
@@ -87,6 +87,27 @@ describe('SharingCryptoService', () => {
87
87
  expect(telemetry.recordMetric).not.toHaveBeenCalled();
88
88
  });
89
89
 
90
+ it('should decrypt bookmark with custom password', async () => {
91
+ // First 12 characters are the generated password. Anything beyond is the custom password.
92
+ driveCrypto.decryptShareUrlPassword = jest.fn().mockResolvedValue('urlPassword1WithCustomPassword');
93
+
94
+ const result = await cryptoService.decryptBookmark(encryptedBookmark);
95
+
96
+ expect(result).toMatchObject({
97
+ url: resultOk('https://drive.proton.me/urls/tokenId#urlPassword1'),
98
+ nodeName: resultOk('nodeName'),
99
+ });
100
+ expect(driveCrypto.decryptShareUrlPassword).toHaveBeenCalledWith('encryptedUrlPassword', ['addressKey']);
101
+ expect(driveCrypto.decryptKeyWithSrpPassword).toHaveBeenCalledWith(
102
+ 'urlPassword1WithCustomPassword',
103
+ 'base64SharePasswordSalt',
104
+ 'armoredKey',
105
+ 'armoredPassphrase',
106
+ );
107
+ expect(driveCrypto.decryptNodeName).toHaveBeenCalledWith('encryptedName', 'decryptedKey', []);
108
+ expect(telemetry.recordMetric).not.toHaveBeenCalled();
109
+ });
110
+
90
111
  it('should handle undecryptable URL password', async () => {
91
112
  const error = new Error('Failed to decrypt URL password');
92
113
  driveCrypto.decryptShareUrlPassword = jest.fn().mockRejectedValue(error);
@@ -37,6 +37,7 @@ import {
37
37
  EncryptedBookmark,
38
38
  SharesService,
39
39
  } from './interface';
40
+ import { DecryptionError } from '../../errors';
40
41
 
41
42
  // Version 2 of bcrypt with 2**10 rounds.
42
43
  // https://en.wikipedia.org/wiki/Bcrypt#Description
@@ -425,10 +426,11 @@ export class SharingCryptoService {
425
426
  // TODO: Signatures are not checked and not specified in the interface.
426
427
  // In the future, we will need to add authorship verification.
427
428
 
429
+ let password: string;
428
430
  let urlPassword: string;
429
431
  let customPassword: Result<string | undefined, Error>;
430
432
  try {
431
- const password = await this.decryptBookmarkUrlPassword(encryptedBookmark);
433
+ password = await this.decryptBookmarkUrlPassword(encryptedBookmark);
432
434
  const result = splitGeneratedAndCustomPassword(password);
433
435
  urlPassword = result.password;
434
436
  customPassword = resultOk(result.customPassword);
@@ -446,7 +448,7 @@ export class SharingCryptoService {
446
448
 
447
449
  let shareKey: PrivateKey;
448
450
  try {
449
- shareKey = await this.decryptBookmarkKey(encryptedBookmark, urlPassword);
451
+ shareKey = await this.decryptBookmarkKey(encryptedBookmark, password);
450
452
  } catch (originalError: unknown) {
451
453
  const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
452
454
  return {
@@ -493,15 +495,15 @@ export class SharingCryptoService {
493
495
 
494
496
  const message = getErrorMessage(error);
495
497
  const errorMessage = c('Error').t`Failed to decrypt bookmark password: ${message}`;
496
- throw new Error(errorMessage);
498
+ throw new DecryptionError(errorMessage, { cause: error });
497
499
  }
498
500
  }
499
501
 
500
- private async decryptBookmarkKey(encryptedBookmark: EncryptedBookmark, urlPassword: string): Promise<PrivateKey> {
502
+ private async decryptBookmarkKey(encryptedBookmark: EncryptedBookmark, password: string): Promise<PrivateKey> {
501
503
  try {
502
504
  // Use the password to decrypt the share key.
503
505
  const { key: shareKey } = await this.driveCrypto.decryptKeyWithSrpPassword(
504
- urlPassword,
506
+ password,
505
507
  encryptedBookmark.url.base64SharePasswordSalt,
506
508
  encryptedBookmark.share.armoredKey,
507
509
  encryptedBookmark.share.armoredPassphrase,
@@ -519,7 +521,7 @@ export class SharingCryptoService {
519
521
 
520
522
  const message = getErrorMessage(error);
521
523
  const errorMessage = c('Error').t`Failed to decrypt bookmark key: ${message}`;
522
- throw new Error(errorMessage);
524
+ throw new DecryptionError(errorMessage, { cause: error });
523
525
  }
524
526
  }
525
527
 
@@ -32,6 +32,7 @@ export function initSharingModule(
32
32
  const sharingManagement = new SharingManagement(
33
33
  telemetry.getLogger('sharing'),
34
34
  api,
35
+ cache,
35
36
  cryptoService,
36
37
  account,
37
38
  sharesService,
@@ -145,10 +145,6 @@ export interface SharesService {
145
145
  getMyFilesIDs(): Promise<{ volumeId: string }>;
146
146
  loadEncryptedShare(shareId: string): Promise<EncryptedShare>;
147
147
  getMyFilesShareMemberEmailKey(): Promise<{
148
- addressId: string;
149
- addressKey: PrivateKey;
150
- }>;
151
- getContextShareMemberEmailKey(shareId: string): Promise<{
152
148
  email: string;
153
149
  addressId: string;
154
150
  addressKey: PrivateKey;
@@ -3,7 +3,7 @@ import { SharingAPIService } from './apiService';
3
3
  import { SharingCache } from './cache';
4
4
  import { SharingCryptoService } from './cryptoService';
5
5
  import { SharesService, NodesService } from './interface';
6
- import { SharingAccess } from './sharingAccess';
6
+ import { SharingAccess, BATCH_LOADING_SIZE } from './sharingAccess';
7
7
 
8
8
  describe('SharingAccess', () => {
9
9
  let apiService: SharingAPIService;
@@ -14,7 +14,7 @@ describe('SharingAccess', () => {
14
14
 
15
15
  let sharingAccess: SharingAccess;
16
16
 
17
- const nodeUids = Array.from({ length: 15 }, (_, i) => `nodeUid${i}`);
17
+ const nodeUids = Array.from({ length: BATCH_LOADING_SIZE + 5 }, (_, i) => `nodeUid${i}`);
18
18
  const nodes = nodeUids.map((nodeUid) => ({ nodeUid }));
19
19
  const nodeUidsIterator = async function* () {
20
20
  for (const nodeUid of nodeUids) {
@@ -84,7 +84,7 @@ describe('SharingAccess', () => {
84
84
 
85
85
  expect(result).toEqual(nodes);
86
86
  expect(apiService.iterateSharedNodeUids).toHaveBeenCalledWith('volumeId', undefined);
87
- expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // 15 / 10 per batch
87
+ expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
88
88
  expect(cache.setSharedByMeNodeUids).toHaveBeenCalledWith(nodeUids);
89
89
  });
90
90
  });
@@ -107,7 +107,7 @@ describe('SharingAccess', () => {
107
107
 
108
108
  expect(result).toEqual(nodes);
109
109
  expect(apiService.iterateSharedWithMeNodeUids).toHaveBeenCalledWith(undefined);
110
- expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // 15 / 10 per batch
110
+ expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
111
111
  expect(cache.setSharedWithMeNodeUids).toHaveBeenCalledWith(nodeUids);
112
112
  });
113
113
  });
@@ -9,6 +9,10 @@ import { SharingCache } from './cache';
9
9
  import { SharingCryptoService } from './cryptoService';
10
10
  import { SharesService, NodesService } from './interface';
11
11
 
12
+ // This is the number of nodes that are loaded in parallel.
13
+ // It is a trade-off between initial wait time and overhead of API calls.
14
+ export const BATCH_LOADING_SIZE = 30;
15
+
12
16
  /**
13
17
  * Provides high-level actions for access shared nodes.
14
18
  *
@@ -66,6 +70,7 @@ export class SharingAccess {
66
70
  ): AsyncGenerator<DecryptedNode> {
67
71
  const batchLoading = new BatchLoading<string, DecryptedNode>({
68
72
  iterateItems: (nodeUids) => this.iterateNodesAndIgnoreMissingOnes(nodeUids, signal),
73
+ batchSize: BATCH_LOADING_SIZE,
69
74
  });
70
75
  for (const nodeUid of nodeUids) {
71
76
  yield* batchLoading.load(nodeUid);
@@ -81,6 +86,7 @@ export class SharingAccess {
81
86
  const loadedNodeUids = [];
82
87
  const batchLoading = new BatchLoading<string, DecryptedNode>({
83
88
  iterateItems: (nodeUids) => this.iterateNodesAndIgnoreMissingOnes(nodeUids, signal),
89
+ batchSize: BATCH_LOADING_SIZE,
84
90
  });
85
91
  for await (const nodeUid of nodeUidsIterator) {
86
92
  loadedNodeUids.push(nodeUid);