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