@protontech/drive-sdk 0.3.1 → 0.4.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/crypto/driveCrypto.d.ts +1 -1
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/interface.d.ts +1 -1
- package/dist/crypto/openPGPCrypto.d.ts +1 -1
- package/dist/crypto/openPGPCrypto.js +4 -1
- package/dist/crypto/openPGPCrypto.js.map +1 -1
- package/dist/internal/apiService/errorCodes.d.ts +1 -0
- package/dist/internal/apiService/errors.d.ts +3 -0
- package/dist/internal/apiService/errors.js +7 -1
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/devices/interface.d.ts +1 -1
- package/dist/internal/devices/manager.js +1 -1
- package/dist/internal/devices/manager.js.map +1 -1
- package/dist/internal/devices/manager.test.js +3 -3
- package/dist/internal/devices/manager.test.js.map +1 -1
- package/dist/internal/download/cryptoService.js +2 -2
- package/dist/internal/download/cryptoService.js.map +1 -1
- package/dist/internal/download/fileDownloader.js +2 -2
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +3 -1
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/events/apiService.js +1 -1
- package/dist/internal/events/apiService.js.map +1 -1
- package/dist/internal/events/coreEventManager.js +1 -1
- package/dist/internal/events/coreEventManager.js.map +1 -1
- package/dist/internal/events/coreEventManager.test.js +18 -24
- package/dist/internal/events/coreEventManager.test.js.map +1 -1
- package/dist/internal/events/index.d.ts +3 -4
- package/dist/internal/events/index.js +4 -4
- package/dist/internal/events/index.js.map +1 -1
- package/dist/internal/events/interface.d.ts +3 -0
- package/dist/internal/nodes/apiService.d.ts +12 -3
- package/dist/internal/nodes/apiService.js +53 -13
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +19 -2
- 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/cryptoReporter.d.ts +20 -0
- package/dist/internal/nodes/cryptoReporter.js +96 -0
- package/dist/internal/nodes/cryptoReporter.js.map +1 -0
- package/dist/internal/nodes/cryptoService.d.ts +18 -13
- package/dist/internal/nodes/cryptoService.js +18 -98
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +7 -5
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/errors.d.ts +4 -0
- package/dist/internal/nodes/errors.js +9 -0
- package/dist/internal/nodes/errors.js.map +1 -0
- package/dist/internal/nodes/index.js +3 -1
- package/dist/internal/nodes/index.js.map +1 -1
- package/dist/internal/nodes/index.test.js +1 -1
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +5 -2
- package/dist/internal/nodes/nodesAccess.d.ts +4 -4
- package/dist/internal/nodes/nodesAccess.js +77 -69
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +48 -8
- package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +2 -0
- package/dist/internal/nodes/nodesManagement.js +86 -9
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +81 -5
- package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
- package/dist/internal/photos/albums.d.ts +9 -7
- package/dist/internal/photos/albums.js +26 -13
- package/dist/internal/photos/albums.js.map +1 -1
- package/dist/internal/photos/apiService.d.ts +34 -3
- package/dist/internal/photos/apiService.js +96 -3
- package/dist/internal/photos/apiService.js.map +1 -1
- package/dist/internal/photos/index.d.ts +20 -4
- package/dist/internal/photos/index.js +30 -7
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +25 -1
- package/dist/internal/photos/shares.d.ts +43 -0
- package/dist/internal/photos/shares.js +112 -0
- package/dist/internal/photos/shares.js.map +1 -0
- package/dist/internal/photos/timeline.d.ts +15 -0
- package/dist/internal/photos/timeline.js +22 -0
- package/dist/internal/photos/timeline.js.map +1 -0
- package/dist/internal/shares/manager.d.ts +1 -1
- package/dist/internal/shares/manager.js +4 -4
- package/dist/internal/shares/manager.js.map +1 -1
- package/dist/internal/shares/manager.test.js +7 -7
- package/dist/internal/shares/manager.test.js.map +1 -1
- package/dist/internal/sharing/cache.d.ts +3 -0
- package/dist/internal/sharing/cache.js +17 -2
- package/dist/internal/sharing/cache.js.map +1 -1
- package/dist/internal/sharing/interface.d.ts +2 -2
- package/dist/internal/sharing/interface.js +1 -1
- package/dist/internal/sharing/sharingAccess.js +7 -1
- package/dist/internal/sharing/sharingAccess.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.test.js +243 -34
- package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
- package/dist/internal/sharingPublic/apiService.d.ts +1 -1
- package/dist/internal/sharingPublic/apiService.js +9 -2
- package/dist/internal/sharingPublic/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoService.d.ts +6 -20
- package/dist/internal/sharingPublic/cryptoService.js +40 -103
- package/dist/internal/sharingPublic/cryptoService.js.map +1 -1
- package/dist/internal/sharingPublic/index.d.ts +2 -2
- package/dist/internal/sharingPublic/index.js +2 -2
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/interface.d.ts +1 -43
- package/dist/internal/sharingPublic/manager.d.ts +1 -1
- package/dist/internal/sharingPublic/manager.js +9 -7
- package/dist/internal/sharingPublic/manager.js.map +1 -1
- package/dist/internal/upload/streamUploader.js +1 -1
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +3 -1
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +20 -3
- package/dist/protonDriveClient.js +24 -4
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +86 -12
- package/dist/protonDrivePhotosClient.js +132 -29
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +13 -4
- package/dist/protonDrivePublicLinkClient.js +13 -11
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +1 -1
- package/src/crypto/interface.ts +1 -1
- package/src/crypto/openPGPCrypto.ts +5 -2
- package/src/internal/apiService/errorCodes.ts +1 -0
- package/src/internal/apiService/errors.ts +6 -0
- package/src/internal/devices/interface.ts +1 -1
- package/src/internal/devices/manager.test.ts +3 -3
- package/src/internal/devices/manager.ts +1 -1
- package/src/internal/download/cryptoService.ts +2 -2
- package/src/internal/download/fileDownloader.test.ts +3 -1
- package/src/internal/download/fileDownloader.ts +2 -2
- package/src/internal/events/apiService.ts +1 -1
- package/src/internal/events/coreEventManager.test.ts +21 -27
- package/src/internal/events/coreEventManager.ts +1 -1
- package/src/internal/events/index.ts +3 -4
- package/src/internal/events/interface.ts +4 -0
- package/src/internal/nodes/apiService.test.ts +35 -1
- package/src/internal/nodes/apiService.ts +103 -17
- package/src/internal/nodes/cache.ts +3 -1
- package/src/internal/nodes/cryptoReporter.ts +145 -0
- package/src/internal/nodes/cryptoService.test.ts +11 -9
- package/src/internal/nodes/cryptoService.ts +45 -138
- package/src/internal/nodes/errors.ts +5 -0
- package/src/internal/nodes/index.test.ts +1 -1
- package/src/internal/nodes/index.ts +3 -1
- package/src/internal/nodes/interface.ts +6 -2
- package/src/internal/nodes/nodesAccess.test.ts +68 -8
- package/src/internal/nodes/nodesAccess.ts +101 -76
- package/src/internal/nodes/nodesManagement.test.ts +100 -5
- package/src/internal/nodes/nodesManagement.ts +100 -13
- package/src/internal/photos/albums.ts +31 -12
- package/src/internal/photos/apiService.ts +159 -4
- package/src/internal/photos/index.ts +54 -9
- package/src/internal/photos/interface.ts +23 -1
- package/src/internal/photos/shares.ts +134 -0
- package/src/internal/photos/timeline.ts +24 -0
- package/src/internal/shares/manager.test.ts +7 -7
- package/src/internal/shares/manager.ts +4 -4
- package/src/internal/sharing/cache.ts +19 -2
- package/src/internal/sharing/interface.ts +2 -2
- package/src/internal/sharing/sharingAccess.test.ts +283 -35
- package/src/internal/sharing/sharingAccess.ts +7 -1
- package/src/internal/sharingPublic/apiService.ts +11 -2
- package/src/internal/sharingPublic/cryptoService.ts +71 -135
- package/src/internal/sharingPublic/index.ts +3 -2
- package/src/internal/sharingPublic/interface.ts +8 -53
- package/src/internal/sharingPublic/manager.ts +9 -8
- package/src/internal/upload/streamUploader.test.ts +3 -1
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/protonDriveClient.ts +34 -4
- package/src/protonDrivePhotosClient.ts +211 -32
- package/src/protonDrivePublicLinkClient.ts +26 -12
- package/dist/internal/photos/cache.d.ts +0 -6
- package/dist/internal/photos/cache.js +0 -15
- package/dist/internal/photos/cache.js.map +0 -1
- package/dist/internal/photos/photosTimeline.d.ts +0 -10
- package/dist/internal/photos/photosTimeline.js +0 -19
- package/dist/internal/photos/photosTimeline.js.map +0 -1
- package/src/internal/photos/cache.ts +0 -11
- package/src/internal/photos/photosTimeline.ts +0 -17
|
@@ -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
|
-
|
|
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;
|
|
@@ -983,7 +985,7 @@ describe('nodesCryptoService', () => {
|
|
|
983
985
|
});
|
|
984
986
|
});
|
|
985
987
|
|
|
986
|
-
describe('
|
|
988
|
+
describe('encryptNodeWithNewParent', () => {
|
|
987
989
|
it('should encrypt node data for move operation', async () => {
|
|
988
990
|
const node = {
|
|
989
991
|
name: { ok: true, value: 'testFile.txt' },
|
|
@@ -1010,7 +1012,7 @@ describe('nodesCryptoService', () => {
|
|
|
1010
1012
|
armoredPassphraseSignature: 'passphraseSignature',
|
|
1011
1013
|
});
|
|
1012
1014
|
|
|
1013
|
-
const result = await cryptoService.
|
|
1015
|
+
const result = await cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, address);
|
|
1014
1016
|
|
|
1015
1017
|
expect(result).toEqual({
|
|
1016
1018
|
encryptedName: 'encryptedNodeName',
|
|
@@ -1054,9 +1056,9 @@ describe('nodesCryptoService', () => {
|
|
|
1054
1056
|
addressKey: 'addressKey' as any,
|
|
1055
1057
|
};
|
|
1056
1058
|
|
|
1057
|
-
await expect(
|
|
1058
|
-
|
|
1059
|
-
);
|
|
1059
|
+
await expect(
|
|
1060
|
+
cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, address),
|
|
1061
|
+
).rejects.toThrow('Moving item to a non-folder is not allowed');
|
|
1060
1062
|
});
|
|
1061
1063
|
|
|
1062
1064
|
it('should throw error when node has invalid name', async () => {
|
|
@@ -1077,9 +1079,9 @@ describe('nodesCryptoService', () => {
|
|
|
1077
1079
|
addressKey: 'addressKey' as any,
|
|
1078
1080
|
};
|
|
1079
1081
|
|
|
1080
|
-
await expect(
|
|
1081
|
-
|
|
1082
|
-
);
|
|
1082
|
+
await expect(
|
|
1083
|
+
cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, address),
|
|
1084
|
+
).rejects.toThrow('Cannot move item without a valid name, please rename the item first');
|
|
1083
1085
|
});
|
|
1084
1086
|
});
|
|
1085
1087
|
});
|
|
@@ -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
|
|
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
|
-
|
|
49
|
-
|
|
67
|
+
telemetry: ProtonDriveTelemetry,
|
|
68
|
+
protected driveCrypto: DriveCrypto,
|
|
50
69
|
private account: ProtonDriveAccount,
|
|
51
|
-
private
|
|
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.
|
|
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;
|
|
@@ -582,7 +601,7 @@ export class NodesCryptoService {
|
|
|
582
601
|
};
|
|
583
602
|
}
|
|
584
603
|
|
|
585
|
-
async
|
|
604
|
+
async encryptNodeWithNewParent(
|
|
586
605
|
node: Pick<DecryptedNode, 'name'>,
|
|
587
606
|
keys: { passphrase: string; passphraseSessionKey: SessionKey; nameSessionKey: SessionKey },
|
|
588
607
|
parentKeys: { key: PrivateKey; hashKey: Uint8Array },
|
|
@@ -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(
|
|
@@ -52,7 +52,7 @@ describe('nodesModules integration tests', () => {
|
|
|
52
52
|
driveCrypto = {};
|
|
53
53
|
// @ts-expect-error No need to implement all methods for mocking
|
|
54
54
|
sharesService = {
|
|
55
|
-
|
|
55
|
+
getOwnVolumeIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
nodesModule = initNodesModule(
|
|
@@ -10,6 +10,7 @@ import { NodeAPIService } from './apiService';
|
|
|
10
10
|
import { NodesCache } from './cache';
|
|
11
11
|
import { NodesCryptoCache } from './cryptoCache';
|
|
12
12
|
import { NodesCryptoService } from './cryptoService';
|
|
13
|
+
import { NodesCryptoReporter } from './cryptoReporter';
|
|
13
14
|
import { SharesService } from './interface';
|
|
14
15
|
import { NodesAccess } from './nodesAccess';
|
|
15
16
|
import { NodesManagement } from './nodesManagement';
|
|
@@ -40,7 +41,8 @@ export function initNodesModule(
|
|
|
40
41
|
const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService);
|
|
41
42
|
const cache = new NodesCache(telemetry.getLogger('nodes-cache'), driveEntitiesCache);
|
|
42
43
|
const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
|
|
43
|
-
const
|
|
44
|
+
const cryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
|
|
45
|
+
const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
|
|
44
46
|
const nodesAccess = new NodesAccess(
|
|
45
47
|
telemetry.getLogger('nodes'),
|
|
46
48
|
api,
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
RevisionState,
|
|
13
13
|
} from '../../interface';
|
|
14
14
|
|
|
15
|
+
export type FilterOptions = {
|
|
16
|
+
type?: NodeType;
|
|
17
|
+
};
|
|
18
|
+
|
|
15
19
|
/**
|
|
16
20
|
* Internal common node interface for both encrypted or decrypted node.
|
|
17
21
|
*/
|
|
@@ -56,7 +60,7 @@ export interface EncryptedNodeCrypto {
|
|
|
56
60
|
nameSignatureEmail?: string;
|
|
57
61
|
armoredKey: string;
|
|
58
62
|
armoredNodePassphrase: string;
|
|
59
|
-
armoredNodePassphraseSignature
|
|
63
|
+
armoredNodePassphraseSignature?: string;
|
|
60
64
|
membership?: {
|
|
61
65
|
inviterEmail: string;
|
|
62
66
|
base64MemberSharePassphraseKeyPacket: string;
|
|
@@ -172,7 +176,7 @@ export interface DecryptedRevision extends Revision {
|
|
|
172
176
|
* Interface describing the dependencies to the shares module.
|
|
173
177
|
*/
|
|
174
178
|
export interface SharesService {
|
|
175
|
-
|
|
179
|
+
getOwnVolumeIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
|
|
176
180
|
getSharePrivateKey(shareId: string): Promise<PrivateKey>;
|
|
177
181
|
getMyFilesShareMemberEmailKey(): Promise<{
|
|
178
182
|
email: string;
|
|
@@ -46,7 +46,7 @@ describe('nodesAccess', () => {
|
|
|
46
46
|
};
|
|
47
47
|
// @ts-expect-error No need to implement all methods for mocking
|
|
48
48
|
shareService = {
|
|
49
|
-
|
|
49
|
+
getOwnVolumeIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
|
|
50
50
|
getSharePrivateKey: jest.fn(),
|
|
51
51
|
};
|
|
52
52
|
|
|
@@ -209,7 +209,12 @@ describe('nodesAccess', () => {
|
|
|
209
209
|
|
|
210
210
|
const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
|
|
211
211
|
expect(result).toMatchObject([node1, node4, node2, node3]);
|
|
212
|
-
expect(apiService.iterateNodes).toHaveBeenCalledWith(
|
|
212
|
+
expect(apiService.iterateNodes).toHaveBeenCalledWith(
|
|
213
|
+
[node2.uid, node3.uid],
|
|
214
|
+
'volumeId',
|
|
215
|
+
undefined, // filterOptions
|
|
216
|
+
undefined, // signal
|
|
217
|
+
);
|
|
213
218
|
expect(cryptoService.decryptNode).toHaveBeenCalledTimes(2);
|
|
214
219
|
expect(cache.setNode).toHaveBeenCalledTimes(2);
|
|
215
220
|
expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(2);
|
|
@@ -226,7 +231,11 @@ describe('nodesAccess', () => {
|
|
|
226
231
|
|
|
227
232
|
const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
|
|
228
233
|
expect(result).toMatchObject([node1, node2, node3, node4]);
|
|
229
|
-
expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith(
|
|
234
|
+
expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith(
|
|
235
|
+
'volumeId~parentNodeid',
|
|
236
|
+
false, // onlyFolders
|
|
237
|
+
undefined, // signal
|
|
238
|
+
);
|
|
230
239
|
expect(apiService.iterateNodes).not.toHaveBeenCalled();
|
|
231
240
|
expect(cache.setFolderChildrenLoaded).toHaveBeenCalledWith('volumeId~parentNodeid');
|
|
232
241
|
});
|
|
@@ -247,11 +256,16 @@ describe('nodesAccess', () => {
|
|
|
247
256
|
|
|
248
257
|
const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
|
|
249
258
|
expect(result).toMatchObject([node1, node2, node3, node4]);
|
|
250
|
-
expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith(
|
|
259
|
+
expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith(
|
|
260
|
+
'volumeId~parentNodeid',
|
|
261
|
+
false, // onlyFolders
|
|
262
|
+
undefined, // signal
|
|
263
|
+
);
|
|
251
264
|
expect(apiService.iterateNodes).toHaveBeenCalledWith(
|
|
252
265
|
['volumeId~node1', 'volumeId~node2', 'volumeId~node3', 'volumeId~node4'],
|
|
253
266
|
'volumeId',
|
|
254
|
-
undefined,
|
|
267
|
+
undefined, // filterOptions
|
|
268
|
+
undefined, // signal
|
|
255
269
|
);
|
|
256
270
|
expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
|
|
257
271
|
expect(cache.setNode).toHaveBeenCalledTimes(4);
|
|
@@ -320,6 +334,50 @@ describe('nodesAccess', () => {
|
|
|
320
334
|
expect(error.cause).toEqual([new DecryptionError('Decryption failed')]);
|
|
321
335
|
}
|
|
322
336
|
});
|
|
337
|
+
|
|
338
|
+
it('should return only filtered nodes from cache', async () => {
|
|
339
|
+
cache.isFolderChildrenLoaded = jest.fn().mockResolvedValue(true);
|
|
340
|
+
cache.iterateChildren = jest.fn().mockImplementation(async function* () {
|
|
341
|
+
yield { ok: true, node: { ...node1, type: NodeType.Folder } };
|
|
342
|
+
yield { ok: true, node: { ...node2, type: NodeType.Folder } };
|
|
343
|
+
yield { ok: true, node: { ...node3, type: NodeType.File } };
|
|
344
|
+
yield { ok: true, node: { ...node4, type: NodeType.File } };
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const result = await Array.fromAsync(
|
|
348
|
+
access.iterateFolderChildren('volumeId~parentNodeid', { type: NodeType.Folder }),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
expect(result).toMatchObject([node1, node2]);
|
|
352
|
+
expect(cache.setFolderChildrenLoaded).not.toHaveBeenCalled();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it.only('should return only filtered nodes from API', async () => {
|
|
356
|
+
cache.isFolderChildrenLoaded = jest.fn().mockResolvedValue(false);
|
|
357
|
+
cache.getNode = jest.fn().mockImplementation((uid: string) => {
|
|
358
|
+
if (uid === parentNode.uid) {
|
|
359
|
+
return parentNode;
|
|
360
|
+
}
|
|
361
|
+
throw new Error('Entity not found');
|
|
362
|
+
});
|
|
363
|
+
apiService.iterateChildrenNodeUids = jest.fn().mockImplementation(async function* () {
|
|
364
|
+
yield 'volumeId~node1';
|
|
365
|
+
yield 'volumeId~node2';
|
|
366
|
+
yield 'volumeId~node3';
|
|
367
|
+
yield 'volumeId~node4';
|
|
368
|
+
});
|
|
369
|
+
apiService.iterateNodes = jest.fn().mockImplementation(async function* () {
|
|
370
|
+
yield { ...node1, parentUid: 'volumeId~parentNodeId', type: NodeType.Folder };
|
|
371
|
+
yield { ...node2, parentUid: 'volumeId~parentNodeId', type: NodeType.Folder };
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const result = await Array.fromAsync(
|
|
375
|
+
access.iterateFolderChildren('volumeId~parentNodeid', { type: NodeType.Folder }),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
expect(result).toMatchObject([node1, node2]);
|
|
379
|
+
expect(cache.setFolderChildrenLoaded).not.toHaveBeenCalled();
|
|
380
|
+
});
|
|
323
381
|
});
|
|
324
382
|
|
|
325
383
|
describe('iterateTrashedNodes', () => {
|
|
@@ -330,7 +388,7 @@ describe('nodesAccess', () => {
|
|
|
330
388
|
const node4 = { uid: 'volumeId~node4', isStale: false } as DecryptedNode;
|
|
331
389
|
|
|
332
390
|
beforeEach(() => {
|
|
333
|
-
shareService.
|
|
391
|
+
shareService.getOwnVolumeIDs = jest.fn().mockResolvedValue({ volumeId });
|
|
334
392
|
apiService.iterateTrashedNodeUids = jest.fn().mockImplementation(async function* () {
|
|
335
393
|
yield node1.uid;
|
|
336
394
|
yield node2.uid;
|
|
@@ -359,7 +417,8 @@ describe('nodesAccess', () => {
|
|
|
359
417
|
expect(apiService.iterateNodes).toHaveBeenCalledWith(
|
|
360
418
|
['volumeId~node1', 'volumeId~node2', 'volumeId~node3', 'volumeId~node4'],
|
|
361
419
|
volumeId,
|
|
362
|
-
undefined,
|
|
420
|
+
undefined, // filterOptions
|
|
421
|
+
undefined, // signal
|
|
363
422
|
);
|
|
364
423
|
expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
|
|
365
424
|
expect(cache.setNode).toHaveBeenCalledTimes(4);
|
|
@@ -417,7 +476,8 @@ describe('nodesAccess', () => {
|
|
|
417
476
|
expect(apiService.iterateNodes).toHaveBeenCalledWith(
|
|
418
477
|
['volumeId~node2', 'volumeId~node3'],
|
|
419
478
|
'volumeId',
|
|
420
|
-
undefined,
|
|
479
|
+
undefined, // filterOptions
|
|
480
|
+
undefined, // signal
|
|
421
481
|
);
|
|
422
482
|
});
|
|
423
483
|
|