@protontech/drive-sdk 0.7.2 → 0.8.0

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 (55) hide show
  1. package/dist/crypto/driveCrypto.js +1 -1
  2. package/dist/crypto/driveCrypto.js.map +1 -1
  3. package/dist/crypto/interface.d.ts +3 -1
  4. package/dist/crypto/openPGPCrypto.d.ts +4 -1
  5. package/dist/crypto/openPGPCrypto.js +2 -1
  6. package/dist/crypto/openPGPCrypto.js.map +1 -1
  7. package/dist/interface/account.d.ts +6 -0
  8. package/dist/internal/apiService/driveTypes.d.ts +197 -22
  9. package/dist/internal/nodes/apiService.d.ts +1 -1
  10. package/dist/internal/nodes/apiService.js +2 -2
  11. package/dist/internal/nodes/apiService.js.map +1 -1
  12. package/dist/internal/nodes/cryptoService.d.ts +1 -0
  13. package/dist/internal/nodes/cryptoService.js +28 -4
  14. package/dist/internal/nodes/cryptoService.js.map +1 -1
  15. package/dist/internal/nodes/cryptoService.test.js +70 -2
  16. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  17. package/dist/internal/nodes/nodesManagement.d.ts +1 -1
  18. package/dist/internal/nodes/nodesManagement.js +1 -1
  19. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  20. package/dist/internal/photos/timeline.d.ts +1 -1
  21. package/dist/internal/photos/timeline.js +4 -4
  22. package/dist/internal/photos/timeline.js.map +1 -1
  23. package/dist/internal/photos/timeline.test.js +34 -7
  24. package/dist/internal/photos/timeline.test.js.map +1 -1
  25. package/dist/internal/shares/apiService.js +2 -0
  26. package/dist/internal/shares/apiService.js.map +1 -1
  27. package/dist/internal/sharingPublic/nodes.d.ts +1 -1
  28. package/dist/internal/sharingPublic/nodes.js +2 -2
  29. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  30. package/dist/protonDriveClient.d.ts +1 -1
  31. package/dist/protonDriveClient.js +2 -2
  32. package/dist/protonDriveClient.js.map +1 -1
  33. package/dist/protonDrivePhotosClient.d.ts +20 -0
  34. package/dist/protonDrivePhotosClient.js +25 -2
  35. package/dist/protonDrivePhotosClient.js.map +1 -1
  36. package/dist/protonDrivePublicLinkClient.d.ts +3 -1
  37. package/dist/protonDrivePublicLinkClient.js +4 -2
  38. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/crypto/driveCrypto.ts +1 -0
  41. package/src/crypto/interface.ts +1 -0
  42. package/src/crypto/openPGPCrypto.ts +3 -0
  43. package/src/interface/account.ts +6 -0
  44. package/src/internal/apiService/driveTypes.ts +197 -22
  45. package/src/internal/nodes/apiService.ts +13 -6
  46. package/src/internal/nodes/cryptoService.test.ts +113 -2
  47. package/src/internal/nodes/cryptoService.ts +53 -8
  48. package/src/internal/nodes/nodesManagement.ts +1 -1
  49. package/src/internal/photos/timeline.test.ts +39 -7
  50. package/src/internal/photos/timeline.ts +4 -4
  51. package/src/internal/shares/apiService.ts +3 -1
  52. package/src/internal/sharingPublic/nodes.ts +2 -2
  53. package/src/protonDriveClient.ts +2 -2
  54. package/src/protonDrivePhotosClient.ts +26 -2
  55. package/src/protonDrivePublicLinkClient.ts +4 -2
