@protontech/drive-sdk 0.2.0 → 0.3.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 (75) hide show
  1. package/dist/diagnostic/interface.d.ts +26 -29
  2. package/dist/diagnostic/sdkDiagnostic.js +50 -24
  3. package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
  4. package/dist/errors.d.ts +3 -3
  5. package/dist/errors.js +7 -7
  6. package/dist/errors.js.map +1 -1
  7. package/dist/interface/author.d.ts +1 -1
  8. package/dist/interface/events.d.ts +1 -1
  9. package/dist/interface/events.js.map +1 -1
  10. package/dist/interface/sharing.d.ts +3 -1
  11. package/dist/internal/apiService/apiService.js +11 -3
  12. package/dist/internal/apiService/apiService.js.map +1 -1
  13. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  14. package/dist/internal/apiService/errors.js +1 -0
  15. package/dist/internal/apiService/errors.js.map +1 -1
  16. package/dist/internal/nodes/apiService.js +3 -0
  17. package/dist/internal/nodes/apiService.js.map +1 -1
  18. package/dist/internal/nodes/apiService.test.js +18 -0
  19. package/dist/internal/nodes/apiService.test.js.map +1 -1
  20. package/dist/internal/nodes/cache.js +3 -1
  21. package/dist/internal/nodes/cache.js.map +1 -1
  22. package/dist/internal/nodes/cache.test.js +25 -0
  23. package/dist/internal/nodes/cache.test.js.map +1 -1
  24. package/dist/internal/nodes/cryptoService.js +44 -20
  25. package/dist/internal/nodes/cryptoService.js.map +1 -1
  26. package/dist/internal/nodes/nodesAccess.js +2 -2
  27. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  28. package/dist/internal/nodes/nodesManagement.js +0 -2
  29. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  30. package/dist/internal/sharing/cryptoService.d.ts +1 -0
  31. package/dist/internal/sharing/cryptoService.js +14 -5
  32. package/dist/internal/sharing/cryptoService.js.map +1 -1
  33. package/dist/internal/sharing/interface.d.ts +0 -4
  34. package/dist/internal/sharing/sharingAccess.d.ts +1 -0
  35. package/dist/internal/sharing/sharingAccess.js +10 -3
  36. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  37. package/dist/internal/sharing/sharingAccess.test.js +9 -3
  38. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  39. package/dist/internal/sharing/sharingManagement.js +27 -16
  40. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  41. package/dist/internal/sharing/sharingManagement.test.js +29 -13
  42. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  43. package/dist/internal/upload/manager.js +1 -3
  44. package/dist/internal/upload/manager.js.map +1 -1
  45. package/dist/internal/upload/manager.test.js +2 -2
  46. package/dist/internal/upload/manager.test.js.map +1 -1
  47. package/dist/protonDriveClient.d.ts +8 -8
  48. package/dist/protonDriveClient.js +14 -14
  49. package/dist/protonDriveClient.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/diagnostic/interface.ts +27 -29
  52. package/src/diagnostic/sdkDiagnostic.ts +58 -30
  53. package/src/errors.ts +5 -5
  54. package/src/interface/author.ts +1 -1
  55. package/src/interface/events.ts +1 -7
  56. package/src/interface/sharing.ts +3 -1
  57. package/src/internal/apiService/apiService.ts +12 -3
  58. package/src/internal/apiService/errorCodes.ts +1 -0
  59. package/src/internal/apiService/errors.ts +1 -0
  60. package/src/internal/nodes/apiService.test.ts +28 -0
  61. package/src/internal/nodes/apiService.ts +3 -0
  62. package/src/internal/nodes/cache.test.ts +28 -0
  63. package/src/internal/nodes/cache.ts +3 -1
  64. package/src/internal/nodes/cryptoService.ts +68 -34
  65. package/src/internal/nodes/nodesAccess.ts +2 -2
  66. package/src/internal/nodes/nodesManagement.ts +0 -3
  67. package/src/internal/sharing/cryptoService.ts +19 -5
  68. package/src/internal/sharing/interface.ts +0 -4
  69. package/src/internal/sharing/sharingAccess.test.ts +10 -4
  70. package/src/internal/sharing/sharingAccess.ts +10 -2
  71. package/src/internal/sharing/sharingManagement.test.ts +54 -24
  72. package/src/internal/sharing/sharingManagement.ts +47 -16
  73. package/src/internal/upload/manager.test.ts +2 -2
  74. package/src/internal/upload/manager.ts +2 -4
  75. package/src/protonDriveClient.ts +17 -16
