@protontech/drive-sdk 0.3.0 → 0.3.2

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 (178) hide show
  1. package/dist/crypto/driveCrypto.d.ts +1 -1
  2. package/dist/crypto/driveCrypto.js.map +1 -1
  3. package/dist/crypto/interface.d.ts +6 -1
  4. package/dist/crypto/openPGPCrypto.d.ts +1 -1
  5. package/dist/crypto/openPGPCrypto.js +4 -1
  6. package/dist/crypto/openPGPCrypto.js.map +1 -1
  7. package/dist/diagnostic/httpClient.d.ts +3 -3
  8. package/dist/interface/httpClient.d.ts +5 -5
  9. package/dist/interface/index.d.ts +15 -5
  10. package/dist/internal/apiService/apiService.js +1 -1
  11. package/dist/internal/apiService/apiService.js.map +1 -1
  12. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  13. package/dist/internal/apiService/errorCodes.js.map +1 -1
  14. package/dist/internal/apiService/errors.d.ts +4 -3
  15. package/dist/internal/apiService/errors.js +7 -4
  16. package/dist/internal/apiService/errors.js.map +1 -1
  17. package/dist/internal/apiService/errors.test.js +2 -1
  18. package/dist/internal/apiService/errors.test.js.map +1 -1
  19. package/dist/internal/download/cryptoService.js +2 -2
  20. package/dist/internal/download/cryptoService.js.map +1 -1
  21. package/dist/internal/download/fileDownloader.js +2 -2
  22. package/dist/internal/download/fileDownloader.js.map +1 -1
  23. package/dist/internal/download/fileDownloader.test.js +3 -1
  24. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  25. package/dist/internal/events/index.d.ts +1 -1
  26. package/dist/internal/nodes/cache.js +3 -1
  27. package/dist/internal/nodes/cache.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/cryptoReporter.d.ts +20 -0
  33. package/dist/internal/nodes/cryptoReporter.js +96 -0
  34. package/dist/internal/nodes/cryptoReporter.js.map +1 -0
  35. package/dist/internal/nodes/cryptoService.d.ts +17 -12
  36. package/dist/internal/nodes/cryptoService.js +17 -97
  37. package/dist/internal/nodes/cryptoService.js.map +1 -1
  38. package/dist/internal/nodes/cryptoService.test.js +3 -1
  39. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  40. package/dist/internal/nodes/index.js +3 -1
  41. package/dist/internal/nodes/index.js.map +1 -1
  42. package/dist/internal/nodes/interface.d.ts +1 -1
  43. package/dist/internal/nodes/nodesAccess.d.ts +2 -2
  44. package/dist/internal/nodes/nodesAccess.js +52 -54
  45. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  46. package/dist/internal/shares/cryptoCache.d.ts +4 -3
  47. package/dist/internal/shares/cryptoCache.js +23 -6
  48. package/dist/internal/shares/cryptoCache.js.map +1 -1
  49. package/dist/internal/shares/cryptoCache.test.js +3 -2
  50. package/dist/internal/shares/cryptoCache.test.js.map +1 -1
  51. package/dist/internal/shares/index.js +1 -1
  52. package/dist/internal/shares/index.js.map +1 -1
  53. package/dist/internal/sharing/cache.d.ts +3 -0
  54. package/dist/internal/sharing/cache.js +17 -2
  55. package/dist/internal/sharing/cache.js.map +1 -1
  56. package/dist/internal/sharing/cryptoService.js +8 -6
  57. package/dist/internal/sharing/cryptoService.js.map +1 -1
  58. package/dist/internal/sharing/cryptoService.test.js +13 -0
  59. package/dist/internal/sharing/cryptoService.test.js.map +1 -1
  60. package/dist/internal/sharing/index.js +1 -1
  61. package/dist/internal/sharing/index.js.map +1 -1
  62. package/dist/internal/sharing/interface.d.ts +1 -1
  63. package/dist/internal/sharing/interface.js +1 -1
  64. package/dist/internal/sharing/sharingAccess.js +6 -0
  65. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  66. package/dist/internal/sharing/sharingAccess.test.js +242 -33
  67. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  68. package/dist/internal/sharing/sharingManagement.d.ts +3 -1
  69. package/dist/internal/sharing/sharingManagement.js +10 -1
  70. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  71. package/dist/internal/sharing/sharingManagement.test.js +32 -1
  72. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  73. package/dist/internal/sharingPublic/apiService.d.ts +19 -0
  74. package/dist/internal/sharingPublic/apiService.js +141 -0
  75. package/dist/internal/sharingPublic/apiService.js.map +1 -0
  76. package/dist/internal/sharingPublic/cryptoCache.d.ts +19 -0
  77. package/dist/internal/sharingPublic/cryptoCache.js +72 -0
  78. package/dist/internal/sharingPublic/cryptoCache.js.map +1 -0
  79. package/dist/internal/sharingPublic/cryptoService.d.ts +9 -0
  80. package/dist/internal/sharingPublic/cryptoService.js +57 -0
  81. package/dist/internal/sharingPublic/cryptoService.js.map +1 -0
  82. package/dist/internal/sharingPublic/index.d.ts +15 -0
  83. package/dist/internal/sharingPublic/index.js +27 -0
  84. package/dist/internal/sharingPublic/index.js.map +1 -0
  85. package/dist/internal/sharingPublic/interface.d.ts +6 -0
  86. package/dist/internal/sharingPublic/interface.js +3 -0
  87. package/dist/internal/sharingPublic/interface.js.map +1 -0
  88. package/dist/internal/sharingPublic/manager.d.ts +19 -0
  89. package/dist/internal/sharingPublic/manager.js +81 -0
  90. package/dist/internal/sharingPublic/manager.js.map +1 -0
  91. package/dist/internal/sharingPublic/session/apiService.d.ts +28 -0
  92. package/dist/internal/sharingPublic/session/apiService.js +55 -0
  93. package/dist/internal/sharingPublic/session/apiService.js.map +1 -0
  94. package/dist/internal/sharingPublic/session/httpClient.d.ts +16 -0
  95. package/dist/internal/sharingPublic/session/httpClient.js +41 -0
  96. package/dist/internal/sharingPublic/session/httpClient.js.map +1 -0
  97. package/dist/internal/sharingPublic/session/index.d.ts +1 -0
  98. package/dist/internal/sharingPublic/session/index.js +6 -0
  99. package/dist/internal/sharingPublic/session/index.js.map +1 -0
  100. package/dist/internal/sharingPublic/session/interface.d.ts +18 -0
  101. package/dist/internal/sharingPublic/session/interface.js +3 -0
  102. package/dist/internal/sharingPublic/session/interface.js.map +1 -0
  103. package/dist/internal/sharingPublic/session/manager.d.ts +49 -0
  104. package/dist/internal/sharingPublic/session/manager.js +75 -0
  105. package/dist/internal/sharingPublic/session/manager.js.map +1 -0
  106. package/dist/internal/sharingPublic/session/session.d.ts +34 -0
  107. package/dist/internal/sharingPublic/session/session.js +67 -0
  108. package/dist/internal/sharingPublic/session/session.js.map +1 -0
  109. package/dist/internal/sharingPublic/session/url.d.ts +12 -0
  110. package/dist/internal/sharingPublic/session/url.js +23 -0
  111. package/dist/internal/sharingPublic/session/url.js.map +1 -0
  112. package/dist/internal/sharingPublic/session/url.test.d.ts +1 -0
  113. package/dist/internal/sharingPublic/session/url.test.js +59 -0
  114. package/dist/internal/sharingPublic/session/url.test.js.map +1 -0
  115. package/dist/internal/upload/streamUploader.js +1 -1
  116. package/dist/internal/upload/streamUploader.js.map +1 -1
  117. package/dist/internal/upload/streamUploader.test.js +3 -1
  118. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  119. package/dist/protonDriveClient.d.ts +18 -3
  120. package/dist/protonDriveClient.js +31 -8
  121. package/dist/protonDriveClient.js.map +1 -1
  122. package/dist/protonDrivePublicLinkClient.d.ts +57 -0
  123. package/dist/protonDrivePublicLinkClient.js +73 -0
  124. package/dist/protonDrivePublicLinkClient.js.map +1 -0
  125. package/package.json +1 -1
  126. package/src/crypto/driveCrypto.ts +1 -1
  127. package/src/crypto/interface.ts +12 -1
  128. package/src/crypto/openPGPCrypto.ts +5 -2
  129. package/src/diagnostic/httpClient.ts +4 -4
  130. package/src/interface/httpClient.ts +5 -5
  131. package/src/interface/index.ts +18 -6
  132. package/src/internal/apiService/apiService.ts +1 -1
  133. package/src/internal/apiService/errorCodes.ts +1 -0
  134. package/src/internal/apiService/errors.test.ts +2 -1
  135. package/src/internal/apiService/errors.ts +15 -4
  136. package/src/internal/download/cryptoService.ts +2 -2
  137. package/src/internal/download/fileDownloader.test.ts +3 -1
  138. package/src/internal/download/fileDownloader.ts +2 -2
  139. package/src/internal/events/index.ts +1 -1
  140. package/src/internal/nodes/cache.ts +3 -1
  141. package/src/internal/nodes/cryptoCache.test.ts +4 -7
  142. package/src/internal/nodes/cryptoCache.ts +6 -7
  143. package/src/internal/nodes/cryptoReporter.ts +145 -0
  144. package/src/internal/nodes/cryptoService.test.ts +3 -1
  145. package/src/internal/nodes/cryptoService.ts +44 -137
  146. package/src/internal/nodes/index.ts +3 -1
  147. package/src/internal/nodes/interface.ts +3 -1
  148. package/src/internal/nodes/nodesAccess.ts +59 -61
  149. package/src/internal/shares/cryptoCache.test.ts +3 -2
  150. package/src/internal/shares/cryptoCache.ts +26 -7
  151. package/src/internal/shares/index.ts +1 -1
  152. package/src/internal/sharing/cache.ts +19 -2
  153. package/src/internal/sharing/cryptoService.test.ts +22 -1
  154. package/src/internal/sharing/cryptoService.ts +8 -6
  155. package/src/internal/sharing/index.ts +1 -0
  156. package/src/internal/sharing/interface.ts +1 -1
  157. package/src/internal/sharing/sharingAccess.test.ts +282 -34
  158. package/src/internal/sharing/sharingAccess.ts +6 -0
  159. package/src/internal/sharing/sharingManagement.test.ts +33 -0
  160. package/src/internal/sharing/sharingManagement.ts +9 -0
  161. package/src/internal/sharingPublic/apiService.ts +173 -0
  162. package/src/internal/sharingPublic/cryptoCache.ts +79 -0
  163. package/src/internal/sharingPublic/cryptoService.ts +98 -0
  164. package/src/internal/sharingPublic/index.ts +41 -0
  165. package/src/internal/sharingPublic/interface.ts +14 -0
  166. package/src/internal/sharingPublic/manager.ts +86 -0
  167. package/src/internal/sharingPublic/session/apiService.ts +74 -0
  168. package/src/internal/sharingPublic/session/httpClient.ts +48 -0
  169. package/src/internal/sharingPublic/session/index.ts +1 -0
  170. package/src/internal/sharingPublic/session/interface.ts +20 -0
  171. package/src/internal/sharingPublic/session/manager.ts +97 -0
  172. package/src/internal/sharingPublic/session/session.ts +78 -0
  173. package/src/internal/sharingPublic/session/url.test.ts +72 -0
  174. package/src/internal/sharingPublic/session/url.ts +23 -0
  175. package/src/internal/upload/streamUploader.test.ts +3 -1
  176. package/src/internal/upload/streamUploader.ts +1 -1
  177. package/src/protonDriveClient.ts +48 -11
  178. package/src/protonDrivePublicLinkClient.ts +135 -0