@@ -491,7 +491,8 @@ export interface paths {
491
491
  get?: never;
492
492
  put?: never;
493
493
  /**
494
- * Delete children
494
+ * Delete drafts from folder
495
+ * @deprecated
495
496
  * @description Permanently delete children from folder, skipping trash. Can only be done for draft links.
496
497
  */
497
498
  post: operations["post_drive-shares-{shareID}-folders-{linkID}-delete_multiple"];
@@ -531,8 +532,8 @@ export interface paths {
531
532
  get?: never;
532
533
  put?: never;
533
534
  /**
534
- * Trash children
535
- * @description Send children to trash
535
+ * Trash children from folder
536
+ * @deprecated
536
537
  */
537
538
  post: operations["post_drive-shares-{shareID}-folders-{linkID}-trash_multiple"];
538
539
  delete?: never;
@@ -672,8 +673,8 @@ export interface paths {
672
673
  get?: never;
673
674
  put?: never;
674
675
  /**
675
- * Delete multiple (v2)
676
- * @description Permanently delete links, skipping trash. Can only be done for draft links.
676
+ * Delete drafts
677
+ * @description Permanently delete files, skipping trash. Can only be done for draft links.
677
678
  */
678
679
  post: operations["post_drive-v2-volumes-{volumeID}-delete_multiple"];
679
680
  delete?: never;
@@ -815,6 +816,32 @@ export interface paths {
815
816
  patch?: never;
816
817
  trace?: never;
817
818
  };
819
+ "/drive/v2/volumes/{volumeID}/remove-mine": {
820
+ parameters: {
821
+ query?: never;
822
+ header?: never;
823
+ path?: never;
824
+ cookie?: never;
825
+ };
826
+ get?: never;
827
+ put?: never;
828
+ /**
829
+ * Remove my nodes skipping trash
830
+ * @description This is called by Web SDK on public sharing to remove active nodes created by the same user
831
+ * as a way to delete wrongly uploaded files without going to trash. It's supported on the following conditions:
832
+ * - anonymous users must have created the node in their own session
833
+ * - for authenticated users the signature email must match
834
+ * - file/folder must have been created within the last 1 hour
835
+ * - folders must be empty
836
+ * - files must have all revisions created by this user
837
+ */
838
+ post: operations["post_drive-v2-volumes-{volumeID}-remove-mine"];
839
+ delete?: never;
840
+ options?: never;
841
+ head?: never;
842
+ patch?: never;
843
+ trace?: never;
844
+ };
818
845
  "/drive/v2/volumes/{volumeID}/links/{linkID}/rename": {
819
846
  parameters: {
820
847
  query?: never;
@@ -829,6 +856,11 @@ export interface paths {
829
856
  *
830
857
  * Clients renaming a file or folder MUST reuse the existing session key
831
858
  * for the name as it is also used by shares pointing to the link.
859
+ *
860
+ * Users with access only through a public sharing URL (no editor membership) are limited to renaming
861
+ * their own files and folders:
862
+ * - Unauthenticated users must have created them in their session
863
+ * - Authenticated users' email must match the signature email on the node for folders or active revision for files
832
864
  */
833
865
  put: operations["put_drive-v2-volumes-{volumeID}-links-{linkID}-rename"];
834
866
  post?: never;
@@ -852,6 +884,11 @@ export interface paths {
852
884
  *
853
885
  * Clients renaming a file or folder MUST reuse the existing session key
854
886
  * for the name as it is also used by shares pointing to the link.
887
+ *
888
+ * Users with access only through a public sharing URL (no editor membership) are limited to renaming
889
+ * their own files and folders:
890
+ * - Unauthenticated users must have created them in their session
891
+ * - Authenticated users' email must match the signature email on the node for folders or active revision for files
855
892
  */
856
893
  put: operations["put_drive-shares-{shareID}-links-{linkID}-rename"];
857
894
  post?: never;
@@ -2227,7 +2264,7 @@ export interface paths {
2227
2264
  get?: never;
2228
2265
  put?: never;
2229
2266
  /**
2230
- * Delete multiple (v2)
2267
+ * Delete drafts
2231
2268
  * @description See /drive/v2/volumes/{volumeID}/delete_multiple for full documentation
2232
2269
  */
2233
2270
  post: operations["post_drive-unauth-v2-volumes-{volumeID}-delete_multiple"];
@@ -2297,6 +2334,26 @@ export interface paths {
2297
2334
  patch?: never;
2298
2335
  trace?: never;
2299
2336
  };
2337
+ "/drive/unauth/v2/volumes/{volumeID}/remove-mine": {
2338
+ parameters: {
2339
+ query?: never;
2340
+ header?: never;
2341
+ path?: never;
2342
+ cookie?: never;
2343
+ };
2344
+ get?: never;
2345
+ put?: never;
2346
+ /**
2347
+ * Remove my nodes skipping trash
2348
+ * @description See /drive/v2/volumes/{volumeID}/remove-mine for full documentation
2349
+ */
2350
+ post: operations["post_drive-unauth-v2-volumes-{volumeID}-remove-mine"];
2351
+ delete?: never;
2352
+ options?: never;
2353
+ head?: never;
2354
+ patch?: never;
2355
+ trace?: never;
2356
+ };
2300
2357
  "/drive/unauth/v2/volumes/{volumeID}/links/{linkID}/rename": {
2301
2358
  parameters: {
2302
2359
  query?: never;
@@ -2978,6 +3035,30 @@ export interface paths {
2978
3035
  patch?: never;
2979
3036
  trace?: never;
2980
3037
  };
3038
+ "/drive/organization/volumes": {
3039
+ parameters: {
3040
+ query?: never;
3041
+ header?: never;
3042
+ path?: never;
3043
+ cookie?: never;
3044
+ };
3045
+ get?: never;
3046
+ put?: never;
3047
+ /**
3048
+ * Create Organization volume
3049
+ * @description Only allowed to Org administrators
3050
+ *
3051
+ * This new volume would have:
3052
+ * + OwnerOrgID filled with the orgID of the request
3053
+ * + specific membership for the owner (OrgAdmin to true)
3054
+ */
3055
+ post: operations["post_drive-organization-volumes"];
3056
+ delete?: never;
3057
+ options?: never;
3058
+ head?: never;
3059
+ patch?: never;
3060
+ trace?: never;
3061
+ };
2981
3062
  "/drive/volumes": {
2982
3063
  parameters: {
2983
3064
  query?: never;
@@ -4852,6 +4933,31 @@ export interface components {
4852
4933
  /** @description Order and visibility of Photo Tags, tags not in the list should not be shown; Use defaults when NULL; Show no tags if empty array. */
4853
4934
  PhotoTags?: components["schemas"]["TagType"][] | null;
4854
4935
  };
4936
+ CreateOrgVolumeRequestDto: {
4937
+ AddressID: components["schemas"]["AddressID"];
4938
+ /** @description XX's encrypted AddressKeyID. Must be the primary key from the AddressID */
4939
+ AddressKeyID: string;
4940
+ ShareKey: components["schemas"]["PGPPrivateKey"];
4941
+ SharePassphrase: components["schemas"]["PGPMessage"];
4942
+ SharePassphraseSignature: components["schemas"]["PGPSignature"];
4943
+ FolderName: components["schemas"]["PGPMessage"];
4944
+ FolderKey: components["schemas"]["PGPPrivateKey"];
4945
+ FolderPassphrase: components["schemas"]["PGPMessage"];
4946
+ FolderPassphraseSignature: components["schemas"]["PGPSignature"];
4947
+ FolderHashKey: components["schemas"]["PGPMessage"];
4948
+ OrganizationID: components["schemas"]["Id"];
4949
+ /** @description Name of the org. volume. It's plain text so that name can be displayed in UI menu */
4950
+ VolumeName: string;
4951
+ };
4952
+ GetVolumeResponseDto: {
4953
+ Volume: components["schemas"]["VolumeResponseDto"];
4954
+ /**
4955
+ * ProtonResponseCode
4956
+ * @example 1000
4957
+ * @enum {integer}
4958
+ */
4959
+ Code: 1000;
4960
+ };
4855
4961
  CreateVolumeRequestDto: {
4856
4962
  AddressID: components["schemas"]["AddressID"];
4857
4963
  ShareKey: components["schemas"]["PGPPrivateKey"];
@@ -4875,15 +4981,6 @@ export interface components {
4875
4981
  */
4876
4982
  ShareName: string | null;
4877
4983
  };
4878
- GetVolumeResponseDto: {
4879
- Volume: components["schemas"]["VolumeResponseDto"];
4880
- /**
4881
- * ProtonResponseCode
4882
- * @example 1000
4883
- * @enum {integer}
4884
- */
4885
- Code: 1000;
4886
- };
4887
4984
  ListVolumesResponseDto: {
4888
4985
  Volumes: components["schemas"]["VolumeResponseDto"][];
4889
4986
  /**
@@ -6213,20 +6310,20 @@ export interface components {
6213
6310
  Album: null | null;
6214
6311
  };
6215
6312
  /**
6216
- * @description <p>1=Main, 2=Standard, 3=Device, 4=Photo</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Name</th><th>Description</th></tr><tr><td>1</td><td>Main</td><td>* Root share for my files</td></tr><tr><td>2</td><td>Standard</td><td>* Collaborative share anywhere in the link tree (but not at the root folder as it cannot be shared)</td></tr><tr><td>3</td><td>Device</td><td>* Root share of devices</td></tr><tr><td>4</td><td>Photo</td><td>* Root share for photos</td></tr></table></details></details>
6313
+ * @description <p>1=Main, 2=Standard, 3=Device, 4=Photo</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Name</th><th>Description</th></tr><tr><td>1</td><td>Main</td><td>* Root share for my files</td></tr><tr><td>2</td><td>Standard</td><td>* Collaborative share anywhere in the link tree (but not at the root folder as it cannot be shared)</td></tr><tr><td>3</td><td>Device</td><td>* Root share of devices</td></tr><tr><td>4</td><td>Photo</td><td>* Root share for photos</td></tr><tr><td>5</td><td>Organization</td><td>* Root share for organization</td></tr></table></details></details>
6217
6314
  * @enum {integer}
6218
6315
  */
6219
- ShareType: 1 | 2 | 3 | 4;
6316
+ ShareType: 1 | 2 | 3 | 4 | 5;
6220
6317
  /**
6221
6318
  * @description <p>1=Active, 3=Restored</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Active</td></tr><tr><td>2</td><td>Deleted</td></tr><tr><td>3</td><td>Restored</td></tr><tr><td>6</td><td>Locked</td></tr></table></details></details>
6222
6319
  * @enum {integer}
6223
6320
  */
6224
6321
  ShareState: 1 | 2 | 3 | 6;
6225
6322
  /**
6226
- * @description <p>1=Regular, 2=Photo</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Regular</td></tr><tr><td>2</td><td>Photo</td></tr></table></details></details>
6323
+ * @description <p>1=Regular, 2=Photo</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Regular</td></tr><tr><td>2</td><td>Photo</td></tr><tr><td>3</td><td>Organization</td></tr></table></details></details>
6227
6324
  * @enum {integer}
6228
6325
  */
6229
- VolumeType: 1 | 2;
6326
+ VolumeType: 1 | 2 | 3;
6230
6327
  /**
6231
6328
  * @description <p>1=folder, 2=file</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Folder</td></tr><tr><td>2</td><td>File</td></tr><tr><td>3</td><td>Album</td></tr></table></details></details>
6232
6329
  * @enum {integer}
@@ -6776,10 +6873,10 @@ export interface components {
6776
6873
  LinkID: components["schemas"]["Id2"];
6777
6874
  };
6778
6875
  /**
6779
- * @description <details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Regular</td></tr><tr><td>2</td><td>Photo</td></tr></table></details></details>
6876
+ * @description <details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Regular</td></tr><tr><td>2</td><td>Photo</td></tr><tr><td>3</td><td>Organization</td></tr></table></details></details>
6780
6877
  * @enum {integer}
6781
6878
  */
6782
- VolumeType2: 1 | 2;
6879
+ VolumeType2: 1 | 2 | 3;
6783
6880
  /**
6784
6881
  * @description <p>Can be null if the Link was deleted</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>0</td><td>Draft</td></tr><tr><td>1</td><td>Active</td></tr><tr><td>2</td><td>Trashed</td></tr></table></details></details>
6785
6882
  * @enum {integer}
@@ -9119,6 +9216,36 @@ export interface operations {
9119
9216
  };
9120
9217
  };
9121
9218
  };
9219
+ "post_drive-v2-volumes-{volumeID}-remove-mine": {
9220
+ parameters: {
9221
+ query?: never;
9222
+ header?: never;
9223
+ path: {
9224
+ volumeID: string;
9225
+ };
9226
+ cookie?: never;
9227
+ };
9228
+ requestBody?: {
9229
+ content: {
9230
+ "application/json": components["schemas"]["LinkIDsRequestDto"];
9231
+ };
9232
+ };
9233
+ responses: {
9234
+ /** @description Ok */
9235
+ 200: {
9236
+ headers: {
9237
+ [name: string]: unknown;
9238
+ };
9239
+ content: {
9240
+ "application/json": {
9241
+ /** @enum {integer} */
9242
+ Code?: 1001;
9243
+ Responses?: components["schemas"]["MultiDeleteTransformer"][];
9244
+ };
9245
+ };
9246
+ };
9247
+ };
9248
+ };
9122
9249
  "put_drive-v2-volumes-{volumeID}-links-{linkID}-rename": {
9123
9250
  parameters: {
9124
9251
  query?: never;
@@ -12150,6 +12277,29 @@ export interface operations {
12150
12277
  };
12151
12278
  };
12152
12279
  };
12280
+ "post_drive-unauth-v2-volumes-{volumeID}-remove-mine": {
12281
+ parameters: {
12282
+ query?: never;
12283
+ header?: never;
12284
+ path: {
12285
+ volumeID: string;
12286
+ };
12287
+ cookie?: never;
12288
+ };
12289
+ requestBody?: {
12290
+ content: {
12291
+ "application/json": components["schemas"]["LinkIDsRequestDto"];
12292
+ };
12293
+ };
12294
+ responses: {
12295
+ default: {
12296
+ headers: {
12297
+ [name: string]: unknown;
12298
+ };
12299
+ content?: never;
12300
+ };
12301
+ };
12302
+ };
12153
12303
  "put_drive-unauth-v2-volumes-{volumeID}-links-{linkID}-rename": {
12154
12304
  parameters: {
12155
12305
  query?: never;
@@ -12404,7 +12554,7 @@ export interface operations {
12404
12554
  /** @description Show disabled shares as well, i.e. Shares where the ShareMemberShip for the user is non-active (locked), otherwise only return with active Membership */
12405
12555
  ShowAll?: 0 | 1;
12406
12556
  /** @description Filter on Share Type */
12407
- ShareType?: 1 | 2 | 3 | 4;
12557
+ ShareType?: 1 | 2 | 3 | 4 | 5;
12408
12558
  };
12409
12559
  header?: never;
12410
12560
  path?: never;
@@ -13477,6 +13627,31 @@ export interface operations {
13477
13627
  };
13478
13628
  };
13479
13629
  };
13630
+ "post_drive-organization-volumes": {
13631
+ parameters: {
13632
+ query?: never;
13633
+ header?: never;
13634
+ path?: never;
13635
+ cookie?: never;
13636
+ };
13637
+ requestBody?: {
13638
+ content: {
13639
+ "application/json": components["schemas"]["CreateOrgVolumeRequestDto"];
13640
+ };
13641
+ };
13642
+ responses: {
13643
+ /** @description Success */
13644
+ 200: {
13645
+ headers: {
13646
+ "x-pm-code": 1000;
13647
+ [name: string]: unknown;
13648
+ };
13649
+ content: {
13650
+ "application/json": components["schemas"]["GetVolumeResponseDto"];
13651
+ };
13652
+ };
13653
+ };
13654
+ };
13480
13655
  "get_drive-volumes": {
13481
13656
  parameters: {
13482
13657
  query?: {
@@ -71,13 +71,20 @@ type PutRestoreNodesRequest = Extract<
71
71
  type PutRestoreNodesResponse =
72
72
  drivePaths['/drive/v2/volumes/{volumeID}/trash/restore_multiple']['put']['responses']['200']['content']['application/json'];
73
73
 
74
- type PostDeleteNodesRequest = Extract<
74
+ type PostDeleteTrashedNodesRequest = Extract<
75
75
  drivePaths['/drive/v2/volumes/{volumeID}/trash/delete_multiple']['post']['requestBody'],
76
76
  { content: object }
77
77
  >['content']['application/json'];
78
- type PostDeleteNodesResponse =
78
+ type PostDeleteTrashedNodesResponse =
79
79
  drivePaths['/drive/v2/volumes/{volumeID}/trash/delete_multiple']['post']['responses']['200']['content']['application/json'];
80
80
 
81
+ type PostDeleteMyNodesRequest = Extract<
82
+ drivePaths['/drive/v2/volumes/{volumeID}/remove-mine']['post']['requestBody'],
83
+ { content: object }
84
+ >['content']['application/json'];
85
+ type PostDeleteMyNodesResponse =
86
+ drivePaths['/drive/v2/volumes/{volumeID}/remove-mine']['post']['responses']['200']['content']['application/json'];
87
+
81
88
  type PostCreateFolderRequest = Extract<
82
89
  drivePaths['/drive/v2/volumes/{volumeID}/folders']['post']['requestBody'],
83
90
  { content: object }
@@ -446,7 +453,7 @@ export abstract class NodeAPIServiceBase<
446
453
 
447
454
  async *deleteTrashedNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
448
455
  for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
449
- const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
456
+ const response = await this.apiService.post<PostDeleteTrashedNodesRequest, PostDeleteTrashedNodesResponse>(
450
457
  `drive/v2/volumes/${volumeId}/trash/delete_multiple`,
451
458
  {
452
459
  LinkIDs: batchNodeIds,
@@ -459,10 +466,10 @@ export abstract class NodeAPIServiceBase<
459
466
  }
460
467
  }
461
468
 
462
- async *deleteExistingNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
469
+ async *deleteMyNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
463
470
  for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
464
- const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
465
- `drive/v2/volumes/${volumeId}/delete_multiple`,
471
+ const response = await this.apiService.post<PostDeleteMyNodesRequest, PostDeleteMyNodesResponse>(
472
+ `drive/v2/volumes/${volumeId}/remove-mine`,
466
473
  {
467
474
  LinkIDs: batchNodeIds,
468
475
  },
@@ -20,6 +20,9 @@ describe('nodesCryptoService', () => {
20
20
 
21
21
  let cryptoService: NodesCryptoService;
22
22
 
23
+ const publicAddressKey = { _idx: 21312 };
24
+ const ownPrivateAddressKey = { id: 'id', key: 'key' as unknown as PrivateKey };
25
+
23
26
  beforeEach(() => {
24
27
  jest.clearAllMocks();
25
28
 
@@ -71,7 +74,15 @@ describe('nodesCryptoService', () => {
71
74
  };
72
75
  account = {
73
76
  // @ts-expect-error No need to implement all methods for mocking
74
- getPublicKeys: jest.fn(async () => [{ _idx: 21312 }]),
77
+ getPublicKeys: jest.fn(async () => [publicAddressKey]),
78
+ getOwnAddresses: jest.fn(async () => [
79
+ {
80
+ email: 'email',
81
+ addressId: 'addressId',
82
+ primaryKeyIndex: 0,
83
+ keys: [ownPrivateAddressKey],
84
+ },
85
+ ]),
75
86
  };
76
87
  // @ts-expect-error No need to implement all methods for mocking
77
88
  sharesService = {
@@ -576,6 +587,7 @@ describe('nodesCryptoService', () => {
576
587
  armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
577
588
  file: {
578
589
  base64ContentKeyPacket: 'base64ContentKeyPacket',
590
+ armoredContentKeyPacketSignature: 'armoredContentKeyPacketSignature',
579
591
  },
580
592
  activeRevision: {
581
593
  uid: 'revisionUid',
@@ -764,7 +776,7 @@ describe('nodesCryptoService', () => {
764
776
  });
765
777
  });
766
778
 
767
- it('on content key packet', async () => {
779
+ it('on content key packet without fallback verification', async () => {
768
780
  driveCrypto.decryptAndVerifySessionKey = jest.fn(
769
781
  async () =>
770
782
  Promise.resolve({
@@ -789,6 +801,105 @@ describe('nodesCryptoService', () => {
789
801
  error: 'verification error',
790
802
  });
791
803
  });
804
+
805
+ it('on content key packet with successful fallback verification', async () => {
806
+ driveCrypto.decryptAndVerifySessionKey = jest
807
+ .fn()
808
+ .mockImplementationOnce(
809
+ async () =>
810
+ Promise.resolve({
811
+ sessionKey: 'contentKeyPacketSessionKey',
812
+ verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
813
+ verificationErrors: [new Error('verification error')],
814
+ }) as any,
815
+ )
816
+ .mockImplementationOnce(
817
+ async () =>
818
+ Promise.resolve({
819
+ sessionKey: 'contentKeyPacketSessionKey',
820
+ verified: VERIFICATION_STATUS.SIGNED_AND_VALID,
821
+ }) as any,
822
+ );
823
+
824
+ const result = await cryptoService.decryptNode(
825
+ {
826
+ ...encryptedNode,
827
+ creationTime: new Date('2022-01-01'),
828
+ },
829
+ parentKey,
830
+ );
831
+ verifyResult(result);
832
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledTimes(2);
833
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
834
+ 'base64ContentKeyPacket',
835
+ 'armoredContentKeyPacketSignature',
836
+ 'decryptedKey',
837
+ ['decryptedKey', publicAddressKey],
838
+ );
839
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
840
+ 'base64ContentKeyPacket',
841
+ 'armoredContentKeyPacketSignature',
842
+ 'decryptedKey',
843
+ [ownPrivateAddressKey.key],
844
+ );
845
+ expect(telemetry.recordMetric).not.toHaveBeenCalled();
846
+ });
847
+
848
+ it('on content key packet with failed fallback verification', async () => {
849
+ driveCrypto.decryptAndVerifySessionKey = jest
850
+ .fn()
851
+ .mockImplementationOnce(
852
+ async () =>
853
+ Promise.resolve({
854
+ sessionKey: 'contentKeyPacketSessionKey',
855
+ verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
856
+ verificationErrors: [new Error('verification error')],
857
+ }) as any,
858
+ )
859
+ .mockImplementationOnce(
860
+ async () =>
861
+ Promise.resolve({
862
+ sessionKey: 'contentKeyPacketSessionKey',
863
+ verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
864
+ verificationErrors: [new Error('fallback verification error')],
865
+ }) as any,
866
+ );
867
+
868
+ const result = await cryptoService.decryptNode(
869
+ {
870
+ ...encryptedNode,
871
+ creationTime: new Date('2022-01-01'),
872
+ },
873
+ parentKey,
874
+ );
875
+ verifyResult(result, {
876
+ keyAuthor: {
877
+ ok: false,
878
+ error: {
879
+ claimedAuthor: 'signatureEmail',
880
+ error: 'Signature verification for content key failed: verification error',
881
+ },
882
+ },
883
+ });
884
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledTimes(2);
885
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
886
+ 'base64ContentKeyPacket',
887
+ 'armoredContentKeyPacketSignature',
888
+ 'decryptedKey',
889
+ ['decryptedKey', publicAddressKey],
890
+ );
891
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
892
+ 'base64ContentKeyPacket',
893
+ 'armoredContentKeyPacketSignature',
894
+ 'decryptedKey',
895
+ [ownPrivateAddressKey.key],
896
+ );
897
+ verifyLogEventVerificationError({
898
+ field: 'nodeContentKey',
899
+ error: 'verification error',
900
+ fromBefore2024: true,
901
+ });
902
+ });
792
903
  });
793
904
 
794
905
  describe('should decrypt with decryption issues', () => {
@@ -25,6 +25,7 @@ import {
25
25
  EncryptedRevision,
26
26
  DecryptedUnparsedRevision,
27
27
  NodeSigningKeys,
28
+ EncryptedNodeFileCrypto,
28
29
  } from './interface';
29
30
 
30
31
  export interface NodesCryptoReporter {
@@ -200,14 +201,7 @@ export class NodesCryptoService {
200
201
  if ('file' in node.encryptedCrypto) {
201
202
  const [activeRevisionPromise, contentKeyPacketSessionKeyPromise] = [
202
203
  this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
203
- this.driveCrypto.decryptAndVerifySessionKey(
204
- node.encryptedCrypto.file.base64ContentKeyPacket,
205
- node.encryptedCrypto.file.armoredContentKeyPacketSignature,
206
- key,
207
- // Content key packet is signed with the node key, but
208
- // in the past some clients signed with the address key.
209
- [key, ...keyVerificationKeys],
210
- ),
204
+ this.decryptContentKeyPacket(node, node.encryptedCrypto, key, keyVerificationKeys),
211
205
  ];
212
206
 
213
207
  try {
@@ -502,6 +496,57 @@ export class NodesCryptoService {
502
496
  };
503
497
  }
504
498
 
499
+ private async decryptContentKeyPacket(
500
+ node: EncryptedNode,
501
+ encryptedCrypto: EncryptedNodeFileCrypto,
502
+ key: PrivateKey,
503
+ keyVerificationKeys: PublicKey[],
504
+ ): Promise<{
505
+ sessionKey: SessionKey;
506
+ verified?: VERIFICATION_STATUS;
507
+ verificationErrors?: Error[];
508
+ }> {
509
+ const result = await this.driveCrypto.decryptAndVerifySessionKey(
510
+ encryptedCrypto.file.base64ContentKeyPacket,
511
+ encryptedCrypto.file.armoredContentKeyPacketSignature,
512
+ key,
513
+ // Content key packet is signed with the node key, but
514
+ // in the past some clients signed with the address key.
515
+ [key, ...keyVerificationKeys],
516
+ );
517
+
518
+ // Return right away if the verification is signed or not signed.
519
+ // If the verification is failing and the file is before 2023, try
520
+ // to decrypt with all owners keys. Because of the old nodes signed
521
+ // with address key instead of node key, when the node was renamed
522
+ // or moved, it could change the address but without updating the
523
+ // content key packet, which is now failing.
524
+ if (result.verified !== VERIFICATION_STATUS.SIGNED_AND_INVALID || node.creationTime > new Date(2023, 0, 1)) {
525
+ return result;
526
+ }
527
+
528
+ const allAddresses = await this.account.getOwnAddresses();
529
+ const allKeys = allAddresses.flatMap((address) => address.keys.map(({ key }) => key));
530
+
531
+ const resultWithAllKeys = await this.driveCrypto.decryptAndVerifySessionKey(
532
+ encryptedCrypto.file.base64ContentKeyPacket,
533
+ encryptedCrypto.file.armoredContentKeyPacketSignature,
534
+ key,
535
+ // Content key packet is signed with the node key, but
536
+ // in the past some clients signed with the address key.
537
+ allKeys,
538
+ );
539
+
540
+ // Return original result with original error if the fallback verification also fails.
541
+ if (resultWithAllKeys.verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
542
+ this.logger.warn(
543
+ 'Content key packet signature verification failed, but fallback to all addresses succeeded',
544
+ );
545
+ return resultWithAllKeys;
546
+ }
547
+ return result;
548
+ }
549
+
505
550
  private async decryptExtendedAttributes(
506
551
  node: { uid: string; creationTime: Date },
507
552
  encryptedExtendedAttributes: string | undefined,
@@ -293,7 +293,7 @@ export abstract class NodesManagementBase<
293
293
  }
294
294
  }
295
295
 
296
- async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
296
+ async *deleteTrashedNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
297
297
  for await (const result of this.apiService.deleteTrashedNodes(nodeUids, signal)) {
298
298
  if (result.ok) {
299
299
  await this.nodesAccess.notifyNodeDeleted(result.uid);