@@ -1,6 +1,6 @@
1
1
  import { Author, FileDownloader, MaybeNode, NodeType, Revision, ThumbnailType } from '../interface';
2
2
  import { ProtonDriveClient } from '../protonDriveClient';
3
- import { Diagnostic, DiagnosticOptions, DiagnosticResult } from './interface';
3
+ import { Diagnostic, DiagnosticOptions, DiagnosticResult, NodeDetails } from './interface';
4
4
  import { IntegrityVerificationStream } from './integrityVerificationStream';
5
5
 
6
6
  /**
@@ -43,32 +43,19 @@ export class SDKDiagnostic implements Diagnostic {
43
43
  }
44
44
 
45
45
  private async *verifyNode(node: MaybeNode, options?: DiagnosticOptions): AsyncGenerator<DiagnosticResult> {
46
- const nodeUid = node.ok ? node.value.uid : node.error.uid;
47
-
48
46
  if (!node.ok) {
49
47
  yield {
50
48
  type: 'degraded_node',
51
- nodeUid,
52
- node: node.error,
49
+ ...getNodeDetails(node),
53
50
  };
54
51
  }
55
52
 
53
+ yield* this.verifyAuthor(node.ok ? node.value.keyAuthor : node.error.keyAuthor, 'key', node);
54
+ yield* this.verifyAuthor(node.ok ? node.value.nameAuthor : node.error.nameAuthor, 'name', node);
55
+
56
56
  const activeRevision = getActiveRevision(node);
57
- const nodeInfo = {
58
- ...getNodeUids(node),
59
- node,
60
- };
61
-
62
- yield* this.verifyAuthor(node.ok ? node.value.keyAuthor : node.error.keyAuthor, {
63
- ...nodeInfo,
64
- authorType: 'key',
65
- });
66
- yield* this.verifyAuthor(node.ok ? node.value.nameAuthor : node.error.nameAuthor, {
67
- ...nodeInfo,
68
- authorType: 'name',
69
- });
70
57
  if (activeRevision) {
71
- yield* this.verifyAuthor(activeRevision.contentAuthor, { ...nodeInfo, authorType: 'content' });
58
+ yield* this.verifyAuthor(activeRevision.contentAuthor, 'content', node);
72
59
  }
73
60
 
74
61
  yield* this.verifyFileExtendedAttributes(node);
@@ -81,16 +68,14 @@ export class SDKDiagnostic implements Diagnostic {
81
68
  }
82
69
  }
83
70
 
84
- private async *verifyAuthor(
85
- author: Author,
86
- info: { nodeUid: string; authorType: string; revisionUid?: string; node: MaybeNode },
87
- ): AsyncGenerator<DiagnosticResult> {
71
+ private async *verifyAuthor(author: Author, authorType: string, node: MaybeNode): AsyncGenerator<DiagnosticResult> {
88
72
  if (!author.ok) {
89
73
  yield {
90
74
  type: 'unverified_author',
75
+ authorType,
91
76
  claimedAuthor: author.error.claimedAuthor,
92
77
  error: author.error.error,
93
- ...info,
78
+ ...getNodeDetails(node),
94
79
  };
95
80
  }
96
81
  }
@@ -104,17 +89,17 @@ export class SDKDiagnostic implements Diagnostic {
104
89
  if (claimedSha1 && !/^[0-9a-f]{40}$/i.test(claimedSha1)) {
105
90
  yield {
106
91
  type: 'extended_attributes_error',
107
- ...getNodeUids(node),
108
92
  field: 'sha1',
109
93
  value: claimedSha1,
94
+ ...getNodeDetails(node),
110
95
  };
111
96
  }
112
97
 
113
98
  if (expectedAttributes && !claimedSha1) {
114
99
  yield {
115
100
  type: 'extended_attributes_missing_field',
116
- ...getNodeUids(node),
117
101
  missingField: 'sha1',
102
+ ...getNodeDetails(node),
118
103
  };
119
104
  }
120
105
  }
@@ -127,7 +112,7 @@ export class SDKDiagnostic implements Diagnostic {
127
112
  if (!activeRevision) {
128
113
  yield {
129
114
  type: 'content_file_missing_revision',
130
- nodeUid: node.ok ? node.value.uid : node.error.uid,
115
+ ...getNodeDetails(node),
131
116
  };
132
117
  return;
133
118
  }
@@ -158,18 +143,18 @@ export class SDKDiagnostic implements Diagnostic {
158
143
  if (claimedSha1 !== computedSha1 || claimedSizeInBytes !== computedSizeInBytes) {
159
144
  yield {
160
145
  type: 'content_integrity_error',
161
- ...getNodeUids(node),
162
146
  claimedSha1,
163
147
  computedSha1,
164
148
  claimedSizeInBytes,
165
149
  computedSizeInBytes,
150
+ ...getNodeDetails(node),
166
151
  };
167
152
  }
168
153
  } catch (error: unknown) {
169
154
  yield {
170
155
  type: 'content_download_error',
171
- ...getNodeUids(node),
172
156
  error,
157
+ ...getNodeDetails(node),
173
158
  };
174
159
  }
175
160
  }
@@ -197,8 +182,8 @@ export class SDKDiagnostic implements Diagnostic {
197
182
  if (!result[0].ok && result[0].error !== 'Node has no thumbnail') {
198
183
  yield {
199
184
  type: 'thumbnails_error',
200
- nodeUid,
201
185
  error: result[0].error,
186
+ ...getNodeDetails(node),
202
187
  };
203
188
  }
204
189
  } catch (error: unknown) {
@@ -226,6 +211,49 @@ export class SDKDiagnostic implements Diagnostic {
226
211
  }
227
212
  }
228
213
 
214
+ function getNodeDetails(node: MaybeNode): NodeDetails {
215
+ const errors: {
216
+ field: string;
217
+ error: unknown;
218
+ }[] = [];
219
+
220
+ if (!node.ok) {
221
+ const degradedNode = node.error;
222
+ if (!degradedNode.name.ok) {
223
+ errors.push({
224
+ field: 'name',
225
+ error: degradedNode.name.error,
226
+ });
227
+ }
228
+ if (degradedNode.activeRevision?.ok === false) {
229
+ errors.push({
230
+ field: 'activeRevision',
231
+ error: degradedNode.activeRevision.error,
232
+ });
233
+ }
234
+ for (const error of degradedNode.errors ?? []) {
235
+ if (error instanceof Error) {
236
+ errors.push({
237
+ field: 'error',
238
+ error,
239
+ });
240
+ }
241
+ }
242
+ }
243
+
244
+ return {
245
+ safeNodeDetails: {
246
+ ...getNodeUids(node),
247
+ nodeType: getNodeType(node),
248
+ nodeCreationTime: node.ok ? node.value.creationTime : node.error.creationTime,
249
+ keyAuthor: node.ok ? node.value.keyAuthor : node.error.keyAuthor,
250
+ nameAuthor: node.ok ? node.value.nameAuthor : node.error.nameAuthor,
251
+ errors,
252
+ },
253
+ sensitiveNodeDetails: node,
254
+ };
255
+ }
256
+
229
257
  function getNodeUids(node: MaybeNode): { nodeUid: string; revisionUid?: string } {
230
258
  const activeRevision = getActiveRevision(node);
231
259
  return {
package/src/errors.ts CHANGED
@@ -70,17 +70,17 @@ export class ValidationError extends ProtonDriveError {
70
70
  * or choose another name. The available name is provided in the `availableName`
71
71
  * property (that will contain original name with the index that can be used).
72
72
  */
