@protontech/drive-sdk 0.3.1 → 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 (86) 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 +1 -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/internal/download/cryptoService.js +2 -2
  8. package/dist/internal/download/cryptoService.js.map +1 -1
  9. package/dist/internal/download/fileDownloader.js +2 -2
  10. package/dist/internal/download/fileDownloader.js.map +1 -1
  11. package/dist/internal/download/fileDownloader.test.js +3 -1
  12. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  13. package/dist/internal/nodes/cache.js +3 -1
  14. package/dist/internal/nodes/cache.js.map +1 -1
  15. package/dist/internal/nodes/cryptoReporter.d.ts +20 -0
  16. package/dist/internal/nodes/cryptoReporter.js +96 -0
  17. package/dist/internal/nodes/cryptoReporter.js.map +1 -0
  18. package/dist/internal/nodes/cryptoService.d.ts +17 -12
  19. package/dist/internal/nodes/cryptoService.js +17 -97
  20. package/dist/internal/nodes/cryptoService.js.map +1 -1
  21. package/dist/internal/nodes/cryptoService.test.js +3 -1
  22. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  23. package/dist/internal/nodes/index.js +3 -1
  24. package/dist/internal/nodes/index.js.map +1 -1
  25. package/dist/internal/nodes/interface.d.ts +1 -1
  26. package/dist/internal/nodes/nodesAccess.d.ts +2 -2
  27. package/dist/internal/nodes/nodesAccess.js +52 -54
  28. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  29. package/dist/internal/sharing/cache.d.ts +3 -0
  30. package/dist/internal/sharing/cache.js +17 -2
  31. package/dist/internal/sharing/cache.js.map +1 -1
  32. package/dist/internal/sharing/interface.d.ts +1 -1
  33. package/dist/internal/sharing/interface.js +1 -1
  34. package/dist/internal/sharing/sharingAccess.js +6 -0
  35. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  36. package/dist/internal/sharing/sharingAccess.test.js +242 -33
  37. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  38. package/dist/internal/sharingPublic/apiService.d.ts +1 -1
  39. package/dist/internal/sharingPublic/apiService.js +9 -2
  40. package/dist/internal/sharingPublic/apiService.js.map +1 -1
  41. package/dist/internal/sharingPublic/cryptoService.d.ts +6 -20
  42. package/dist/internal/sharingPublic/cryptoService.js +40 -103
  43. package/dist/internal/sharingPublic/cryptoService.js.map +1 -1
  44. package/dist/internal/sharingPublic/index.d.ts +2 -2
  45. package/dist/internal/sharingPublic/index.js +2 -2
  46. package/dist/internal/sharingPublic/index.js.map +1 -1
  47. package/dist/internal/sharingPublic/interface.d.ts +1 -43
  48. package/dist/internal/sharingPublic/manager.d.ts +1 -1
  49. package/dist/internal/sharingPublic/manager.js +9 -7
  50. package/dist/internal/sharingPublic/manager.js.map +1 -1
  51. package/dist/internal/upload/streamUploader.js +1 -1
  52. package/dist/internal/upload/streamUploader.js.map +1 -1
  53. package/dist/internal/upload/streamUploader.test.js +3 -1
  54. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  55. package/dist/protonDriveClient.js +1 -0
  56. package/dist/protonDriveClient.js.map +1 -1
  57. package/dist/protonDrivePublicLinkClient.d.ts +13 -4
  58. package/dist/protonDrivePublicLinkClient.js +13 -11
  59. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/crypto/driveCrypto.ts +1 -1
  62. package/src/crypto/interface.ts +1 -1
  63. package/src/crypto/openPGPCrypto.ts +5 -2
  64. package/src/internal/download/cryptoService.ts +2 -2
  65. package/src/internal/download/fileDownloader.test.ts +3 -1
  66. package/src/internal/download/fileDownloader.ts +2 -2
  67. package/src/internal/nodes/cache.ts +3 -1
  68. package/src/internal/nodes/cryptoReporter.ts +145 -0
  69. package/src/internal/nodes/cryptoService.test.ts +3 -1
  70. package/src/internal/nodes/cryptoService.ts +44 -137
  71. package/src/internal/nodes/index.ts +3 -1
  72. package/src/internal/nodes/interface.ts +1 -1
  73. package/src/internal/nodes/nodesAccess.ts +59 -61
  74. package/src/internal/sharing/cache.ts +19 -2
  75. package/src/internal/sharing/interface.ts +1 -1
  76. package/src/internal/sharing/sharingAccess.test.ts +282 -34
  77. package/src/internal/sharing/sharingAccess.ts +6 -0
  78. package/src/internal/sharingPublic/apiService.ts +11 -2
  79. package/src/internal/sharingPublic/cryptoService.ts +71 -135
  80. package/src/internal/sharingPublic/index.ts +3 -2
  81. package/src/internal/sharingPublic/interface.ts +8 -53
  82. package/src/internal/sharingPublic/manager.ts +9 -8
  83. package/src/internal/upload/streamUploader.test.ts +3 -1
  84. package/src/internal/upload/streamUploader.ts +1 -1
  85. package/src/protonDriveClient.ts +1 -0
  86. package/src/protonDrivePublicLinkClient.ts +26 -12
