@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.
- package/dist/diagnostic/interface.d.ts +26 -29
- package/dist/diagnostic/sdkDiagnostic.js +50 -24
- package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +7 -7
- package/dist/errors.js.map +1 -1
- package/dist/interface/author.d.ts +1 -1
- package/dist/interface/events.d.ts +1 -1
- package/dist/interface/events.js.map +1 -1
- package/dist/interface/sharing.d.ts +3 -1
- package/dist/internal/apiService/apiService.js +11 -3
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/errorCodes.d.ts +1 -0
- package/dist/internal/apiService/errors.js +1 -0
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/nodes/apiService.js +3 -0
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +18 -0
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.js +3 -1
- package/dist/internal/nodes/cache.js.map +1 -1
- package/dist/internal/nodes/cache.test.js +25 -0
- package/dist/internal/nodes/cache.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.js +44 -20
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.js +2 -2
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.js +0 -2
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/sharing/cryptoService.d.ts +1 -0
- package/dist/internal/sharing/cryptoService.js +14 -5
- package/dist/internal/sharing/cryptoService.js.map +1 -1
- package/dist/internal/sharing/interface.d.ts +0 -4
- package/dist/internal/sharing/sharingAccess.d.ts +1 -0
- package/dist/internal/sharing/sharingAccess.js +10 -3
- package/dist/internal/sharing/sharingAccess.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.test.js +9 -3
- package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.js +27 -16
- package/dist/internal/sharing/sharingManagement.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.test.js +29 -13
- package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
- package/dist/internal/upload/manager.js +1 -3
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +2 -2
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +8 -8
- package/dist/protonDriveClient.js +14 -14
- package/dist/protonDriveClient.js.map +1 -1
- package/package.json +1 -1
- package/src/diagnostic/interface.ts +27 -29
- package/src/diagnostic/sdkDiagnostic.ts +58 -30
- package/src/errors.ts +5 -5
- package/src/interface/author.ts +1 -1
- package/src/interface/events.ts +1 -7
- package/src/interface/sharing.ts +3 -1
- package/src/internal/apiService/apiService.ts +12 -3
- package/src/internal/apiService/errorCodes.ts +1 -0
- package/src/internal/apiService/errors.ts +1 -0
- package/src/internal/nodes/apiService.test.ts +28 -0
- package/src/internal/nodes/apiService.ts +3 -0
- package/src/internal/nodes/cache.test.ts +28 -0
- package/src/internal/nodes/cache.ts +3 -1
- package/src/internal/nodes/cryptoService.ts +68 -34
- package/src/internal/nodes/nodesAccess.ts +2 -2
- package/src/internal/nodes/nodesManagement.ts +0 -3
- package/src/internal/sharing/cryptoService.ts +19 -5
- package/src/internal/sharing/interface.ts +0 -4
- package/src/internal/sharing/sharingAccess.test.ts +10 -4
- package/src/internal/sharing/sharingAccess.ts +10 -2
- package/src/internal/sharing/sharingManagement.test.ts +54 -24
- package/src/internal/sharing/sharingManagement.ts +47 -16
- package/src/internal/upload/manager.test.ts +2 -2
- package/src/internal/upload/manager.ts +2 -4
- 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
|
-
|
|
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,
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
74
|
-
name = '
|
|
73
|
+
export class NodeWithSameNameExistsValidationError extends ValidationError {
|
|
74
|
+
name = 'NodeWithSameNameExistsValidationError';
|
|
75
75
|
|
|
76
76
|
public readonly existingNodeUid?: string;
|
|
77
77
|
|
|
78
|
-
public readonly
|
|
78
|
+
public readonly isUnfinishedUpload: boolean;
|
|
79
79
|
|
|
80
|
-
constructor(message: string, code: number, existingNodeUid?: string,
|
|
80
|
+
constructor(message: string, code: number, existingNodeUid?: string, isUnfinishedUpload = false) {
|
|
81
81
|
super(message, code);
|
|
82
82
|
this.existingNodeUid = existingNodeUid;
|
|
83
|
-
this.
|
|
83
|
+
this.isUnfinishedUpload = isUnfinishedUpload;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
package/src/interface/author.ts
CHANGED
package/src/interface/events.ts
CHANGED
|
@@ -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
|
| {
|
package/src/interface/sharing.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|
|
@@ -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:
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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;
|