@protontech/drive-sdk 0.6.2 → 0.7.1
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/interface/index.d.ts +1 -0
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +14 -10
- package/dist/interface/nodes.js +5 -8
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/photos.d.ts +62 -0
- package/dist/interface/photos.js +3 -0
- package/dist/interface/photos.js.map +1 -0
- package/dist/internal/apiService/apiService.d.ts +2 -2
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +1294 -517
- package/dist/internal/apiService/errors.js +4 -3
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/download/cryptoService.js +8 -6
- package/dist/internal/download/cryptoService.js.map +1 -1
- package/dist/internal/download/fileDownloader.d.ts +2 -1
- package/dist/internal/download/fileDownloader.js +6 -3
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/index.d.ts +1 -1
- package/dist/internal/download/index.js +3 -3
- package/dist/internal/download/index.js.map +1 -1
- package/dist/internal/errors.d.ts +1 -0
- package/dist/internal/errors.js +4 -0
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/nodes/apiService.d.ts +68 -16
- package/dist/internal/nodes/apiService.js +138 -85
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +7 -5
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.d.ts +16 -8
- package/dist/internal/nodes/cache.js +19 -5
- package/dist/internal/nodes/cache.js.map +1 -1
- package/dist/internal/nodes/cache.test.js +1 -0
- package/dist/internal/nodes/cache.test.js.map +1 -1
- package/dist/internal/nodes/cryptoReporter.d.ts +3 -3
- package/dist/internal/nodes/cryptoReporter.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +13 -22
- package/dist/internal/nodes/cryptoService.js +47 -16
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +262 -17
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/events.d.ts +2 -2
- package/dist/internal/nodes/events.js.map +1 -1
- package/dist/internal/nodes/index.test.js +1 -0
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +14 -3
- package/dist/internal/nodes/nodesAccess.d.ts +36 -20
- package/dist/internal/nodes/nodesAccess.js +54 -29
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +34 -14
- package/dist/internal/nodes/nodesManagement.js +44 -31
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +60 -14
- package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
- package/dist/internal/nodes/nodesRevisions.d.ts +2 -2
- package/dist/internal/nodes/nodesRevisions.js.map +1 -1
- package/dist/internal/photos/albums.d.ts +2 -2
- package/dist/internal/photos/albums.js.map +1 -1
- package/dist/internal/photos/index.d.ts +19 -3
- package/dist/internal/photos/index.js +38 -8
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +18 -9
- package/dist/internal/photos/nodes.d.ts +57 -0
- package/dist/internal/photos/nodes.js +165 -0
- package/dist/internal/photos/nodes.js.map +1 -0
- package/dist/internal/photos/timeline.d.ts +2 -2
- package/dist/internal/photos/timeline.js.map +1 -1
- package/dist/internal/photos/timeline.test.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +2 -2
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/sharingPublic/index.d.ts +6 -6
- package/dist/internal/sharingPublic/index.js +8 -7
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +16 -3
- package/dist/internal/sharingPublic/nodes.js +34 -2
- package/dist/internal/sharingPublic/nodes.js.map +1 -1
- package/dist/internal/sharingPublic/unauthApiService.d.ts +17 -0
- package/dist/internal/sharingPublic/unauthApiService.js +31 -0
- package/dist/internal/sharingPublic/unauthApiService.js.map +1 -0
- package/dist/internal/sharingPublic/unauthApiService.test.d.ts +1 -0
- package/dist/internal/sharingPublic/unauthApiService.test.js +27 -0
- package/dist/internal/sharingPublic/unauthApiService.test.js.map +1 -0
- package/dist/internal/upload/apiService.d.ts +4 -3
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +8 -3
- package/dist/internal/upload/cryptoService.js +45 -9
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/fileUploader.test.js +1 -1
- package/dist/internal/upload/fileUploader.test.js.map +1 -1
- package/dist/internal/upload/interface.d.ts +25 -13
- package/dist/internal/upload/manager.js +7 -4
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +5 -4
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/streamUploader.js +9 -4
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +8 -5
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +11 -2
- package/dist/protonDriveClient.js +20 -4
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +8 -8
- package/dist/protonDrivePhotosClient.js +8 -9
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +9 -2
- package/dist/protonDrivePublicLinkClient.js +16 -5
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/transformers.d.ts +7 -2
- package/dist/transformers.js +37 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/interface/index.ts +1 -0
- package/src/interface/nodes.ts +14 -11
- package/src/interface/photos.ts +67 -0
- package/src/internal/apiService/apiService.ts +2 -2
- package/src/internal/apiService/driveTypes.ts +1294 -517
- package/src/internal/apiService/errors.ts +5 -4
- package/src/internal/download/cryptoService.ts +13 -6
- package/src/internal/download/fileDownloader.ts +4 -2
- package/src/internal/download/index.ts +3 -0
- package/src/internal/errors.ts +4 -0
- package/src/internal/nodes/apiService.test.ts +7 -5
- package/src/internal/nodes/apiService.ts +210 -124
- package/src/internal/nodes/cache.test.ts +1 -0
- package/src/internal/nodes/cache.ts +32 -13
- package/src/internal/nodes/cryptoReporter.ts +3 -3
- package/src/internal/nodes/cryptoService.test.ts +380 -18
- package/src/internal/nodes/cryptoService.ts +77 -36
- package/src/internal/nodes/events.ts +2 -2
- package/src/internal/nodes/index.test.ts +1 -0
- package/src/internal/nodes/interface.ts +17 -2
- package/src/internal/nodes/nodesAccess.ts +99 -54
- package/src/internal/nodes/nodesManagement.test.ts +69 -14
- package/src/internal/nodes/nodesManagement.ts +94 -48
- package/src/internal/nodes/nodesRevisions.ts +3 -3
- package/src/internal/photos/albums.ts +2 -2
- package/src/internal/photos/index.ts +45 -3
- package/src/internal/photos/interface.ts +21 -9
- package/src/internal/photos/nodes.ts +233 -0
- package/src/internal/photos/timeline.test.ts +2 -2
- package/src/internal/photos/timeline.ts +2 -2
- package/src/internal/photos/upload.ts +3 -3
- package/src/internal/sharingPublic/index.ts +7 -3
- package/src/internal/sharingPublic/nodes.ts +43 -2
- package/src/internal/sharingPublic/unauthApiService.test.ts +29 -0
- package/src/internal/sharingPublic/unauthApiService.ts +32 -0
- package/src/internal/upload/apiService.ts +4 -3
- package/src/internal/upload/cryptoService.ts +73 -12
- package/src/internal/upload/fileUploader.test.ts +1 -1
- package/src/internal/upload/interface.ts +24 -13
- package/src/internal/upload/manager.test.ts +5 -4
- package/src/internal/upload/manager.ts +7 -4
- package/src/internal/upload/streamUploader.test.ts +8 -5
- package/src/internal/upload/streamUploader.ts +10 -4
- package/src/protonDriveClient.ts +27 -5
- package/src/protonDrivePhotosClient.ts +23 -23
- package/src/protonDrivePublicLinkClient.ts +19 -3
- package/src/transformers.ts +49 -2
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
DecryptedNodeKeys,
|
|
25
25
|
EncryptedRevision,
|
|
26
26
|
DecryptedUnparsedRevision,
|
|
27
|
+
NodeSigningKeys,
|
|
27
28
|
} from './interface';
|
|
28
29
|
|
|
29
30
|
export interface NodesCryptoReporter {
|
|
@@ -33,7 +34,7 @@ export interface NodesCryptoReporter {
|
|
|
33
34
|
signatureType: string,
|
|
34
35
|
verified: VERIFICATION_STATUS,
|
|
35
36
|
verificationErrors?: Error[],
|
|
36
|
-
claimedAuthor?: string,
|
|
37
|
+
claimedAuthor?: string | AnonymousUser,
|
|
37
38
|
notAvailableVerificationKeys?: boolean,
|
|
38
39
|
): Promise<Author>;
|
|
39
40
|
reportDecryptionError(node: NodesCryptoReporterNode, field: MetricsDecryptionErrorField, error: unknown): void;
|
|
@@ -336,11 +337,19 @@ export class NodesCryptoService {
|
|
|
336
337
|
const nameSignatureEmail = node.encryptedCrypto.nameSignatureEmail;
|
|
337
338
|
|
|
338
339
|
try {
|
|
339
|
-
const {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
);
|
|
340
|
+
const {
|
|
341
|
+
name,
|
|
342
|
+
verified: verificationStatus,
|
|
343
|
+
verificationErrors,
|
|
344
|
+
} = await this.driveCrypto.decryptNodeName(node.encryptedName, parentKey, verificationKeys);
|
|
345
|
+
|
|
346
|
+
let verified = verificationStatus;
|
|
347
|
+
// The name was not signed until Drive web Beta 3.
|
|
348
|
+
// It is decided to ignore this and consider it signed.
|
|
349
|
+
// The problem will be gone with migration to new crypto model.
|
|
350
|
+
if (verificationStatus === VERIFICATION_STATUS.NOT_SIGNED && node.creationTime < new Date(2021, 0, 1)) {
|
|
351
|
+
verified = VERIFICATION_STATUS.SIGNED_AND_VALID;
|
|
352
|
+
}
|
|
344
353
|
|
|
345
354
|
return {
|
|
346
355
|
name: resultOk(name),
|
|
@@ -438,11 +447,19 @@ export class NodesCryptoService {
|
|
|
438
447
|
throw new Error('Node is not a folder');
|
|
439
448
|
}
|
|
440
449
|
|
|
441
|
-
const {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
);
|
|
450
|
+
const {
|
|
451
|
+
hashKey,
|
|
452
|
+
verified: verificationStatus,
|
|
453
|
+
verificationErrors,
|
|
454
|
+
} = await this.driveCrypto.decryptNodeHashKey(node.encryptedCrypto.folder.armoredHashKey, nodeKey, addressKeys);
|
|
455
|
+
|
|
456
|
+
let verified = verificationStatus;
|
|
457
|
+
// The hash was not signed until Drive web Beta 17.
|
|
458
|
+
// It is decided to ignore this and consider it signed.
|
|
459
|
+
// The problem will be gone with migration to new crypto model.
|
|
460
|
+
if (verificationStatus === VERIFICATION_STATUS.NOT_SIGNED && node.creationTime < new Date(2021, 7, 1)) {
|
|
461
|
+
verified = VERIFICATION_STATUS.SIGNED_AND_VALID;
|
|
462
|
+
}
|
|
446
463
|
|
|
447
464
|
return {
|
|
448
465
|
hashKey,
|
|
@@ -490,7 +507,7 @@ export class NodesCryptoService {
|
|
|
490
507
|
encryptedExtendedAttributes: string | undefined,
|
|
491
508
|
nodeKey: PrivateKey,
|
|
492
509
|
addressKeys: PublicKey[],
|
|
493
|
-
signatureEmail?: string,
|
|
510
|
+
signatureEmail?: string | AnonymousUser,
|
|
494
511
|
): Promise<{
|
|
495
512
|
extendedAttributes?: string;
|
|
496
513
|
author: Author;
|
|
@@ -522,31 +539,44 @@ export class NodesCryptoService {
|
|
|
522
539
|
|
|
523
540
|
async createFolder(
|
|
524
541
|
parentKeys: { key: PrivateKey; hashKey: Uint8Array },
|
|
525
|
-
|
|
542
|
+
signingKeys: NodeSigningKeys,
|
|
526
543
|
name: string,
|
|
527
544
|
extendedAttributes?: string,
|
|
528
545
|
): Promise<{
|
|
529
|
-
encryptedCrypto: EncryptedNodeFolderCrypto & {
|
|
546
|
+
encryptedCrypto: Omit<EncryptedNodeFolderCrypto, 'signatureEmail' | 'nameSignatureEmail'> & {
|
|
547
|
+
signatureEmail: string | AnonymousUser;
|
|
548
|
+
nameSignatureEmail: string | AnonymousUser;
|
|
530
549
|
armoredNodePassphraseSignature: string;
|
|
531
|
-
// signatureEmail and nameSignatureEmail are not optional.
|
|
532
|
-
signatureEmail: string;
|
|
533
|
-
nameSignatureEmail: string;
|
|
534
550
|
encryptedName: string;
|
|
535
551
|
hash: string;
|
|
536
552
|
};
|
|
537
553
|
keys: DecryptedNodeKeys;
|
|
538
554
|
}> {
|
|
539
|
-
const
|
|
555
|
+
const email = signingKeys.type === 'userAddress' ? signingKeys.email : null;
|
|
556
|
+
const nameAndPassprhaseSigningKey =
|
|
557
|
+
signingKeys.type === 'userAddress' ? signingKeys.key : signingKeys.parentNodeKey;
|
|
558
|
+
if (!nameAndPassprhaseSigningKey) {
|
|
559
|
+
// This is a bug within the SDK.
|
|
560
|
+
throw new Error('Cannot create new node without a name and passphrase signing key');
|
|
561
|
+
}
|
|
562
|
+
|
|
540
563
|
const [nodeKeys, { armoredNodeName }, hash] = await Promise.all([
|
|
541
|
-
this.driveCrypto.generateKey([parentKeys.key],
|
|
542
|
-
this.driveCrypto.encryptNodeName(name, undefined, parentKeys.key,
|
|
564
|
+
this.driveCrypto.generateKey([parentKeys.key], nameAndPassprhaseSigningKey),
|
|
565
|
+
this.driveCrypto.encryptNodeName(name, undefined, parentKeys.key, nameAndPassprhaseSigningKey),
|
|
543
566
|
this.driveCrypto.generateLookupHash(name, parentKeys.hashKey),
|
|
544
567
|
]);
|
|
545
568
|
|
|
546
569
|
const { armoredHashKey, hashKey } = await this.driveCrypto.generateHashKey(nodeKeys.decrypted.key);
|
|
547
570
|
|
|
571
|
+
const extendedAttributesSigningKey =
|
|
572
|
+
signingKeys.type === 'userAddress' ? signingKeys.key : signingKeys.nodeKey || nodeKeys.decrypted.key;
|
|
573
|
+
|
|
548
574
|
const { armoredExtendedAttributes } = extendedAttributes
|
|
549
|
-
? await this.driveCrypto.encryptExtendedAttributes(
|
|
575
|
+
? await this.driveCrypto.encryptExtendedAttributes(
|
|
576
|
+
extendedAttributes,
|
|
577
|
+
nodeKeys.decrypted.key,
|
|
578
|
+
extendedAttributesSigningKey,
|
|
579
|
+
)
|
|
550
580
|
: { armoredExtendedAttributes: undefined };
|
|
551
581
|
|
|
552
582
|
return {
|
|
@@ -575,20 +605,25 @@ export class NodesCryptoService {
|
|
|
575
605
|
async encryptNewName(
|
|
576
606
|
parentKeys: { key: PrivateKey; hashKey?: Uint8Array },
|
|
577
607
|
nodeNameSessionKey: SessionKey,
|
|
578
|
-
|
|
608
|
+
signingKeys: NodeSigningKeys,
|
|
579
609
|
newName: string,
|
|
580
610
|
): Promise<{
|
|
581
|
-
signatureEmail: string;
|
|
611
|
+
signatureEmail: string | AnonymousUser;
|
|
582
612
|
armoredNodeName: string;
|
|
583
613
|
hash?: string;
|
|
584
614
|
}> {
|
|
585
|
-
const
|
|
615
|
+
const email = signingKeys.type === 'userAddress' ? signingKeys.email : null;
|
|
616
|
+
const nameSigningKey = signingKeys.type === 'userAddress' ? signingKeys.key : signingKeys.parentNodeKey;
|
|
617
|
+
if (!nameSigningKey) {
|
|
618
|
+
// This is a bug within the SDK.
|
|
619
|
+
throw new Error('Cannot encrypt new node name without a name signing key');
|
|
620
|
+
}
|
|
586
621
|
|
|
587
622
|
const { armoredNodeName } = await this.driveCrypto.encryptNodeName(
|
|
588
623
|
newName,
|
|
589
624
|
nodeNameSessionKey,
|
|
590
625
|
parentKeys.key,
|
|
591
|
-
|
|
626
|
+
nameSigningKey,
|
|
592
627
|
);
|
|
593
628
|
|
|
594
629
|
const hash = parentKeys.hashKey
|
|
@@ -602,38 +637,44 @@ export class NodesCryptoService {
|
|
|
602
637
|
}
|
|
603
638
|
|
|
604
639
|
async encryptNodeWithNewParent(
|
|
605
|
-
|
|
640
|
+
nodeName: DecryptedNode['name'],
|
|
606
641
|
keys: { passphrase: string; passphraseSessionKey: SessionKey; nameSessionKey: SessionKey },
|
|
607
642
|
parentKeys: { key: PrivateKey; hashKey: Uint8Array },
|
|
608
|
-
|
|
643
|
+
signingKeys: NodeSigningKeys,
|
|
609
644
|
): Promise<{
|
|
610
645
|
encryptedName: string;
|
|
611
646
|
hash: string;
|
|
612
647
|
armoredNodePassphrase: string;
|
|
613
648
|
armoredNodePassphraseSignature: string;
|
|
614
|
-
signatureEmail: string;
|
|
615
|
-
nameSignatureEmail: string;
|
|
649
|
+
signatureEmail: string | AnonymousUser;
|
|
650
|
+
nameSignatureEmail: string | AnonymousUser;
|
|
616
651
|
}> {
|
|
617
652
|
if (!parentKeys.hashKey) {
|
|
618
653
|
throw new ValidationError('Moving item to a non-folder is not allowed');
|
|
619
654
|
}
|
|
620
|
-
if (!
|
|
655
|
+
if (!nodeName.ok) {
|
|
621
656
|
throw new ValidationError('Cannot move item without a valid name, please rename the item first');
|
|
622
657
|
}
|
|
623
658
|
|
|
624
|
-
const
|
|
659
|
+
const email = signingKeys.type === 'userAddress' ? signingKeys.email : null;
|
|
660
|
+
const nameAndPassprhaseSigningKey = signingKeys.type === 'userAddress' ? signingKeys.key : signingKeys.nodeKey;
|
|
661
|
+
if (!nameAndPassprhaseSigningKey) {
|
|
662
|
+
// This is a bug within the SDK.
|
|
663
|
+
throw new Error('Cannot re-encrypt node without a name and passphrase signing key');
|
|
664
|
+
}
|
|
665
|
+
|
|
625
666
|
const { armoredNodeName } = await this.driveCrypto.encryptNodeName(
|
|
626
|
-
|
|
667
|
+
nodeName.value,
|
|
627
668
|
keys.nameSessionKey,
|
|
628
669
|
parentKeys.key,
|
|
629
|
-
|
|
670
|
+
nameAndPassprhaseSigningKey,
|
|
630
671
|
);
|
|
631
|
-
const hash = await this.driveCrypto.generateLookupHash(
|
|
672
|
+
const hash = await this.driveCrypto.generateLookupHash(nodeName.value, parentKeys.hashKey);
|
|
632
673
|
const { armoredPassphrase, armoredPassphraseSignature } = await this.driveCrypto.encryptPassphrase(
|
|
633
674
|
keys.passphrase,
|
|
634
675
|
keys.passphraseSessionKey,
|
|
635
676
|
[parentKeys.key],
|
|
636
|
-
|
|
677
|
+
nameAndPassprhaseSigningKey,
|
|
637
678
|
);
|
|
638
679
|
|
|
639
680
|
return {
|
|
@@ -657,7 +698,7 @@ export class NodesCryptoService {
|
|
|
657
698
|
}
|
|
658
699
|
|
|
659
700
|
function getClaimedAuthor(
|
|
660
|
-
claimedAuthor?: string,
|
|
701
|
+
claimedAuthor?: string | AnonymousUser,
|
|
661
702
|
notAvailableVerificationKeys = false,
|
|
662
703
|
): string | AnonymousUser | undefined {
|
|
663
704
|
if (!claimedAuthor && notAvailableVerificationKeys) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Logger } from '../../interface';
|
|
2
2
|
import { DriveEvent, DriveEventType } from '../events';
|
|
3
|
-
import {
|
|
3
|
+
import { NodesCacheBase } from './cache';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Provides internal event handling.
|
|
@@ -11,7 +11,7 @@ import { NodesCache } from './cache';
|
|
|
11
11
|
export class NodesEventsHandler {
|
|
12
12
|
constructor(
|
|
13
13
|
private logger: Logger,
|
|
14
|
-
private cache:
|
|
14
|
+
private cache: NodesCacheBase,
|
|
15
15
|
) {}
|
|
16
16
|
|
|
17
17
|
async updateNodesCacheOnEvent(event: DriveEvent): Promise<void> {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
MetricVolumeType,
|
|
11
11
|
Revision,
|
|
12
12
|
RevisionState,
|
|
13
|
+
AnonymousUser,
|
|
13
14
|
} from '../../interface';
|
|
14
15
|
|
|
15
16
|
export type FilterOptions = {
|
|
@@ -32,6 +33,7 @@ interface BaseNode {
|
|
|
32
33
|
type: NodeType;
|
|
33
34
|
mediaType?: string;
|
|
34
35
|
creationTime: Date; // created on the server
|
|
36
|
+
modificationTime: Date; // modified on server
|
|
35
37
|
trashTime?: Date;
|
|
36
38
|
totalStorageSize?: number;
|
|
37
39
|
|
|
@@ -57,8 +59,8 @@ export interface EncryptedNode extends BaseNode {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
export interface EncryptedNodeCrypto {
|
|
60
|
-
signatureEmail?: string;
|
|
61
|
-
nameSignatureEmail?: string;
|
|
62
|
+
signatureEmail?: string | AnonymousUser;
|
|
63
|
+
nameSignatureEmail?: string | AnonymousUser;
|
|
62
64
|
armoredKey: string;
|
|
63
65
|
armoredNodePassphrase: string;
|
|
64
66
|
armoredNodePassphraseSignature?: string;
|
|
@@ -88,6 +90,19 @@ export interface EncryptedNodeFolderCrypto extends EncryptedNodeCrypto {
|
|
|
88
90
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
89
91
|
export interface EncryptedNodeAlbumCrypto extends EncryptedNodeCrypto {}
|
|
90
92
|
|
|
93
|
+
export type NodeSigningKeys =
|
|
94
|
+
| {
|
|
95
|
+
type: 'userAddress';
|
|
96
|
+
email: string;
|
|
97
|
+
addressId: string;
|
|
98
|
+
key: PrivateKey;
|
|
99
|
+
}
|
|
100
|
+
| {
|
|
101
|
+
type: 'nodeKey';
|
|
102
|
+
nodeKey?: PrivateKey;
|
|
103
|
+
parentNodeKey?: PrivateKey;
|
|
104
|
+
};
|
|
105
|
+
|
|
91
106
|
/**
|
|
92
107
|
* Interface used only internally in the nodes module.
|
|
93
108
|
*
|
|
@@ -16,8 +16,8 @@ import { asyncIteratorMap } from '../asyncIteratorMap';
|
|
|
16
16
|
import { getErrorMessage } from '../errors';
|
|
17
17
|
import { BatchLoading } from '../batchLoading';
|
|
18
18
|
import { makeNodeUid, splitNodeUid } from '../uids';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { NodeAPIServiceBase } from './apiService';
|
|
20
|
+
import { NodesCacheBase } from './cache';
|
|
21
21
|
import { NodesCryptoCache } from './cryptoCache';
|
|
22
22
|
import { NodesCryptoService } from './cryptoService';
|
|
23
23
|
import { NodesDebouncer } from './debouncer';
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
DecryptedNode,
|
|
30
30
|
DecryptedNodeKeys,
|
|
31
31
|
FilterOptions,
|
|
32
|
+
NodeSigningKeys,
|
|
32
33
|
} from './interface';
|
|
33
34
|
import { validateNodeName } from './validations';
|
|
34
35
|
import { isProtonDocument, isProtonSheet } from './mediaTypes';
|
|
@@ -49,17 +50,21 @@ const DECRYPTION_CONCURRENCY = 30;
|
|
|
49
50
|
* The node access module is responsible for fetching, decrypting and caching
|
|
50
51
|
* nodes metadata.
|
|
51
52
|
*/
|
|
52
|
-
export class
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
export abstract class NodesAccessBase<
|
|
54
|
+
TEncryptedNode extends EncryptedNode = EncryptedNode,
|
|
55
|
+
TDecryptedNode extends DecryptedNode = DecryptedNode,
|
|
56
|
+
TCryptoService extends NodesCryptoService = NodesCryptoService,
|
|
57
|
+
> {
|
|
58
|
+
protected logger: Logger;
|
|
59
|
+
protected debouncer: NodesDebouncer;
|
|
55
60
|
|
|
56
61
|
constructor(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
protected telemetry: ProtonDriveTelemetry,
|
|
63
|
+
protected apiService: NodeAPIServiceBase<TEncryptedNode>,
|
|
64
|
+
protected cache: NodesCacheBase<TDecryptedNode>,
|
|
65
|
+
protected cryptoCache: NodesCryptoCache,
|
|
66
|
+
protected cryptoService: TCryptoService,
|
|
67
|
+
protected shareService: Pick<
|
|
63
68
|
SharesService,
|
|
64
69
|
'getRootIDs' | 'getSharePrivateKey' | 'getContextShareMemberEmailKey'
|
|
65
70
|
>,
|
|
@@ -79,7 +84,7 @@ export class NodesAccess {
|
|
|
79
84
|
return this.getNode(nodeUid);
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
async getNode(nodeUid: string): Promise<
|
|
87
|
+
async getNode(nodeUid: string): Promise<TDecryptedNode> {
|
|
83
88
|
let cachedNode;
|
|
84
89
|
try {
|
|
85
90
|
await this.debouncer.waitForLoadingNode(nodeUid);
|
|
@@ -100,11 +105,11 @@ export class NodesAccess {
|
|
|
100
105
|
parentNodeUid: string,
|
|
101
106
|
filterOptions?: FilterOptions,
|
|
102
107
|
signal?: AbortSignal,
|
|
103
|
-
): AsyncGenerator<
|
|
108
|
+
): AsyncGenerator<TDecryptedNode> {
|
|
104
109
|
// Ensure the parent is loaded and up-to-date.
|
|
105
110
|
const parentNode = await this.getNode(parentNodeUid);
|
|
106
111
|
|
|
107
|
-
const batchLoading = new BatchLoading<string,
|
|
112
|
+
const batchLoading = new BatchLoading<string, TDecryptedNode>({
|
|
108
113
|
iterateItems: (nodeUids) => this.loadNodes(nodeUids, filterOptions, signal),
|
|
109
114
|
batchSize: BATCH_LOADING_SIZE,
|
|
110
115
|
});
|
|
@@ -153,9 +158,9 @@ export class NodesAccess {
|
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
// Improvement requested: keep status of loaded trash and leverage cache.
|
|
156
|
-
async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<
|
|
161
|
+
async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<TDecryptedNode> {
|
|
157
162
|
const { volumeId } = await this.shareService.getRootIDs();
|
|
158
|
-
const batchLoading = new BatchLoading<string,
|
|
163
|
+
const batchLoading = new BatchLoading<string, TDecryptedNode>({
|
|
159
164
|
iterateItems: (nodeUids) => this.loadNodes(nodeUids, undefined, signal),
|
|
160
165
|
batchSize: BATCH_LOADING_SIZE,
|
|
161
166
|
});
|
|
@@ -176,8 +181,8 @@ export class NodesAccess {
|
|
|
176
181
|
yield* batchLoading.loadRest();
|
|
177
182
|
}
|
|
178
183
|
|
|
179
|
-
async *iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<
|
|
180
|
-
const batchLoading = new BatchLoading<string,
|
|
184
|
+
async *iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<TDecryptedNode | MissingNode> {
|
|
185
|
+
const batchLoading = new BatchLoading<string, TDecryptedNode | MissingNode>({
|
|
181
186
|
iterateItems: (nodeUids) => this.loadNodesWithMissingReport(nodeUids, undefined, signal),
|
|
182
187
|
batchSize: BATCH_LOADING_SIZE,
|
|
183
188
|
});
|
|
@@ -227,7 +232,7 @@ export class NodesAccess {
|
|
|
227
232
|
await this.cache.removeNodes([nodeUid]);
|
|
228
233
|
}
|
|
229
234
|
|
|
230
|
-
private async loadNode(nodeUid: string): Promise<{ node:
|
|
235
|
+
private async loadNode(nodeUid: string): Promise<{ node: TDecryptedNode; keys?: DecryptedNodeKeys }> {
|
|
231
236
|
this.debouncer.loadingNode(nodeUid);
|
|
232
237
|
try {
|
|
233
238
|
const { volumeId: ownVolumeId } = await this.shareService.getRootIDs();
|
|
@@ -242,7 +247,7 @@ export class NodesAccess {
|
|
|
242
247
|
nodeUids: string[],
|
|
243
248
|
filterOptions?: FilterOptions,
|
|
244
249
|
signal?: AbortSignal,
|
|
245
|
-
): AsyncGenerator<
|
|
250
|
+
): AsyncGenerator<TDecryptedNode> {
|
|
246
251
|
for await (const result of this.loadNodesWithMissingReport(nodeUids, filterOptions, signal)) {
|
|
247
252
|
if ('missingUid' in result) {
|
|
248
253
|
continue;
|
|
@@ -255,7 +260,7 @@ export class NodesAccess {
|
|
|
255
260
|
nodeUids: string[],
|
|
256
261
|
filterOptions?: FilterOptions,
|
|
257
262
|
signal?: AbortSignal,
|
|
258
|
-
): AsyncGenerator<
|
|
263
|
+
): AsyncGenerator<TDecryptedNode | MissingNode> {
|
|
259
264
|
const returnedNodeUids: string[] = [];
|
|
260
265
|
const errors = [];
|
|
261
266
|
|
|
@@ -263,13 +268,13 @@ export class NodesAccess {
|
|
|
263
268
|
|
|
264
269
|
const apiNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, filterOptions, signal);
|
|
265
270
|
|
|
266
|
-
const debouncedNodeMapper = async (encryptedNode:
|
|
271
|
+
const debouncedNodeMapper = async (encryptedNode: TEncryptedNode): Promise<TEncryptedNode> => {
|
|
267
272
|
this.debouncer.loadingNode(encryptedNode.uid);
|
|
268
273
|
return encryptedNode;
|
|
269
274
|
};
|
|
270
275
|
const encryptedNodesIterator = asyncIteratorMap(apiNodesIterator, debouncedNodeMapper, 1);
|
|
271
276
|
|
|
272
|
-
const decryptNodeMapper = async (encryptedNode:
|
|
277
|
+
const decryptNodeMapper = async (encryptedNode: TEncryptedNode): Promise<Result<TDecryptedNode, unknown>> => {
|
|
273
278
|
returnedNodeUids.push(encryptedNode.uid);
|
|
274
279
|
try {
|
|
275
280
|
const { node } = await this.decryptNode(encryptedNode);
|
|
@@ -309,8 +314,8 @@ export class NodesAccess {
|
|
|
309
314
|
}
|
|
310
315
|
|
|
311
316
|
private async decryptNode(
|
|
312
|
-
encryptedNode:
|
|
313
|
-
): Promise<{ node:
|
|
317
|
+
encryptedNode: TEncryptedNode,
|
|
318
|
+
): Promise<{ node: TDecryptedNode; keys?: DecryptedNodeKeys }> {
|
|
314
319
|
let parentKey;
|
|
315
320
|
try {
|
|
316
321
|
const parentKeys = await this.getParentKeys(encryptedNode);
|
|
@@ -318,38 +323,14 @@ export class NodesAccess {
|
|
|
318
323
|
} catch (error: unknown) {
|
|
319
324
|
if (error instanceof DecryptionError) {
|
|
320
325
|
return {
|
|
321
|
-
node:
|
|
322
|
-
...encryptedNode,
|
|
323
|
-
isStale: false,
|
|
324
|
-
name: resultError(error),
|
|
325
|
-
keyAuthor: resultError({
|
|
326
|
-
claimedAuthor: encryptedNode.encryptedCrypto.signatureEmail,
|
|
327
|
-
error: getErrorMessage(error),
|
|
328
|
-
}),
|
|
329
|
-
nameAuthor: resultError({
|
|
330
|
-
claimedAuthor: encryptedNode.encryptedCrypto.nameSignatureEmail,
|
|
331
|
-
error: getErrorMessage(error),
|
|
332
|
-
}),
|
|
333
|
-
membership: encryptedNode.membership
|
|
334
|
-
? {
|
|
335
|
-
role: encryptedNode.membership.role,
|
|
336
|
-
inviteTime: encryptedNode.membership.inviteTime,
|
|
337
|
-
sharedBy: resultError({
|
|
338
|
-
claimedAuthor: encryptedNode.encryptedCrypto.membership?.inviterEmail,
|
|
339
|
-
error: getErrorMessage(error),
|
|
340
|
-
}),
|
|
341
|
-
}
|
|
342
|
-
: undefined,
|
|
343
|
-
errors: [error],
|
|
344
|
-
treeEventScopeId: splitNodeUid(encryptedNode.uid).volumeId,
|
|
345
|
-
},
|
|
326
|
+
node: this.getDegradedUndecryptableNode(encryptedNode, error),
|
|
346
327
|
};
|
|
347
328
|
}
|
|
348
329
|
throw error;
|
|
349
330
|
}
|
|
350
331
|
|
|
351
332
|
const { node: unparsedNode, keys } = await this.cryptoService.decryptNode(encryptedNode, parentKey);
|
|
352
|
-
const node =
|
|
333
|
+
const node = this.parseNode(unparsedNode);
|
|
353
334
|
try {
|
|
354
335
|
await this.cache.setNode(node);
|
|
355
336
|
} catch (error: unknown) {
|
|
@@ -366,8 +347,45 @@ export class NodesAccess {
|
|
|
366
347
|
return { node, keys };
|
|
367
348
|
}
|
|
368
349
|
|
|
350
|
+
protected abstract getDegradedUndecryptableNode(
|
|
351
|
+
encryptedNode: TEncryptedNode,
|
|
352
|
+
error: DecryptionError,
|
|
353
|
+
): TDecryptedNode;
|
|
354
|
+
|
|
355
|
+
protected getDegradedUndecryptableNodeBase(encryptedNode: EncryptedNode, error: DecryptionError): DecryptedNode {
|
|
356
|
+
return {
|
|
357
|
+
...encryptedNode,
|
|
358
|
+
isStale: false,
|
|
359
|
+
name: resultError(error),
|
|
360
|
+
keyAuthor: resultError({
|
|
361
|
+
claimedAuthor: encryptedNode.encryptedCrypto.signatureEmail,
|
|
362
|
+
error: getErrorMessage(error),
|
|
363
|
+
}),
|
|
364
|
+
nameAuthor: resultError({
|
|
365
|
+
claimedAuthor: encryptedNode.encryptedCrypto.nameSignatureEmail,
|
|
366
|
+
error: getErrorMessage(error),
|
|
367
|
+
}),
|
|
368
|
+
membership: encryptedNode.membership
|
|
369
|
+
? {
|
|
370
|
+
role: encryptedNode.membership.role,
|
|
371
|
+
inviteTime: encryptedNode.membership.inviteTime,
|
|
372
|
+
sharedBy: resultError({
|
|
373
|
+
claimedAuthor: encryptedNode.encryptedCrypto.membership?.inviterEmail,
|
|
374
|
+
error: getErrorMessage(error),
|
|
375
|
+
}),
|
|
376
|
+
}
|
|
377
|
+
: undefined,
|
|
378
|
+
errors: [error],
|
|
379
|
+
treeEventScopeId: splitNodeUid(encryptedNode.uid).volumeId,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
protected abstract parseNode(
|
|
384
|
+
unparsedNode: Awaited<ReturnType<TCryptoService['decryptNode']>>['node'],
|
|
385
|
+
): TDecryptedNode;
|
|
386
|
+
|
|
369
387
|
async getParentKeys(
|
|
370
|
-
node: Pick<
|
|
388
|
+
node: Pick<TDecryptedNode, 'uid' | 'parentUid' | 'shareId'>,
|
|
371
389
|
): Promise<Pick<DecryptedNodeKeys, 'key' | 'hashKey'>> {
|
|
372
390
|
if (node.parentUid) {
|
|
373
391
|
try {
|
|
@@ -425,6 +443,22 @@ export class NodesAccess {
|
|
|
425
443
|
};
|
|
426
444
|
}
|
|
427
445
|
|
|
446
|
+
async getNodeSigningKeys(
|
|
447
|
+
uids: { nodeUid: string; parentNodeUid?: string } | { nodeUid?: string; parentNodeUid: string },
|
|
448
|
+
): Promise<NodeSigningKeys> {
|
|
449
|
+
const contextNodeUid = uids.nodeUid || uids.parentNodeUid;
|
|
450
|
+
if (!contextNodeUid) {
|
|
451
|
+
throw new Error('Context node UID is required for signing keys');
|
|
452
|
+
}
|
|
453
|
+
const address = await this.getRootNodeEmailKey(contextNodeUid);
|
|
454
|
+
return {
|
|
455
|
+
type: 'userAddress',
|
|
456
|
+
email: address.email,
|
|
457
|
+
addressId: address.addressId,
|
|
458
|
+
key: address.addressKey,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
428
462
|
async getRootNodeEmailKey(nodeUid: string): Promise<{
|
|
429
463
|
email: string;
|
|
430
464
|
addressId: string;
|
|
@@ -456,13 +490,23 @@ export class NodesAccess {
|
|
|
456
490
|
return `https://drive.proton.me/${rootNode.shareId}/${type}/${nodeId}`;
|
|
457
491
|
}
|
|
458
492
|
|
|
459
|
-
private async getRootNode(nodeUid: string): Promise<
|
|
493
|
+
private async getRootNode(nodeUid: string): Promise<TDecryptedNode> {
|
|
460
494
|
const node = await this.getNode(nodeUid);
|
|
461
495
|
return node.parentUid ? this.getRootNode(node.parentUid) : node;
|
|
462
496
|
}
|
|
463
497
|
}
|
|
464
498
|
|
|
465
|
-
export
|
|
499
|
+
export class NodesAccess extends NodesAccessBase {
|
|
500
|
+
protected getDegradedUndecryptableNode(encryptedNode: EncryptedNode, error: DecryptionError): DecryptedNode {
|
|
501
|
+
return this.getDegradedUndecryptableNodeBase(encryptedNode, error);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
protected parseNode(unparsedNode: DecryptedUnparsedNode): DecryptedNode {
|
|
505
|
+
return parseNode(this.logger, unparsedNode);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function parseNode(logger: Logger, unparsedNode: DecryptedUnparsedNode): DecryptedNode {
|
|
466
510
|
let nodeName: Result<string, Error | InvalidNameError> = unparsedNode.name;
|
|
467
511
|
if (unparsedNode.name.ok) {
|
|
468
512
|
try {
|
|
@@ -509,6 +553,7 @@ export async function parseNode(logger: Logger, unparsedNode: DecryptedUnparsedN
|
|
|
509
553
|
const extendedAttributes = unparsedNode.folder?.extendedAttributes
|
|
510
554
|
? parseFolderExtendedAttributes(logger, unparsedNode.folder.extendedAttributes)
|
|
511
555
|
: undefined;
|
|
556
|
+
|
|
512
557
|
return {
|
|
513
558
|
...unparsedNode,
|
|
514
559
|
name: nodeName,
|