@@ -7,27 +7,49 @@ import {
7
7
  Result,
8
8
  Author,
9
9
  AnonymousUser,
10
- ProtonDriveAccount,
11
10
  ProtonDriveTelemetry,
12
11
  Logger,
13
12
  MetricsDecryptionErrorField,
14
13
  MetricVerificationErrorField,
15
14
  Membership,
15
+ ProtonDriveAccount,
16
16
  } from '../../interface';
17
17
  import { ValidationError } from '../../errors';
18
- import { getErrorMessage, getVerificationMessage } from '../errors';
19
- import { splitNodeUid } from '../uids';
18
+ import { getErrorMessage } from '../errors';
20
19
  import {
21
20
  EncryptedNode,
22
21
  EncryptedNodeFolderCrypto,
23
22
  DecryptedUnparsedNode,
24
23
  DecryptedNode,
25
24
  DecryptedNodeKeys,
26
- SharesService,
27
25
  EncryptedRevision,
28
26
  DecryptedUnparsedRevision,
29
27
  } from './interface';
30
28
 
29
+ export interface NodesCryptoReporter {
30
+ handleClaimedAuthor(
31
+ node: NodesCryptoReporterNode,
32
+ field: MetricVerificationErrorField,
33
+ signatureType: string,
34
+ verified: VERIFICATION_STATUS,
35
+ verificationErrors?: Error[],
36
+ claimedAuthor?: string,
37
+ notAvailableVerificationKeys?: boolean,
38
+ ): Promise<Author>;
39
+ reportDecryptionError(node: NodesCryptoReporterNode, field: MetricsDecryptionErrorField, error: unknown): void;
40
+ reportVerificationError(
41
+ node: NodesCryptoReporterNode,
42
+ field: MetricVerificationErrorField,
43
+ verificationErrors?: Error[],
44
+ claimedAuthor?: string,
45
+ ): void;
46
+ }
47
+
48
+ type NodesCryptoReporterNode = {
49
+ uid: string;
50
+ creationTime: Date;
51
+ };
52
+
31
53
  /**
32
54
  * Provides crypto operations for nodes metadata.
33
55
  *
@@ -41,20 +63,16 @@ import {
41
63
  export class NodesCryptoService {
42
64
  private logger: Logger;
43
65
 
44
- private reportedDecryptionErrors = new Set<string>();
45
- private reportedVerificationErrors = new Set<string>();
46
-
47
66
  constructor(
48
- private telemetry: ProtonDriveTelemetry,
49
- private driveCrypto: DriveCrypto,
67
+ telemetry: ProtonDriveTelemetry,
68
+ protected driveCrypto: DriveCrypto,
50
69
  private account: ProtonDriveAccount,
51
- private shareService: SharesService,
70
+ private reporter: NodesCryptoReporter,
52
71
  ) {
53
- this.telemetry = telemetry;
54
72
  this.logger = telemetry.getLogger('nodes-crypto');
55
73
  this.driveCrypto = driveCrypto;
56
74
  this.account = account;
57
- this.shareService = shareService;
75
+ this.reporter = reporter;
58
76
  }
59
77
 
60
78
  async decryptNode(
@@ -107,7 +125,7 @@ export class NodesCryptoService {
107
125
  passphraseSessionKey = keyResult.passphraseSessionKey;
108
126
  keyAuthor = keyResult.author;
109
127
  } catch (error: unknown) {
110
- void this.reportDecryptionError(node, 'nodeKey', error);
128
+ void this.reporter.reportDecryptionError(node, 'nodeKey', error);
111
129
  const message = getErrorMessage(error);
112
130
  const errorMessage = c('Error').t`Failed to decrypt node key: ${message}`;
113
131
  const { name, author: nameAuthor } = await namePromise;
@@ -159,7 +177,7 @@ export class NodesCryptoService {
159
177
  hashKey = hashKeyResult.hashKey;
160
178
  hashKeyAuthor = hashKeyResult.author;
161
179
  } catch (error: unknown) {
162
- void this.reportDecryptionError(node, 'nodeHashKey', error);
180
+ void this.reporter.reportDecryptionError(node, 'nodeHashKey', error);
163
181
  errors.push(error);
164
182
  }
165
183
 
@@ -170,7 +188,7 @@ export class NodesCryptoService {
170
188
  };
171
189
  folderExtendedAttributesAuthor = extendedAttributesResult.author;
172
190
  } catch (error: unknown) {
173
- void this.reportDecryptionError(node, 'nodeExtendedAttributes', error);
191
+ void this.reporter.reportDecryptionError(node, 'nodeExtendedAttributes', error);
174
192
  errors.push(error);
175
193
  }
176
194
  }
@@ -194,7 +212,7 @@ export class NodesCryptoService {
194
212
  try {
195
213
  activeRevision = resultOk(await activeRevisionPromise);
196
214
  } catch (error: unknown) {
197
- void this.reportDecryptionError(node, 'nodeExtendedAttributes', error);
215
+ void this.reporter.reportDecryptionError(node, 'nodeExtendedAttributes', error);
198
216
  const message = getErrorMessage(error);
199
217
  const errorMessage = c('Error').t`Failed to decrypt active revision: ${message}`;
200
218
  activeRevision = resultError(new Error(errorMessage));
@@ -205,7 +223,7 @@ export class NodesCryptoService {
205
223
  contentKeyPacketSessionKey = keySessionKeyResult.sessionKey;
206
224
  contentKeyPacketAuthor =
207
225
  keySessionKeyResult.verified !== undefined &&
208
- (await this.handleClaimedAuthor(
226
+ (await this.reporter.handleClaimedAuthor(
209
227
  node,
210
228
  'nodeContentKey',
211
229
  c('Property').t`content key`,
@@ -214,7 +232,7 @@ export class NodesCryptoService {
214
232
  node.encryptedCrypto.signatureEmail,
215
233
  ));
216
234
  } catch (error: unknown) {
217
- void this.reportDecryptionError(node, 'nodeContentKey', error);
235
+ void this.reporter.reportDecryptionError(node, 'nodeContentKey', error);
218
236
  const message = getErrorMessage(error);
219
237
  const errorMessage = c('Error').t`Failed to decrypt content key: ${message}`;
220
238
  contentKeyPacketAuthor = resultError({
@@ -295,7 +313,7 @@ export class NodesCryptoService {
295
313
  passphrase: key.passphrase,
296
314
  key: key.key,
297
315
  passphraseSessionKey: key.passphraseSessionKey,
298
- author: await this.handleClaimedAuthor(
316
+ author: await this.reporter.handleClaimedAuthor(
299
317
  node,
300
318
  'nodeKey',
301
319
  c('Property').t`key`,
@@ -326,7 +344,7 @@ export class NodesCryptoService {
326
344
 
327
345
  return {
328
346
  name: resultOk(name),
329
- author: await this.handleClaimedAuthor(
347
+ author: await this.reporter.handleClaimedAuthor(
330
348
  node,
331
349
  'nodeName',
332
350
  c('Property').t`name`,
@@ -337,7 +355,7 @@ export class NodesCryptoService {
337
355
  ),
338
356
  };
339
357
  } catch (error: unknown) {
340
- void this.reportDecryptionError(node, 'nodeName', error);
358
+ void this.reporter.reportDecryptionError(node, 'nodeName', error);
341
359
  const errorMessage = getErrorMessage(error);
342
360
  return {
343
361
  name: resultError(new Error(errorMessage)),
@@ -378,7 +396,7 @@ export class NodesCryptoService {
378
396
  inviterEmailKeys || [],
379
397
  );
380
398
 
381
- sharedBy = await this.handleClaimedAuthor(
399
+ sharedBy = await this.reporter.handleClaimedAuthor(
382
400
  node,
383
401
  'membershipInviter',
384
402
  c('Property').t`membership`,
@@ -387,7 +405,7 @@ export class NodesCryptoService {
387
405
  node.encryptedCrypto.membership.inviterEmail,
388
406
  );
389
407
  } catch (error: unknown) {
390
- void this.reportVerificationError(node, 'membershipInviter');
408
+ void this.reporter.reportVerificationError(node, 'membershipInviter');
391
409
  this.logger.error('Failed to verify invitation', error);
392
410
  sharedBy = resultError({
393
411
  claimedAuthor: node.encryptedCrypto.membership.inviterEmail,
@@ -428,7 +446,7 @@ export class NodesCryptoService {
428
446
 
429
447
  return {
430
448
  hashKey,
431
- author: await this.handleClaimedAuthor(
449
+ author: await this.reporter.handleClaimedAuthor(
432
450
  node,
433
451
  'nodeHashKey',
434
452
  c('Property').t`hash key`,
@@ -491,7 +509,7 @@ export class NodesCryptoService {
491
509
 
492
510
  return {
493
511
  extendedAttributes,
494
- author: await this.handleClaimedAuthor(
512
+ author: await this.reporter.handleClaimedAuthor(
495
513
  node,
496
514
  'nodeExtendedAttributes',
497
515
  c('Property').t`attributes`,
@@ -509,6 +527,7 @@ export class NodesCryptoService {
509
527
  extendedAttributes?: string,
510
528
  ): Promise<{
511
529
  encryptedCrypto: EncryptedNodeFolderCrypto & {
530
+ armoredNodePassphraseSignature: string;
512
531
  // signatureEmail and nameSignatureEmail are not optional.
513
532
  signatureEmail: string;
514
533
  nameSignatureEmail: string;
@@ -626,118 +645,6 @@ export class NodesCryptoService {
626
645
  nameSignatureEmail: email,
627
646
  };
628
647
  }
629
-
630
- private async handleClaimedAuthor(
631
- node: { uid: string; creationTime: Date },
632
- field: MetricVerificationErrorField,
633
- signatureType: string,
634
- verified: VERIFICATION_STATUS,
635
- verificationErrors?: Error[],
636
- claimedAuthor?: string,
637
- notAvailableVerificationKeys = false,
638
- ): Promise<Author> {
639
- const author = handleClaimedAuthor(
640
- signatureType,
641
- verified,
642
- verificationErrors,
643
- claimedAuthor,
644
- notAvailableVerificationKeys,
645
- );
646
- if (!author.ok) {
647
- void this.reportVerificationError(node, field, verificationErrors, claimedAuthor);
648
- }
649
- return author;
650
- }
651
-
652
- private async reportVerificationError(
653
- node: { uid: string; creationTime: Date },
654
- field: MetricVerificationErrorField,
655
- verificationErrors?: Error[],
656
- claimedAuthor?: string,
657
- ) {
658
- if (this.reportedVerificationErrors.has(node.uid)) {
659
- return;
660
- }
661
- this.reportedVerificationErrors.add(node.uid);
662
-
663
- const fromBefore2024 = node.creationTime < new Date('2024-01-01');
664
-
665
- let addressMatchingDefaultShare, volumeType;
666
- try {
667
- const { volumeId } = splitNodeUid(node.uid);
668
- const { email } = await this.shareService.getMyFilesShareMemberEmailKey();
669
- addressMatchingDefaultShare = claimedAuthor ? claimedAuthor === email : undefined;
670
- volumeType = await this.shareService.getVolumeMetricContext(volumeId);
671
- } catch (error: unknown) {
672
- this.logger.error('Failed to check if claimed author matches default share', error);
673
- }
674
-
675
- this.logger.warn(
676
- `Failed to verify ${field} for node ${node.uid} (from before 2024: ${fromBefore2024}, matching address: ${addressMatchingDefaultShare})`,
677
- );
678
-
679
- this.telemetry.recordMetric({
680
- eventName: 'verificationError',
681
- volumeType,
682
- field,
683
- addressMatchingDefaultShare,
684
- fromBefore2024,
685
- error: verificationErrors?.map((e) => e.message).join(', '),
686
- uid: node.uid,
687
- });
688
- }
689
-
690
- private async reportDecryptionError(node: EncryptedNode, field: MetricsDecryptionErrorField, error: unknown) {
691
- if (this.reportedDecryptionErrors.has(node.uid)) {
692
- return;
693
- }
694
-
695
- const fromBefore2024 = node.creationTime < new Date('2024-01-01');
696
-
697
- let volumeType;
698
- try {
699
- const { volumeId } = splitNodeUid(node.uid);
700
- volumeType = await this.shareService.getVolumeMetricContext(volumeId);
701
- } catch (error: unknown) {
702
- this.logger.error('Failed to get metric context', error);
703
- }
704
-
705
- this.logger.error(`Failed to decrypt node ${node.uid} (from before 2024: ${fromBefore2024})`, error);
706
-
707
- this.telemetry.recordMetric({
708
- eventName: 'decryptionError',
709
- volumeType,
710
- field,
711
- fromBefore2024,
712
- error,
713
- uid: node.uid,
714
- });
715
- this.reportedDecryptionErrors.add(node.uid);
716
- }
717
- }
718
-
719
- /**
720
- * @param signatureType - Must be translated before calling this function.
721
- */
722
- function handleClaimedAuthor(
723
- signatureType: string,
724
- verified: VERIFICATION_STATUS,
725
- verificationErrors?: Error[],
726
- claimedAuthor?: string,
727
- notAvailableVerificationKeys = false,
728
- ): Author {
729
- if (!claimedAuthor && notAvailableVerificationKeys) {
730
- return resultOk(null as AnonymousUser);
731
- }
732
-
733
- if (verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
734
- return resultOk(claimedAuthor || (null as AnonymousUser));
735
- }
736
-
737
- return resultError({
738
- claimedAuthor: claimedAuthor,
739
- error: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
740
- });
741
648
  }