@@ -26,7 +26,7 @@ class ProtonDrivePublicLinkClient {
26
26
  sdkEvents;
27
27
  sharingPublic;
28
28
  experimental;
29
- constructor({ httpClient, cryptoCache, openPGPCryptoModule, srpModule, config, telemetry, token, password, }) {
29
+ constructor({ httpClient, cryptoCache, account, openPGPCryptoModule, srpModule, config, telemetry, token, password, }) {
30
30
  if (!telemetry) {
31
31
  telemetry = new telemetry_1.Telemetry();
32
32
  }
@@ -35,7 +35,7 @@ class ProtonDrivePublicLinkClient {
35
35
  this.sdkEvents = new sdkEvents_1.SDKEvents(telemetry);
36
36
  const apiService = new apiService_1.DriveAPIService(telemetry, this.sdkEvents, httpClient, fullConfig.baseUrl, fullConfig.language);
37
37
  const driveCrypto = new crypto_1.DriveCrypto(openPGPCryptoModule, srpModule);
38
- this.sharingPublic = (0, sharingPublic_1.initSharingPublicModule)(telemetry, apiService, cryptoCache, driveCrypto, token, password);
38
+ this.sharingPublic = (0, sharingPublic_1.initSharingPublicModule)(telemetry, apiService, cryptoCache, driveCrypto, account, token, password);
39
39
  this.experimental = {
40
40
  getNodeUrl: async (nodeUid) => {
41
41
  this.logger.debug(`Getting node URL for ${(0, transformers_1.getUid)(nodeUid)}`);
@@ -52,19 +52,21 @@ class ProtonDrivePublicLinkClient {
52
52
  },
53
53
  };
54
54
  }
55
- // TODO: comment
56
- // TODO: add public node interface
55
+ /**
56
+ * @returns The root folder to the public link.
57
+ */
57
58
  async getRootNode() {
58
59
  this.logger.info(`Getting root node`);
59
- // TODO: conversion to public node
60
- return this.sharingPublic.getRootNode();
60
+ return (0, transformers_1.convertInternalNodePromise)(this.sharingPublic.getRootNode());
61
61
  }
62
- // TODO: comment
63
- // TODO: add public node interface
64
- async *iterateChildren(parentUid) {
62
+ /**
63
+ * Iterates the children of the given parent node.
64
+ *
65
+ * See `ProtonDriveClient.iterateFolderChildren` for more information.
66
+ */
67
+ async *iterateFolderChildren(parentUid, signal) {
65
68
  this.logger.info(`Iterating children of ${(0, transformers_1.getUid)(parentUid)}`);
66
- // TODO: conversion to public node
67
- yield* this.sharingPublic.iterateChildren((0, transformers_1.getUid)(parentUid));
69
+ yield* (0, transformers_1.convertInternalNodeIterator)(this.sharingPublic.iterateFolderChildren((0, transformers_1.getUid)(parentUid), signal));
68
70
  }
69
71
  }
70
72
  exports.ProtonDrivePublicLinkClient = ProtonDrivePublicLinkClient;
@@ -1 +1 @@
1
- {"version":3,"file":"protonDrivePublicLinkClient.js","sourceRoot":"","sources":["../src/protonDrivePublicLinkClient.ts"],"names":[],"mappings":";;;AAAA,qCAAqC;AACrC,qCAA6E;AAS7E,2CAAwC;AACxC,iDAAwC;AACxC,sDAAwD;AACxD,oDAAiD;AACjD,4DAAmE;AAEnE;;;;;;;;;;;;GAYG;AACH,MAAa,2BAA2B;IAC5B,MAAM,CAAS;IACf,SAAS,CAAY;IACrB,aAAa,CAA6C;IAE3D,YAAY,CAejB;IAEF,YAAY,EACR,UAAU,EACV,WAAW,EACX,mBAAmB,EACnB,SAAS,EACT,MAAM,EACN,SAAS,EACT,KAAK,EACL,QAAQ,GAUX;QACG,IAAI,CAAC,SAAS,EAAE,CAAC;YACb,SAAS,GAAG,IAAI,qBAAS,EAAE,CAAC;QAChC,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAE/C,MAAM,UAAU,GAAG,IAAA,kBAAS,EAAC,MAAM,CAAC,CAAC;QACrC,IAAI,CAAC,SAAS,GAAG,IAAI,qBAAS,CAAC,SAAS,CAAC,CAAC;QAE1C,MAAM,UAAU,GAAG,IAAI,4BAAe,CAClC,SAAS,EACT,IAAI,CAAC,SAAS,EACd,UAAU,EACV,UAAU,CAAC,OAAO,EAClB,UAAU,CAAC,QAAQ,CACtB,CAAC;QACF,MAAM,WAAW,GAAG,IAAI,oBAAW,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;QACpE,IAAI,CAAC,aAAa,GAAG,IAAA,uCAAuB,EAAC,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;QAE/G,IAAI,CAAC,YAAY,GAAG;YAChB,UAAU,EAAE,KAAK,EAAE,OAAkB,EAAE,EAAE;gBACrC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wBAAwB,IAAA,qBAAM,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBAC7D,sCAAsC;gBACtC,OAAO,EAAE,CAAC;YACd,CAAC;YACD,UAAU,EAAE,KAAK,EAAE,OAAkB,EAAE,EAAE;gBACrC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,IAAA,qBAAM,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBAC9D,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,IAAA,qBAAM,EAAC,OAAO,CAAC,CAAC,CAAC;gBACnE,IAAI,CAAC,IAAI,CAAC,0BAA0B,EAAE,CAAC;oBACnC,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;gBAC3E,CAAC;gBACD,OAAO,IAAI,CAAC,0BAA0B,CAAC;YAC3C,CAAC;SACJ,CAAC;IACN,CAAC;IAED,gBAAgB;IAChB,kCAAkC;IAClC,KAAK,CAAC,WAAW;QACb,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QACtC,kCAAkC;QAClC,OAAO,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,CAAC;IAC5C,CAAC;IAED,gBAAgB;IAChB,kCAAkC;IAClC,KAAK,CAAC,CAAC,eAAe,CAAC,SAAoB;QACvC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,IAAA,qBAAM,EAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAC/D,kCAAkC;QAClC,KAAM,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,IAAA,qBAAM,EAAC,SAAS,CAAC,CAAC,CAAC;IAClE,CAAC;CACJ;AA3FD,kEA2FC"}
1
+ {"version":3,"file":"protonDrivePublicLinkClient.js","sourceRoot":"","sources":["../src/protonDrivePublicLinkClient.ts"],"names":[],"mappings":";;;AAAA,qCAAqC;AACrC,qCAA6E;AAW7E,2CAAwC;AACxC,iDAAiG;AACjG,sDAAwD;AACxD,oDAAiD;AACjD,4DAAmE;AAEnE;;;;;;;;;;;;GAYG;AACH,MAAa,2BAA2B;IAC5B,MAAM,CAAS;IACf,SAAS,CAAY;IACrB,aAAa,CAA6C;IAE3D,YAAY,CAejB;IAEF,YAAY,EACR,UAAU,EACV,WAAW,EACX,OAAO,EACP,mBAAmB,EACnB,SAAS,EACT,MAAM,EACN,SAAS,EACT,KAAK,EACL,QAAQ,GAWX;QACG,IAAI,CAAC,SAAS,EAAE,CAAC;YACb,SAAS,GAAG,IAAI,qBAAS,EAAE,CAAC;QAChC,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAE/C,MAAM,UAAU,GAAG,IAAA,kBAAS,EAAC,MAAM,CAAC,CAAC;QACrC,IAAI,CAAC,SAAS,GAAG,IAAI,qBAAS,CAAC,SAAS,CAAC,CAAC;QAE1C,MAAM,UAAU,GAAG,IAAI,4BAAe,CAClC,SAAS,EACT,IAAI,CAAC,SAAS,EACd,UAAU,EACV,UAAU,CAAC,OAAO,EAClB,UAAU,CAAC,QAAQ,CACtB,CAAC;QACF,MAAM,WAAW,GAAG,IAAI,oBAAW,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;QACpE,IAAI,CAAC,aAAa,GAAG,IAAA,uCAAuB,EACxC,SAAS,EACT,UAAU,EACV,WAAW,EACX,WAAW,EACX,OAAO,EACP,KAAK,EACL,QAAQ,CACX,CAAC;QAEF,IAAI,CAAC,YAAY,GAAG;YAChB,UAAU,EAAE,KAAK,EAAE,OAAkB,EAAE,EAAE;gBACrC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wBAAwB,IAAA,qBAAM,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBAC7D,sCAAsC;gBACtC,OAAO,EAAE,CAAC;YACd,CAAC;YACD,UAAU,EAAE,KAAK,EAAE,OAAkB,EAAE,EAAE;gBACrC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,IAAA,qBAAM,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBAC9D,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,IAAA,qBAAM,EAAC,OAAO,CAAC,CAAC,CAAC;gBACnE,IAAI,CAAC,IAAI,CAAC,0BAA0B,EAAE,CAAC;oBACnC,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;gBAC3E,CAAC;gBACD,OAAO,IAAI,CAAC,0BAA0B,CAAC;YAC3C,CAAC;SACJ,CAAC;IACN,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,WAAW;QACb,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QACtC,OAAO,IAAA,yCAA0B,EAAC,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,CAAC,CAAC;IACxE,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,CAAC,qBAAqB,CAAC,SAAoB,EAAE,MAAoB;QACnE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,IAAA,qBAAM,EAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAC/D,KAAM,CAAC,CAAC,IAAA,0CAA2B,EAAC,IAAI,CAAC,aAAa,CAAC,qBAAqB,CAAC,IAAA,qBAAM,EAAC,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7G,CAAC;CACJ;AAvGD,kEAuGC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protontech/drive-sdk",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Proton Drive SDK",
5
5
  "license": "GPL-3.0",
6
6
  "main": "dist/index.js",
@@ -170,7 +170,7 @@ export class DriveCrypto {
170
170
  async decryptKey(
171
171
  armoredKey: string,
172
172
  armoredPassphrase: string,
173
- armoredPassphraseSignature: string,
173
+ armoredPassphraseSignature: string | undefined,
174
174
  decryptionKeys: PrivateKey[],
175
175
  verificationKeys: PublicKey[],
176
176
  ): Promise<{
@@ -240,7 +240,7 @@ export interface OpenPGPCrypto {
240
240
 
241
241
  decryptArmoredAndVerifyDetached: (
242
242
  armoredData: string,
243
- armoredSignature: string,
243
+ armoredSignature: string | undefined,
244
244
  sessionKey: SessionKey,
245
245
  verificationKeys: PublicKey | PublicKey[],
246
246
  ) => Promise<{
@@ -1,3 +1,4 @@
1
+ import { c } from 'ttag';
1
2
  import { OpenPGPCrypto, PrivateKey, PublicKey, SessionKey, VERIFICATION_STATUS } from './interface';
2
3
  import { uint8ArrayToBase64String } from './utils';
3
4
 
@@ -393,7 +394,7 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
393
394
 
394
395
  async decryptArmoredAndVerifyDetached(
395
396
  armoredData: string,
396
- armoredSignature: string,
397
+ armoredSignature: string | undefined,
397
398
  sessionKey: SessionKey,
398
399
  verificationKeys: PublicKey | PublicKey[],
399
400
  ) {
@@ -410,7 +411,9 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
410
411
  // pmcrypto 8.3.0 changes `verified` to `verificationStatus`.
411
412
  // Proper typing is too complex, it will be removed to support only newer pmcrypto.
412
413
  verified: verified || verificationStatus!,
413
- verificationErrors,
414
+ verificationErrors: !armoredSignature
415
+ ? [new Error(c('Error').t`Signature is missing`)]
416
+ : verificationErrors,
414
417
  };
415
418
  }
416
419
 
@@ -49,7 +49,7 @@ export class DownloadCryptoService {
49
49
  );
50
50
  } catch (error: unknown) {
51
51
  const message = getErrorMessage(error);
52
- throw new DecryptionError(c('Error').t`Failed to decrypt block: ${message}`);
52
+ throw new DecryptionError(c('Error').t`Failed to decrypt block: ${message}`, { cause: error });
53
53
  }
54
54
 
55
55
  return decryptedBlock;
@@ -66,7 +66,7 @@ export class DownloadCryptoService {
66
66
  decryptedBlock = result.decryptedThumbnail;
67
67
  } catch (error: unknown) {
68
68
  const message = getErrorMessage(error);
69
- throw new DecryptionError(c('Error').t`Failed to decrypt thumbnail: ${message}`);
69
+ throw new DecryptionError(c('Error').t`Failed to decrypt thumbnail: ${message}`, { cause: error });
70
70
  }
71
71
 
72
72
  return decryptedBlock;
@@ -123,8 +123,10 @@ describe('FileDownloader', () => {
123
123
 
124
124
  const verifyOnProgress = async (downloadedBytes: number[]) => {
125
125
  expect(onProgress).toHaveBeenCalledTimes(downloadedBytes.length);
126
+ let fileProgress = 0;
126
127
  for (let i = 0; i < downloadedBytes.length; i++) {
127
- expect(onProgress).toHaveBeenNthCalledWith(i + 1, downloadedBytes[i]);
128
+ fileProgress += downloadedBytes[i];
129
+ expect(onProgress).toHaveBeenNthCalledWith(i + 1, fileProgress);
128
130
  }
129
131
  };
130
132
 
@@ -134,7 +134,7 @@ export class FileDownloader {
134
134
  const blockData = await this.downloadBlockData(blockMetadata, true, cryptoKeys);
135
135
  return blockData.slice(blockOffset);
136
136
  } catch (error: unknown) {
137
- return error instanceof Error ? error : new Error(`Unknown error: ${error}`);
137
+ return error instanceof Error ? error : new Error(`Unknown error: ${error}`, { cause: error });
138
138
  }
139
139
  }
140
140
 
@@ -193,7 +193,7 @@ export class FileDownloader {
193
193
  cryptoKeys,
194
194
  (downloadedBytes) => {
195
195
  fileProgress += downloadedBytes;
196
- onProgress?.(downloadedBytes);
196
+ onProgress?.(fileProgress);
197
197
  },
198
198
  );
199
199
  this.ongoingDownloads.set(blockMetadata.index, { downloadPromise });
@@ -53,7 +53,9 @@ export class NodesCache {
53
53
  return deserialiseNode(nodeData);
54
54
  } catch (error: unknown) {
55
55
  await this.removeCorruptedNode({ nodeUid }, error);
56
- throw new Error(`Failed to deserialise node: ${error instanceof Error ? error.message : error}`);
56
+ throw new Error(`Failed to deserialise node: ${error instanceof Error ? error.message : error}`, {
57
+ cause: error,
58
+ });
57
59
  }
58
60
  }
59
61
 
@@ -0,0 +1,145 @@
1
+ import { VERIFICATION_STATUS } from '../../crypto';
2
+ import {
3
+ resultOk,
4
+ resultError,
5
+ Author,
6
+ AnonymousUser,
7
+ ProtonDriveTelemetry,
8
+ Logger,
9
+ MetricsDecryptionErrorField,
10
+ MetricVerificationErrorField,
11
+ } from '../../interface';
12
+ import { getVerificationMessage } from '../errors';
13
+ import { splitNodeUid } from '../uids';
14
+ import {
15
+ EncryptedNode,
16
+ SharesService,
17
+ } from './interface';
18
+
19
+ export class NodesCryptoReporter {
20
+ private logger: Logger;
21
+
22
+ private reportedDecryptionErrors = new Set<string>();
23
+ private reportedVerificationErrors = new Set<string>();
24
+
25
+ constructor(
26
+ private telemetry: ProtonDriveTelemetry,
27
+ private shareService: SharesService,
28
+ ) {
29
+ this.telemetry = telemetry;
30
+ this.logger = telemetry.getLogger('nodes-crypto');
31
+ this.shareService = shareService;
32
+ }
33
+
34
+ async handleClaimedAuthor(
35
+ node: { uid: string; creationTime: Date },
36
+ field: MetricVerificationErrorField,
37
+ signatureType: string,
38
+ verified: VERIFICATION_STATUS,
39
+ verificationErrors?: Error[],
40
+ claimedAuthor?: string,
41
+ notAvailableVerificationKeys = false,
42
+ ): Promise<Author> {
43
+ const author = handleClaimedAuthor(
44
+ signatureType,
45
+ verified,
46
+ verificationErrors,
47
+ claimedAuthor,
48
+ notAvailableVerificationKeys,
49
+ );
50
+ if (!author.ok) {
51
+ void this.reportVerificationError(node, field, verificationErrors, claimedAuthor);
52
+ }
53
+ return author;
54
+ }
55
+
56
+ async reportVerificationError(
57
+ node: { uid: string; creationTime: Date },
58
+ field: MetricVerificationErrorField,
59
+ verificationErrors?: Error[],
60
+ claimedAuthor?: string,
61
+ ) {
62
+ if (this.reportedVerificationErrors.has(node.uid)) {
63
+ return;
64
+ }
65
+ this.reportedVerificationErrors.add(node.uid);
66
+
67
+ const fromBefore2024 = node.creationTime < new Date('2024-01-01');
68
+
69
+ let addressMatchingDefaultShare, volumeType;
70
+ try {
71
+ const { volumeId } = splitNodeUid(node.uid);
72
+ const { email } = await this.shareService.getMyFilesShareMemberEmailKey();
73
+ addressMatchingDefaultShare = claimedAuthor ? claimedAuthor === email : undefined;
74
+ volumeType = await this.shareService.getVolumeMetricContext(volumeId);
75
+ } catch (error: unknown) {
76
+ this.logger.error('Failed to check if claimed author matches default share', error);
77
+ }
78
+
79
+ this.logger.warn(
80
+ `Failed to verify ${field} for node ${node.uid} (from before 2024: ${fromBefore2024}, matching address: ${addressMatchingDefaultShare})`,
81
+ );
82
+
83
+ this.telemetry.recordMetric({
84
+ eventName: 'verificationError',
85
+ volumeType,
86
+ field,
87
+ addressMatchingDefaultShare,
88
+ fromBefore2024,
89
+ error: verificationErrors?.map((e) => e.message).join(', '),
90
+ uid: node.uid,
91
+ });
92
+ }
93
+
94
+ async reportDecryptionError(node: EncryptedNode, field: MetricsDecryptionErrorField, error: unknown) {
95
+ if (this.reportedDecryptionErrors.has(node.uid)) {
96
+ return;
97
+ }
98
+
99
+ const fromBefore2024 = node.creationTime < new Date('2024-01-01');
100
+
101
+ let volumeType;
102
+ try {
103
+ const { volumeId } = splitNodeUid(node.uid);
104
+ volumeType = await this.shareService.getVolumeMetricContext(volumeId);
105
+ } catch (error: unknown) {
106
+ this.logger.error('Failed to get metric context', error);
107
+ }
108
+
109
+ this.logger.error(`Failed to decrypt node ${node.uid} (from before 2024: ${fromBefore2024})`, error);
110
+
111
+ this.telemetry.recordMetric({
112
+ eventName: 'decryptionError',
113
+ volumeType,
114
+ field,
115
+ fromBefore2024,
116
+ error,
117
+ uid: node.uid,
118
+ });
119
+ this.reportedDecryptionErrors.add(node.uid);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * @param signatureType - Must be translated before calling this function.
125
+ */
126
+ function handleClaimedAuthor(
127
+ signatureType: string,
128
+ verified: VERIFICATION_STATUS,
129
+ verificationErrors?: Error[],
130
+ claimedAuthor?: string,
131
+ notAvailableVerificationKeys = false,
132
+ ): Author {
133
+ if (!claimedAuthor && notAvailableVerificationKeys) {
134
+ return resultOk(null as AnonymousUser);
135
+ }
136
+
137
+ if (verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
138
+ return resultOk(claimedAuthor || (null as AnonymousUser));
139
+ }
140
+
141
+ return resultError({
142
+ claimedAuthor,
143
+ error: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
144
+ });
145
+ }
@@ -3,6 +3,7 @@ import { MemberRole, ProtonDriveAccount, ProtonDriveTelemetry, RevisionState } f
3
3
  import { getMockTelemetry } from '../../tests/telemetry';
4
4
  import { DecryptedNode, DecryptedNodeKeys, DecryptedUnparsedNode, EncryptedNode, SharesService } from './interface';
5
5
  import { NodesCryptoService } from './cryptoService';
6
+ import { NodesCryptoReporter } from './cryptoReporter';
6
7
 
7
8
  describe('nodesCryptoService', () => {
8
9
  let telemetry: ProtonDriveTelemetry;
@@ -74,7 +75,8 @@ describe('nodesCryptoService', () => {
74
75
  getVolumeMetricContext: jest.fn().mockResolvedValue('own_volume'),
75
76
  };
76
77
 
77
- cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, sharesService);
78
+ const nodesCryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
79
+ cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, nodesCryptoReporter);
78
80
  });
79
81
 
80
82
  const parentKey = 'parentKey' as unknown as PrivateKey;
@@ -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(