73
- export class NodeAlreadyExistsValidationError extends ValidationError {
74
- name = 'NodeAlreadyExistsValidationError';
73
+ export class NodeWithSameNameExistsValidationError extends ValidationError {
74
+ name = 'NodeWithSameNameExistsValidationError';
75
75
 
76
76
  public readonly existingNodeUid?: string;
77
77
 
78
- public readonly ongoingUploadByOtherClient: boolean;
78
+ public readonly isUnfinishedUpload: boolean;
79
79
 
80
- constructor(message: string, code: number, existingNodeUid?: string, ongoingUploadByOtherClient = false) {
80
+ constructor(message: string, code: number, existingNodeUid?: string, isUnfinishedUpload = false) {
81
81
  super(message, code);
82
82
  this.existingNodeUid = existingNodeUid;
83
- this.ongoingUploadByOtherClient = ongoingUploadByOtherClient;
83
+ this.isUnfinishedUpload = isUnfinishedUpload;
84
84
  }
85
85
  }
86
86
 
@@ -24,6 +24,6 @@ export type AnonymousUser = null;
24
24
  * the claimed author and the verification error.
25
25
  */
26
26
  export type UnverifiedAuthorError = {
27
- claimedAuthor?: string;
27
+ claimedAuthor?: string | AnonymousUser;
28
28
  error: string;
29
29
  };
@@ -20,13 +20,7 @@ export interface LatestEventIdProvider {
20
20
  */
21
21
  export type DriveListener = (event: DriveEvent) => Promise<void>;
22
22
 
23
- export type DriveEvent =
24
- | NodeEvent
25
- | FastForwardEvent
26
- | TreeRefreshEvent
27
- | TreeRemovalEvent
28
- | FastForwardEvent
29
- | SharedWithMeUpdated;
23
+ export type DriveEvent = NodeEvent | FastForwardEvent | TreeRefreshEvent | TreeRemovalEvent | SharedWithMeUpdated;
30
24
 
31
25
  export type NodeEvent =
32
26
  | {
@@ -51,6 +51,7 @@ export type Bookmark = {
51
51
  uid: string;
52
52
  creationTime: Date;
53
53
  url: string;
54
+ customPassword?: string;
54
55
  node: {
55
56
  name: string;
56
57
  type: NodeType;
@@ -65,8 +66,9 @@ export type Bookmark = {
65
66
  * SDK to represent the bookmark in a way that is easy to work with. Whenever
66
67
  * any field cannot be decrypted, it is returned as `DegradedBookmark` type.
67
68
  */
68
- export type DegradedBookmark = Omit<Bookmark, 'url' | 'node'> & {
69
+ export type DegradedBookmark = Omit<Bookmark, 'url' | 'customPassword' | 'node'> & {
69
70
  url: Result<string, Error>;
71
+ customPassword: Result<string | undefined, Error>;
70
72
  node: Omit<Bookmark['node'], 'name'> & {
71
73
  name: Result<string, Error | InvalidNameError>;
72
74
  };
@@ -239,7 +239,11 @@ export class DriveAPIService {
239
239
  throw new AbortError(c('Error').t`Request aborted`);
240
240
  }
241
241
 
242
- this.logger.debug(`${request.method} ${request.url}`);
242
+ if (attempt > 0) {
243
+ this.logger.debug(`${request.method} ${request.url}: retry ${attempt}`);
244
+ } else {
245
+ this.logger.debug(`${request.method} ${request.url}`);
246
+ }
243
247
 
244
248
  if (this.hasReachedServerErrorLimit) {
245
249
  this.logger.warn('Server errors limit reached');
@@ -250,6 +254,8 @@ export class DriveAPIService {
250
254
  throw new RateLimitedError(c('Error').t`Too many server requests, please try again later`);
251
255
  }
252
256
 
257
+ const start = Date.now();
258
+
253
259
  let response;
254
260
  try {
255
261
  response = await callback();
@@ -276,10 +282,13 @@ export class DriveAPIService {
276
282
  throw error;
277
283
  }
278
284
 
285
+ const end = Date.now();
286
+ const duration = end - start;
287
+
279
288
  if (response.ok) {
280
- this.logger.info(`${request.method} ${request.url}: ${response.status}`);
289
+ this.logger.info(`${request.method} ${request.url}: ${response.status} (${duration}ms)`);
281
290
  } else {
282
- this.logger.warn(`${request.method} ${request.url}: ${response.status}`);
291
+ this.logger.warn(`${request.method} ${request.url}: ${response.status} (${duration}ms)`);
283
292
  }
284
293
 
285
294
  if (response.status === HTTPErrorCode.TOO_MANY_REQUESTS) {
@@ -17,6 +17,7 @@ export const enum ErrorCode {
17
17
  OK = 1000,
18
18
  OK_MANY = 1001,
19
19
  OK_ASYNC = 1002,
20
+ INVALID_VALUE = 2001,
20
21
  NOT_ENOUGH_PERMISSIONS = 2011,
21
22
  NOT_ENOUGH_PERMISSIONS_TO_GRANT_PERMISSIONS = 2026,
22
23
  // Following codes takes name from the API documentation.
@@ -46,6 +46,7 @@ export function apiErrorFactory({ response, result }: { response: Response; resu
46
46
  // Here we convert only general enough codes. Specific cases that are
47
47
  // not clear from the code itself must be handled by each module
48
48
  // separately.
49
+ case ErrorCode.INVALID_VALUE:
49
50
  case ErrorCode.NOT_ENOUGH_PERMISSIONS:
50
51
  case ErrorCode.NOT_ENOUGH_PERMISSIONS_TO_GRANT_PERMISSIONS:
51
52
  case ErrorCode.ALREADY_EXISTS:
@@ -173,6 +173,34 @@ describe('nodeAPIService', () => {
173
173
  api = new NodeAPIService(getMockLogger(), apiMock);
174
174
  });
175
175
 
176
+ describe('getNode', () => {
177
+ it('should get node', async () => {
178
+ // @ts-expect-error Mocking for testing purposes
179
+ apiMock.post = jest.fn(async () =>
180
+ Promise.resolve({
181
+ Links: [generateAPIFolderNode()],
182
+ }),
183
+ );
184
+
185
+ const node = await api.getNode('volumeId~nodeId', 'volumeId');
186
+
187
+ expect(node).toStrictEqual(generateFolderNode());
188
+ });
189
+
190
+ it('should throw error if node is not found', async () => {
191
+ // @ts-expect-error Mocking for testing purposes
192
+ apiMock.post = jest.fn(async () =>
193
+ Promise.resolve({
194
+ Links: [],
195
+ }),
196
+ );
197
+
198
+ const promise = api.getNode('volumeId~nodeId', 'volumeId');
199
+
200
+ await expect(promise).rejects.toThrow('Node not found');
201
+ });
202
+ });
203
+
176
204
  describe('iterateNodes', () => {
177
205
  async function testIterateNodes(mockedLink: any, expectedNode: any, ownVolumeId = 'volumeId') {
178
206
  // @ts-expect-error Mocking for testing purposes
@@ -110,6 +110,9 @@ export class NodeAPIService {
110
110
  async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<EncryptedNode> {
111
111
  const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, signal);
112
112
  const result = await nodesGenerator.next();
113
+ if (!result.value) {
114
+ throw new ValidationError(c('Error').t`Node not found`);
115
+ }
113
116
  await nodesGenerator.return('finish');
114
117
  return result.value;
115
118
  }
@@ -118,6 +118,34 @@ describe('nodesCache', () => {
118
118
  storageSize: 100,
119
119
  contentAuthor: resultOk('test@test.com'),
120
120
  claimedModificationTime: new Date('2021-02-01'),
121
+ claimedSize: 100,
122
+ claimedDigests: {
123
+ sha1: 'hash',
124
+ },
125
+ claimedBlockSizes: [100],
126
+ claimedAdditionalMetadata: {
127
+ media: { width: 100, height: 100 },
128
+ },
129
+ });
130
+ const node = generateNode('node1', '', { activeRevision });
131
+
132
+ await cache.setNode(node);
133
+ const result = await cache.getNode(node.uid);
134
+
135
+ expect(result).toStrictEqual({
136
+ ...node,
137
+ activeRevision,
138
+ });
139
+ });
140
+
141
+ it('should store and retrieve node with active revision with no claimed data', async () => {
142
+ const activeRevision: Result<DecryptedRevision, Error> = resultOk({
143
+ uid: 'revision1',
144
+ state: RevisionState.Active,
145
+ creationTime: new Date('2021-01-01'),
146
+ storageSize: 100,
147
+ contentAuthor: resultOk('test@test.com'),
148
+ claimedModificationTime: undefined,
121
149
  });
122
150
  const node = generateNode('node1', '', { activeRevision });
123
151
 
@@ -302,7 +302,9 @@ function deserialiseRevision(revision: any): Result<DecryptedRevision, Error> {
302
302
  return resultOk({
303
303
  ...revision.value,
304
304
  creationTime: new Date(revision.value.creationTime),
305
- claimedModificationTime: new Date(revision.value.claimedModificationTime),
305
+ claimedModificationTime: revision.value.claimedModificationTime
306
+ ? new Date(revision.value.claimedModificationTime)
307
+ : undefined,
306
308
  });
307
309
  }
308
310
 
@@ -61,6 +61,8 @@ export class NodesCryptoService {
61
61
  node: EncryptedNode,
62
62
  parentKey: PrivateKey,
63
63
  ): Promise<{ node: DecryptedUnparsedNode; keys?: DecryptedNodeKeys }> {
64
+ const start = Date.now();
65
+
64
66
  const commonNodeMetadata = {
65
67
  ...node,
66
68
  encryptedCrypto: undefined,
@@ -89,16 +91,17 @@ export class NodesCryptoService {
89
91
  : nodeParentKeys;
90
92
  }
91
93
 
92
- const { name, author: nameAuthor } = await this.decryptName(node, parentKey, nameVerificationKeys);
93
-
94
- let membership;
95
- if (node.membership) {
96
- membership = await this.decryptMembership(node);
97
- }
94
+ // Start promises early, but await them only when required to do
95
+ // as much work as possible in parallel.
96
+ const [membershipPromise, namePromise, keyPromise] = [
97
+ node.membership ? this.decryptMembership(node) : undefined,
98
+ this.decryptName(node, parentKey, nameVerificationKeys),
99
+ this.decryptKey(node, parentKey, keyVerificationKeys),
100
+ ];
98
101
 
99
102
  let passphrase, key, passphraseSessionKey, keyAuthor;
100
103
  try {
101
- const keyResult = await this.decryptKey(node, parentKey, keyVerificationKeys);
104
+ const keyResult = await keyPromise;
102
105
  passphrase = keyResult.passphrase;
103
106
  key = keyResult.key;
104
107
  passphraseSessionKey = keyResult.passphraseSessionKey;
@@ -107,12 +110,17 @@ export class NodesCryptoService {
107
110
  void this.reportDecryptionError(node, 'nodeKey', error);
108
111
  const message = getErrorMessage(error);
109
112
  const errorMessage = c('Error').t`Failed to decrypt node key: ${message}`;
113
+ const { name, author: nameAuthor } = await namePromise;
114
+ const membership = await membershipPromise;
110
115
  return {
111
116
  node: {
112
117
  ...commonNodeMetadata,
113
118
  name,
114
119
  keyAuthor: resultError({
115
- claimedAuthor: node.encryptedCrypto.signatureEmail,
120
+ claimedAuthor: getClaimedAuthor(
121
+ node.encryptedCrypto.signatureEmail,
122
+ keyVerificationKeys.length === 0,
123
+ ),
116
124
  error: errorMessage,
117
125
  }),
118
126
  nameAuthor,
@@ -131,8 +139,23 @@ export class NodesCryptoService {
131
139
  let folder;
132
140
  let folderExtendedAttributesAuthor;
133
141
  if ('folder' in node.encryptedCrypto) {
142
+ const folderExtendedAttributesVerificationKeys = node.encryptedCrypto.signatureEmail
143
+ ? signatureEmailKeys
144
+ : [key];
145
+
146
+ const [hashKeyPromise, folderExtendedAttributesPromise] = [
147
+ this.decryptHashKey(node, key, signatureEmailKeys),
148
+ this.decryptExtendedAttributes(
149
+ node,
150
+ node.encryptedCrypto.folder.armoredExtendedAttributes,
151
+ key,
152
+ folderExtendedAttributesVerificationKeys,
153
+ node.encryptedCrypto.signatureEmail,
154
+ ),
155
+ ];
156
+
134
157
  try {
135
- const hashKeyResult = await this.decryptHashKey(node, key, signatureEmailKeys);
158
+ const hashKeyResult = await hashKeyPromise;
136
159
  hashKey = hashKeyResult.hashKey;
137
160
  hashKeyAuthor = hashKeyResult.author;
138
161
  } catch (error: unknown) {
@@ -141,16 +164,7 @@ export class NodesCryptoService {
141
164
  }
142
165
 
143
166
  try {
144
- const folderExtendedAttributesVerificationKeys = node.encryptedCrypto.signatureEmail
145
- ? signatureEmailKeys
146
- : [key];
147
- const extendedAttributesResult = await this.decryptExtendedAttributes(
148
- node,
149
- node.encryptedCrypto.folder.armoredExtendedAttributes,
150
- key,
151
- folderExtendedAttributesVerificationKeys,
152
- node.encryptedCrypto.signatureEmail,
153
- );
167
+ const extendedAttributesResult = await folderExtendedAttributesPromise;
154
168
  folder = {
155
169
  extendedAttributes: extendedAttributesResult.extendedAttributes,
156
170
  };
@@ -165,10 +179,20 @@ export class NodesCryptoService {
165
179
  let contentKeyPacketSessionKey;
166
180
  let contentKeyPacketAuthor;
167
181
  if ('file' in node.encryptedCrypto) {
182
+ const [activeRevisionPromise, contentKeyPacketSessionKeyPromise] = [
183
+ this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
184
+ this.driveCrypto.decryptAndVerifySessionKey(
185
+ node.encryptedCrypto.file.base64ContentKeyPacket,
186
+ node.encryptedCrypto.file.armoredContentKeyPacketSignature,
187
+ key,
188
+ // Content key packet is signed with the node key, but
189
+ // in the past some clients signed with the address key.
190
+ [key, ...keyVerificationKeys],
191
+ ),
192
+ ];
193
+
168
194
  try {
169
- activeRevision = resultOk(
170
- await this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
171
- );
195
+ activeRevision = resultOk(await activeRevisionPromise);
172
196
  } catch (error: unknown) {
173
197
  void this.reportDecryptionError(node, 'nodeExtendedAttributes', error);
174
198
  const message = getErrorMessage(error);
@@ -177,18 +201,10 @@ export class NodesCryptoService {
177
201
  }
178
202
 
179
203
  try {
180
- const keySessionKeyResult = await this.driveCrypto.decryptAndVerifySessionKey(
181
- node.encryptedCrypto.file.base64ContentKeyPacket,
182
- node.encryptedCrypto.file.armoredContentKeyPacketSignature,
183
- key,
184
- // Content key packet is signed with the node key, but
185
- // in the past some clients signed with the address key.
186
- [key, ...keyVerificationKeys],
187
- );
188
-
204
+ const keySessionKeyResult = await contentKeyPacketSessionKeyPromise;
189
205
  contentKeyPacketSessionKey = keySessionKeyResult.sessionKey;
190
206
  contentKeyPacketAuthor =
191
- keySessionKeyResult.verified &&
207
+ keySessionKeyResult.verified !== undefined &&
192
208
  (await this.handleClaimedAuthor(
193
209
  node,
194
210
  'nodeContentKey',
@@ -230,6 +246,13 @@ export class NodesCryptoService {
230
246
  finalKeyAuthor = keyAuthor;
231
247
  }
232
248
 
249
+ const { name, author: nameAuthor } = await namePromise;
250
+ const membership = await membershipPromise;
251
+
252
+ const end = Date.now();
253
+ const duration = end - start;
254
+ this.logger.debug(`Node ${node.uid} decrypted in ${duration}ms`);
255
+
233
256
  return {
234
257
  node: {
235
258
  ...commonNodeMetadata,
@@ -319,7 +342,7 @@ export class NodesCryptoService {
319
342
  return {
320
343
  name: resultError(new Error(errorMessage)),
321
344
  author: resultError({
322
- claimedAuthor: nameSignatureEmail,
345
+ claimedAuthor: getClaimedAuthor(nameSignatureEmail, verificationKeys.length === 0),
323
346
  error: errorMessage,
324
347
  }),
325
348
  };
@@ -635,6 +658,7 @@ export class NodesCryptoService {
635
658
  if (this.reportedVerificationErrors.has(node.uid)) {
636
659
  return;
637
660
  }
661
+ this.reportedVerificationErrors.add(node.uid);
638
662
 
639
663
  const fromBefore2024 = node.creationTime < new Date('2024-01-01');
640
664
 
@@ -661,7 +685,6 @@ export class NodesCryptoService {
661
685
  error: verificationErrors?.map((e) => e.message).join(', '),
662
686
  uid: node.uid,
663
687
  });
664
- this.reportedVerificationErrors.add(node.uid);
665
688
  }
666
689
 
667
690
  private async reportDecryptionError(node: EncryptedNode, field: MetricsDecryptionErrorField, error: unknown) {
@@ -716,3 +739,14 @@ function handleClaimedAuthor(
716
739
  error: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
717
740
  });
718
741
  }
742
+
743
+ function getClaimedAuthor(
744
+ claimedAuthor?: string,
745
+ notAvailableVerificationKeys = false,
746
+ ): string | AnonymousUser | undefined {
747
+ if (!claimedAuthor && notAvailableVerificationKeys) {
748
+ return null as AnonymousUser;
749
+ }
750
+
751
+ return claimedAuthor;
752
+ }
@@ -24,7 +24,7 @@ const BATCH_LOADING_SIZE = 30;
24
24
  // It is a trade-off between performance and memory usage.
25
25
  // Higher number means more memory usage, but faster decryption.
26
26
  // Lower number means less memory usage, but slower decryption.
27
- const DECRYPTION_CONCURRENCY = 15;
27
+ const DECRYPTION_CONCURRENCY = 30;
28
28
 
29
29
  /**
30
30
  * Provides access to node metadata.
@@ -234,7 +234,7 @@ export class NodesAccess {
234
234
 
235
235
  if (errors.length > 0) {
236
236
  this.logger.error(`Failed to decrypt ${errors.length} nodes`, errors);
237
- throw new ProtonDriveError(c('Error').t`Failed to decrypt some nodes`, { cause: errors });
237
+ throw new DecryptionError(c('Error').t`Failed to decrypt some nodes`, { cause: errors });
238
238
  }
239
239
 
240
240
  const missingNodeUids = nodeUids.filter((nodeUid) => !returnedNodeUids.includes(nodeUid));
@@ -189,11 +189,8 @@ export class NodesManagement {
189
189
  }
190
190
 
191
191
  async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
192
- const deletedNodeUids = [];
193
-
194
192
  for await (const result of this.apiService.deleteNodes(nodeUids, signal)) {
195
193
  if (result.ok) {
196
- deletedNodeUids.push(result.uid);
197
194
  await this.nodesAccess.notifyNodeDeleted(result.uid);
198
195
  }
199
196
  yield result;