742
649
 
743
650
  function getClaimedAuthor(
@@ -10,6 +10,7 @@ import { NodeAPIService } from './apiService';
10
10
  import { NodesCache } from './cache';
11
11
  import { NodesCryptoCache } from './cryptoCache';
12
12
  import { NodesCryptoService } from './cryptoService';
13
+ import { NodesCryptoReporter } from './cryptoReporter';
13
14
  import { SharesService } from './interface';
14
15
  import { NodesAccess } from './nodesAccess';
15
16
  import { NodesManagement } from './nodesManagement';
@@ -40,7 +41,8 @@ export function initNodesModule(
40
41
  const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService);
41
42
  const cache = new NodesCache(telemetry.getLogger('nodes-cache'), driveEntitiesCache);
42
43
  const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
43
- const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, sharesService);
44
+ const cryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
45
+ const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
44
46
  const nodesAccess = new NodesAccess(
45
47
  telemetry.getLogger('nodes'),
46
48
  api,
@@ -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
@@ -54,7 +56,7 @@ export interface EncryptedNodeCrypto {
54
56
  nameSignatureEmail?: string;
55
57
  armoredKey: string;
56
58
  armoredNodePassphrase: string;
57
- armoredNodePassphraseSignature: string;
59
+ armoredNodePassphraseSignature?: string;
58
60
  membership?: {
59
61
  inviterEmail: string;
60
62
  base64MemberSharePassphraseKeyPacket: string;
@@ -289,7 +289,7 @@ export class NodesAccess {
289
289
  }
290
290
 
291
291
  const { node: unparsedNode, keys } = await this.cryptoService.decryptNode(encryptedNode, parentKey);
292
- const node = await this.parseNode(unparsedNode);
292
+ const node = await parseNode(this.logger, unparsedNode);
293
293
  try {
294
294
  await this.cache.setNode(node);
295
295
  } catch (error: unknown) {
@@ -305,65 +305,6 @@ export class NodesAccess {
305
305
  return { node, keys };
306
306
  }
307
307
 
308
- private async parseNode(unparsedNode: DecryptedUnparsedNode): Promise<DecryptedNode> {
309
- let nodeName: Result<string, Error | InvalidNameError> = unparsedNode.name;
310
- if (unparsedNode.name.ok) {
311
- try {
312
- validateNodeName(unparsedNode.name.value);
313
- } catch (error: unknown) {
314
- this.logger.warn(`Node name validation failed: ${error instanceof Error ? error.message : error}`);
315
- nodeName = resultError({
316
- name: unparsedNode.name.value,
317
- error: error instanceof Error ? error.message : c('Error').t`Unknown error`,
318
- });
319
- }
320
- }
321
-
322
- if (unparsedNode.type === NodeType.File) {
323
- const extendedAttributes = unparsedNode.activeRevision?.ok
324
- ? parseFileExtendedAttributes(
325
- this.logger,
326
- unparsedNode.activeRevision.value.creationTime,
327
- unparsedNode.activeRevision.value.extendedAttributes,
328
- )
329
- : undefined;
330
-
331
- return {
332
- ...unparsedNode,
333
- isStale: false,
334
- activeRevision: !unparsedNode.activeRevision?.ok
335
- ? unparsedNode.activeRevision
336
- : resultOk({
337
- uid: unparsedNode.activeRevision.value.uid,
338
- state: unparsedNode.activeRevision.value.state,
339
- creationTime: unparsedNode.activeRevision.value.creationTime,
340
- storageSize: unparsedNode.activeRevision.value.storageSize,
341
- contentAuthor: unparsedNode.activeRevision.value.contentAuthor,
342
- thumbnails: unparsedNode.activeRevision.value.thumbnails,
343
- ...extendedAttributes,
344
- }),
345
- folder: undefined,
346
- treeEventScopeId: splitNodeUid(unparsedNode.uid).volumeId,
347
- };
348
- }
349
-
350
- const extendedAttributes = unparsedNode.folder?.extendedAttributes
351
- ? parseFolderExtendedAttributes(this.logger, unparsedNode.folder.extendedAttributes)
352
- : undefined;
353
- return {
354
- ...unparsedNode,
355
- name: nodeName,
356
- isStale: false,
357
- activeRevision: undefined,
358
- folder: extendedAttributes
359
- ? {
360
- ...extendedAttributes,
361
- }
362
- : undefined,
363
- treeEventScopeId: splitNodeUid(unparsedNode.uid).volumeId,
364
- };
365
- }
366
-
367
308
  async getParentKeys(
368
309
  node: Pick<DecryptedNode, 'parentUid' | 'shareId'>,
369
310
  ): Promise<Pick<DecryptedNodeKeys, 'key' | 'hashKey'>> {
@@ -375,7 +316,7 @@ export class NodesAccess {
375
316
  // Change the error message to be more specific.
376
317
  // Original error message is referring to node, while here
377
318
  // it referes to as parent to follow the method context.
378
- throw new DecryptionError(c('Error').t`Parent cannot be decrypted`);
319
+ throw new DecryptionError(c('Error').t`Parent cannot be decrypted`, { cause: error });
379
320
  }
380
321
  throw error;
381
322
  }
@@ -458,3 +399,60 @@ export class NodesAccess {
458
399
  return node.parentUid ? this.getRootNode(node.parentUid) : node;
459
400
  }
460
401
  }
402
+
403
+ export async function parseNode(logger: Logger, unparsedNode: DecryptedUnparsedNode): Promise<DecryptedNode> {
404
+ let nodeName: Result<string, Error | InvalidNameError> = unparsedNode.name;
405
+ if (unparsedNode.name.ok) {
406
+ try {
407
+ validateNodeName(unparsedNode.name.value);
408
+ } catch (error: unknown) {
409
+ logger.warn(`Node name validation failed: ${error instanceof Error ? error.message : error}`);
410
+ nodeName = resultError({
411
+ name: unparsedNode.name.value,
412
+ error: error instanceof Error ? error.message : c('Error').t`Unknown error`,
413
+ });
414
+ }
415
+ }
416
+
417
+ const treeEventScopeId = splitNodeUid(unparsedNode.uid).volumeId;
418
+
419
+ if (unparsedNode.type === NodeType.File) {
420
+ const extendedAttributes = unparsedNode.activeRevision?.ok
421
+ ? parseFileExtendedAttributes(
422
+ logger,
423
+ unparsedNode.activeRevision.value.creationTime,
424
+ unparsedNode.activeRevision.value.extendedAttributes,
425
+ )
426
+ : undefined;
427
+
428
+ return {
429
+ ...unparsedNode,
430
+ isStale: false,
431
+ activeRevision: !unparsedNode.activeRevision?.ok
432
+ ? unparsedNode.activeRevision
433
+ : resultOk({
434
+ uid: unparsedNode.activeRevision.value.uid,
435
+ state: unparsedNode.activeRevision.value.state,
436
+ creationTime: unparsedNode.activeRevision.value.creationTime,
437
+ storageSize: unparsedNode.activeRevision.value.storageSize,
438
+ contentAuthor: unparsedNode.activeRevision.value.contentAuthor,
439
+ thumbnails: unparsedNode.activeRevision.value.thumbnails,
440
+ ...extendedAttributes,
441
+ }),
442
+ folder: undefined,
443
+ treeEventScopeId,
444
+ };
445
+ }
446
+
447
+ const extendedAttributes = unparsedNode.folder?.extendedAttributes
448
+ ? parseFolderExtendedAttributes(logger, unparsedNode.folder.extendedAttributes)
449
+ : undefined;
450
+ return {
451
+ ...unparsedNode,
452
+ name: nodeName,
453
+ isStale: false,
454
+ activeRevision: undefined,
455
+ folder: extendedAttributes,
456
+ treeEventScopeId,
457
+ };
458
+ }
@@ -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'),
@@ -44,11 +44,28 @@ export class SharingCache {
44
44
  }
45
45
 
46
46
  async getSharedWithMeNodeUids(): Promise<string[]> {
47
- return this.getNodeUids(SharingType.sharedWithMe);
47
+ return this.getNodeUids(SharingType.SharedWithMe);
48
+ }
49
+
50
+ async hasSharedWithMeNodeUidsLoaded(): Promise<boolean> {
51
+ try {
52
+ await this.getNodeUids(SharingType.SharedWithMe);
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ async addSharedWithMeNodeUid(nodeUid: string): Promise<void> {
60
+ return this.addNodeUid(SharingType.SharedWithMe, nodeUid);
61
+ }
62
+
63
+ async removeSharedWithMeNodeUid(nodeUid: string): Promise<void> {
64
+ return this.removeNodeUid(SharingType.SharedWithMe, nodeUid);
48
65
  }
49
66
 
50
67
  async setSharedWithMeNodeUids(nodeUids: string[] | undefined): Promise<void> {
51
- return this.setNodeUids(SharingType.sharedWithMe, nodeUids);
68
+ return this.setNodeUids(SharingType.SharedWithMe, nodeUids);
52
69
  }
53
70
 
54
71
  /**
@@ -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);