@protontech/drive-sdk 0.6.2 → 0.7.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/internal/apiService/apiService.d.ts +2 -2
- package/dist/internal/apiService/apiService.js.map +1 -1
- 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/nodes/apiService.d.ts +9 -8
- package/dist/internal/nodes/apiService.js +14 -5
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +5 -5
- package/dist/internal/nodes/apiService.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 +12 -21
- package/dist/internal/nodes/cryptoService.js +45 -14
- 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/interface.d.ts +13 -3
- package/dist/internal/nodes/nodesAccess.d.ts +8 -1
- package/dist/internal/nodes/nodesAccess.js +13 -0
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +4 -4
- package/dist/internal/nodes/nodesManagement.js +16 -20
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +21 -10
- package/dist/internal/nodes/nodesManagement.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 +1 -1
- package/dist/protonDriveClient.js +2 -1
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +2 -1
- package/dist/protonDrivePublicLinkClient.js +7 -5
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/package.json +1 -1
- package/src/internal/apiService/apiService.ts +2 -2
- 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/nodes/apiService.test.ts +5 -5
- package/src/internal/nodes/apiService.ts +23 -10
- package/src/internal/nodes/cryptoReporter.ts +3 -3
- package/src/internal/nodes/cryptoService.test.ts +370 -18
- package/src/internal/nodes/cryptoService.ts +73 -32
- package/src/internal/nodes/interface.ts +16 -2
- package/src/internal/nodes/nodesAccess.ts +17 -0
- package/src/internal/nodes/nodesManagement.test.ts +21 -10
- package/src/internal/nodes/nodesManagement.ts +20 -24
- 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 +7 -2
- package/src/protonDrivePublicLinkClient.ts +8 -3
|
@@ -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
|
|
@@ -605,14 +640,14 @@ export class NodesCryptoService {
|
|
|
605
640
|
node: Pick<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');
|
|
@@ -621,19 +656,25 @@ export class NodesCryptoService {
|
|
|
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
|
node.name.value,
|
|
627
668
|
keys.nameSessionKey,
|
|
628
669
|
parentKeys.key,
|
|
629
|
-
|
|
670
|
+
nameAndPassprhaseSigningKey,
|
|
630
671
|
);
|
|
631
672
|
const hash = await this.driveCrypto.generateLookupHash(node.name.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) {
|
|
@@ -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 = {
|
|
@@ -57,8 +58,8 @@ export interface EncryptedNode extends BaseNode {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface EncryptedNodeCrypto {
|
|
60
|
-
signatureEmail?: string;
|
|
61
|
-
nameSignatureEmail?: string;
|
|
61
|
+
signatureEmail?: string | AnonymousUser;
|
|
62
|
+
nameSignatureEmail?: string | AnonymousUser;
|
|
62
63
|
armoredKey: string;
|
|
63
64
|
armoredNodePassphrase: string;
|
|
64
65
|
armoredNodePassphraseSignature?: string;
|
|
@@ -88,6 +89,19 @@ export interface EncryptedNodeFolderCrypto extends EncryptedNodeCrypto {
|
|
|
88
89
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
89
90
|
export interface EncryptedNodeAlbumCrypto extends EncryptedNodeCrypto {}
|
|
90
91
|
|
|
92
|
+
export type NodeSigningKeys =
|
|
93
|
+
| {
|
|
94
|
+
type: 'userAddress';
|
|
95
|
+
email: string;
|
|
96
|
+
addressId: string;
|
|
97
|
+
key: PrivateKey;
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
type: 'nodeKey';
|
|
101
|
+
nodeKey?: PrivateKey;
|
|
102
|
+
parentNodeKey?: PrivateKey;
|
|
103
|
+
};
|
|
104
|
+
|
|
91
105
|
/**
|
|
92
106
|
* Interface used only internally in the nodes module.
|
|
93
107
|
*
|
|
@@ -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';
|
|
@@ -425,6 +426,22 @@ export class NodesAccess {
|
|
|
425
426
|
};
|
|
426
427
|
}
|
|
427
428
|
|
|
429
|
+
async getNodeSigningKeys(
|
|
430
|
+
uids: { nodeUid: string; parentNodeUid?: string } | { nodeUid?: string; parentNodeUid: string },
|
|
431
|
+
): Promise<NodeSigningKeys> {
|
|
432
|
+
const contextNodeUid = uids.nodeUid || uids.parentNodeUid;
|
|
433
|
+
if (!contextNodeUid) {
|
|
434
|
+
throw new Error('Context node UID is required for signing keys');
|
|
435
|
+
}
|
|
436
|
+
const address = await this.getRootNodeEmailKey(contextNodeUid);
|
|
437
|
+
return {
|
|
438
|
+
type: 'userAddress',
|
|
439
|
+
email: address.email,
|
|
440
|
+
addressId: address.addressId,
|
|
441
|
+
key: address.addressKey,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
428
445
|
async getRootNodeEmailKey(nodeUid: string): Promise<{
|
|
429
446
|
email: string;
|
|
430
447
|
addressId: string;
|
|
@@ -57,7 +57,7 @@ describe('NodesManagement', () => {
|
|
|
57
57
|
restoreNodes: jest.fn(async function* (uids) {
|
|
58
58
|
yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
|
|
59
59
|
}),
|
|
60
|
-
|
|
60
|
+
deleteTrashedNodes: jest.fn(async function* (uids) {
|
|
61
61
|
yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
|
|
62
62
|
}),
|
|
63
63
|
createFolder: jest.fn(),
|
|
@@ -117,7 +117,12 @@ describe('NodesManagement', () => {
|
|
|
117
117
|
nameSessionKey: `${uid}-nameSessionKey`,
|
|
118
118
|
}),
|
|
119
119
|
),
|
|
120
|
-
|
|
120
|
+
getNodeSigningKeys: jest.fn().mockResolvedValue({
|
|
121
|
+
type: 'userAddress',
|
|
122
|
+
email: 'root-email',
|
|
123
|
+
addressId: 'root-addressId',
|
|
124
|
+
key: 'root-key',
|
|
125
|
+
}),
|
|
121
126
|
notifyNodeChanged: jest.fn(),
|
|
122
127
|
notifyNodeDeleted: jest.fn(),
|
|
123
128
|
notifyChildCreated: jest.fn(),
|
|
@@ -136,11 +141,11 @@ describe('NodesManagement', () => {
|
|
|
136
141
|
nameAuthor: { ok: true, value: 'newSignatureEmail' },
|
|
137
142
|
hash: 'newHash',
|
|
138
143
|
});
|
|
139
|
-
expect(nodesAccess.
|
|
144
|
+
expect(nodesAccess.getNodeSigningKeys).toHaveBeenCalledWith({ nodeUid: 'nodeUid', parentNodeUid: 'parentUid' });
|
|
140
145
|
expect(cryptoService.encryptNewName).toHaveBeenCalledWith(
|
|
141
146
|
{ key: 'parentUid-key', hashKey: 'parentUid-hashKey' },
|
|
142
147
|
'nodeUid-nameSessionKey',
|
|
143
|
-
{ email: 'root-email',
|
|
148
|
+
{ type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
|
|
144
149
|
'new name',
|
|
145
150
|
);
|
|
146
151
|
expect(apiService.renameNode).toHaveBeenCalledWith(
|
|
@@ -181,7 +186,10 @@ describe('NodesManagement', () => {
|
|
|
181
186
|
keyAuthor: { ok: true, value: 'movedSignatureEmail' },
|
|
182
187
|
nameAuthor: { ok: true, value: 'movedNameSignatureEmail' },
|
|
183
188
|
});
|
|
184
|
-
expect(nodesAccess.
|
|
189
|
+
expect(nodesAccess.getNodeSigningKeys).toHaveBeenCalledWith({
|
|
190
|
+
nodeUid: 'nodeUid',
|
|
191
|
+
parentNodeUid: 'newParentNodeUid',
|
|
192
|
+
});
|
|
185
193
|
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
186
194
|
nodes.nodeUid,
|
|
187
195
|
expect.objectContaining({
|
|
@@ -192,7 +200,7 @@ describe('NodesManagement', () => {
|
|
|
192
200
|
nameSessionKey: 'nodeUid-nameSessionKey',
|
|
193
201
|
}),
|
|
194
202
|
expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
|
|
195
|
-
{ email: 'root-email',
|
|
203
|
+
{ type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
|
|
196
204
|
);
|
|
197
205
|
expect(apiService.moveNode).toHaveBeenCalledWith(
|
|
198
206
|
'nodeUid',
|
|
@@ -232,7 +240,7 @@ describe('NodesManagement', () => {
|
|
|
232
240
|
nameSessionKey: 'anonymousNodeUid-nameSessionKey',
|
|
233
241
|
}),
|
|
234
242
|
expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
|
|
235
|
-
{ email: 'root-email',
|
|
243
|
+
{ type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
|
|
236
244
|
);
|
|
237
245
|
expect(newNode).toEqual({
|
|
238
246
|
...nodes.anonymousNodeUid,
|
|
@@ -276,7 +284,10 @@ describe('NodesManagement', () => {
|
|
|
276
284
|
keyAuthor: { ok: true, value: 'copiedSignatureEmail' },
|
|
277
285
|
nameAuthor: { ok: true, value: 'copiedNameSignatureEmail' },
|
|
278
286
|
});
|
|
279
|
-
expect(nodesAccess.
|
|
287
|
+
expect(nodesAccess.getNodeSigningKeys).toHaveBeenCalledWith({
|
|
288
|
+
nodeUid: 'nodeUid',
|
|
289
|
+
parentNodeUid: 'newParentNodeUid',
|
|
290
|
+
});
|
|
280
291
|
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
281
292
|
nodes.nodeUid,
|
|
282
293
|
expect.objectContaining({
|
|
@@ -287,7 +298,7 @@ describe('NodesManagement', () => {
|
|
|
287
298
|
nameSessionKey: 'nodeUid-nameSessionKey',
|
|
288
299
|
}),
|
|
289
300
|
expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
|
|
290
|
-
{ email: 'root-email',
|
|
301
|
+
{ type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
|
|
291
302
|
);
|
|
292
303
|
expect(apiService.copyNode).toHaveBeenCalledWith('nodeUid', {
|
|
293
304
|
parentUid: 'newParentNodeUid',
|
|
@@ -322,7 +333,7 @@ describe('NodesManagement', () => {
|
|
|
322
333
|
nameSessionKey: 'anonymousNodeUid-nameSessionKey',
|
|
323
334
|
}),
|
|
324
335
|
expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
|
|
325
|
-
{ email: 'root-email',
|
|
336
|
+
{ type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
|
|
326
337
|
);
|
|
327
338
|
expect(newNode).toEqual({
|
|
328
339
|
...nodes.anonymousNodeUid,
|
|
@@ -28,10 +28,10 @@ const AVAILABLE_NAME_LIMIT = 1000;
|
|
|
28
28
|
*/
|
|
29
29
|
export class NodesManagement {
|
|
30
30
|
constructor(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
protected apiService: NodeAPIService,
|
|
32
|
+
protected cryptoCache: NodesCryptoCache,
|
|
33
|
+
protected cryptoService: NodesCryptoService,
|
|
34
|
+
protected nodesAccess: NodesAccess,
|
|
35
35
|
) {
|
|
36
36
|
this.apiService = apiService;
|
|
37
37
|
this.cryptoCache = cryptoCache;
|
|
@@ -49,7 +49,7 @@ export class NodesManagement {
|
|
|
49
49
|
const node = await this.nodesAccess.getNode(nodeUid);
|
|
50
50
|
const { nameSessionKey: nodeNameSessionKey } = await this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid);
|
|
51
51
|
const parentKeys = await this.nodesAccess.getParentKeys(node);
|
|
52
|
-
const
|
|
52
|
+
const signingKeys = await this.nodesAccess.getNodeSigningKeys({ nodeUid, parentNodeUid: node.parentUid });
|
|
53
53
|
|
|
54
54
|
if (!options.allowRenameRootNode && (!node.hash || !parentKeys.hashKey)) {
|
|
55
55
|
throw new ValidationError(c('Error').t`Renaming root item is not allowed`);
|
|
@@ -58,7 +58,7 @@ export class NodesManagement {
|
|
|
58
58
|
const { signatureEmail, armoredNodeName, hash } = await this.cryptoService.encryptNewName(
|
|
59
59
|
parentKeys,
|
|
60
60
|
nodeNameSessionKey,
|
|
61
|
-
|
|
61
|
+
signingKeys,
|
|
62
62
|
newName,
|
|
63
63
|
);
|
|
64
64
|
|
|
@@ -95,7 +95,7 @@ export class NodesManagement {
|
|
|
95
95
|
...node,
|
|
96
96
|
name: resultOk(newName),
|
|
97
97
|
encryptedName: armoredNodeName,
|
|
98
|
-
nameAuthor: resultOk(signatureEmail),
|
|
98
|
+
nameAuthor: resultOk(signatureEmail || null),
|
|
99
99
|
hash,
|
|
100
100
|
};
|
|
101
101
|
return newNode;
|
|
@@ -124,14 +124,12 @@ export class NodesManagement {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
async moveNode(nodeUid: string, newParentUid: string): Promise<DecryptedNode> {
|
|
127
|
-
const
|
|
128
|
-
this.nodesAccess.getNode(nodeUid),
|
|
129
|
-
this.nodesAccess.getRootNodeEmailKey(newParentUid),
|
|
130
|
-
]);
|
|
127
|
+
const node = await this.nodesAccess.getNode(nodeUid);
|
|
131
128
|
|
|
132
|
-
const [keys, newParentKeys] = await Promise.all([
|
|
129
|
+
const [keys, newParentKeys, signingKeys] = await Promise.all([
|
|
133
130
|
this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid),
|
|
134
131
|
this.nodesAccess.getNodeKeys(newParentUid),
|
|
132
|
+
this.nodesAccess.getNodeSigningKeys({ nodeUid, parentNodeUid: newParentUid }),
|
|
135
133
|
]);
|
|
136
134
|
|
|
137
135
|
if (!node.hash) {
|
|
@@ -145,7 +143,7 @@ export class NodesManagement {
|
|
|
145
143
|
node,
|
|
146
144
|
keys,
|
|
147
145
|
{ key: newParentKeys.key, hashKey: newParentKeys.hashKey },
|
|
148
|
-
|
|
146
|
+
signingKeys,
|
|
149
147
|
);
|
|
150
148
|
|
|
151
149
|
// Node could be uploaded or renamed by anonymous user and thus have
|
|
@@ -214,14 +212,12 @@ export class NodesManagement {
|
|
|
214
212
|
}
|
|
215
213
|
|
|
216
214
|
async copyNode(nodeUid: string, newParentUid: string): Promise<DecryptedNode> {
|
|
217
|
-
const
|
|
218
|
-
this.nodesAccess.getNode(nodeUid),
|
|
219
|
-
this.nodesAccess.getRootNodeEmailKey(newParentUid),
|
|
220
|
-
]);
|
|
215
|
+
const node = await this.nodesAccess.getNode(nodeUid);
|
|
221
216
|
|
|
222
|
-
const [keys, newParentKeys] = await Promise.all([
|
|
217
|
+
const [keys, newParentKeys, signingKeys] = await Promise.all([
|
|
223
218
|
this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid),
|
|
224
219
|
this.nodesAccess.getNodeKeys(newParentUid),
|
|
220
|
+
this.nodesAccess.getNodeSigningKeys({ nodeUid, parentNodeUid: newParentUid }),
|
|
225
221
|
]);
|
|
226
222
|
|
|
227
223
|
if (!newParentKeys.hashKey) {
|
|
@@ -232,7 +228,7 @@ export class NodesManagement {
|
|
|
232
228
|
node,
|
|
233
229
|
keys,
|
|
234
230
|
{ key: newParentKeys.key, hashKey: newParentKeys.hashKey },
|
|
235
|
-
|
|
231
|
+
signingKeys,
|
|
236
232
|
);
|
|
237
233
|
|
|
238
234
|
// Node could be uploaded or renamed by anonymous user and thus have
|
|
@@ -286,7 +282,7 @@ export class NodesManagement {
|
|
|
286
282
|
}
|
|
287
283
|
|
|
288
284
|
async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
289
|
-
for await (const result of this.apiService.
|
|
285
|
+
for await (const result of this.apiService.deleteTrashedNodes(nodeUids, signal)) {
|
|
290
286
|
if (result.ok) {
|
|
291
287
|
await this.nodesAccess.notifyNodeDeleted(result.uid);
|
|
292
288
|
}
|
|
@@ -303,12 +299,12 @@ export class NodesManagement {
|
|
|
303
299
|
throw new ValidationError(c('Error').t`Creating folders in non-folders is not allowed`);
|
|
304
300
|
}
|
|
305
301
|
|
|
306
|
-
const
|
|
302
|
+
const signingKeys = await this.nodesAccess.getNodeSigningKeys({ parentNodeUid });
|
|
307
303
|
const extendedAttributes = generateFolderExtendedAttributes(modificationTime);
|
|
308
304
|
|
|
309
305
|
const { encryptedCrypto, keys } = await this.cryptoService.createFolder(
|
|
310
306
|
{ key: parentKeys.key, hashKey: parentKeys.hashKey },
|
|
311
|
-
|
|
307
|
+
signingKeys,
|
|
312
308
|
folderName,
|
|
313
309
|
extendedAttributes,
|
|
314
310
|
);
|
|
@@ -344,8 +340,8 @@ export class NodesManagement {
|
|
|
344
340
|
|
|
345
341
|
// Decrypted metadata
|
|
346
342
|
isStale: false,
|
|
347
|
-
keyAuthor: resultOk(encryptedCrypto.signatureEmail),
|
|
348
|
-
nameAuthor: resultOk(encryptedCrypto.signatureEmail),
|
|
343
|
+
keyAuthor: resultOk(encryptedCrypto.signatureEmail || null),
|
|
344
|
+
nameAuthor: resultOk(encryptedCrypto.signatureEmail || null),
|
|
349
345
|
name: resultOk(folderName),
|
|
350
346
|
treeEventScopeId: splitNodeUid(nodeUid).volumeId,
|
|
351
347
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DriveCrypto } from '../../crypto';
|
|
2
|
-
import { ProtonDriveTelemetry, UploadMetadata, Thumbnail } from '../../interface';
|
|
2
|
+
import { ProtonDriveTelemetry, UploadMetadata, Thumbnail, AnonymousUser } from '../../interface';
|
|
3
3
|
import { DriveAPIService, drivePaths } from '../apiService';
|
|
4
4
|
import { generateFileExtendedAttributes } from '../nodes';
|
|
5
5
|
import { splitNodeRevisionUid } from '../uids';
|
|
@@ -198,7 +198,7 @@ export class PhotoUploadAPIService extends UploadAPIService {
|
|
|
198
198
|
draftNodeRevisionUid: string,
|
|
199
199
|
options: {
|
|
200
200
|
armoredManifestSignature: string;
|
|
201
|
-
signatureEmail: string;
|
|
201
|
+
signatureEmail: string | AnonymousUser;
|
|
202
202
|
armoredExtendedAttributes?: string;
|
|
203
203
|
},
|
|
204
204
|
photo: {
|
|
@@ -220,7 +220,7 @@ export class PhotoUploadAPIService extends UploadAPIService {
|
|
|
220
220
|
XAttr: options.armoredExtendedAttributes || null,
|
|
221
221
|
Photo: {
|
|
222
222
|
ContentHash: photo.contentHash,
|
|
223
|
-
CaptureTime: photo.captureTime ? Math.floor(photo.captureTime?.getTime() /1000) : 0,
|
|
223
|
+
CaptureTime: photo.captureTime ? Math.floor(photo.captureTime?.getTime() / 1000) : 0,
|
|
224
224
|
MainPhotoLinkID: photo.mainPhotoLinkID || null,
|
|
225
225
|
Tags: photo.tags || [],
|
|
226
226
|
Exif: null, // Deprecated field, not used.
|
|
@@ -10,13 +10,13 @@ import { NodeAPIService } from '../nodes/apiService';
|
|
|
10
10
|
import { NodesCache } from '../nodes/cache';
|
|
11
11
|
import { NodesCryptoCache } from '../nodes/cryptoCache';
|
|
12
12
|
import { NodesCryptoService } from '../nodes/cryptoService';
|
|
13
|
-
import { NodesManagement } from '../nodes/nodesManagement';
|
|
14
13
|
import { NodesRevisons } from '../nodes/nodesRevisions';
|
|
15
14
|
import { SharingPublicCryptoReporter } from './cryptoReporter';
|
|
16
|
-
import { SharingPublicNodesAccess } from './nodes';
|
|
15
|
+
import { SharingPublicNodesAccess, SharingPublicNodesManagement } from './nodes';
|
|
17
16
|
import { SharingPublicSharesManager } from './shares';
|
|
18
17
|
|
|
19
18
|
export { SharingPublicSessionManager } from './session/manager';
|
|
19
|
+
export { UnauthDriveAPIService } from './unauthApiService';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Provides facade for the whole sharing public module.
|
|
@@ -38,6 +38,7 @@ export function initSharingPublicModule(
|
|
|
38
38
|
token: string,
|
|
39
39
|
publicShareKey: PrivateKey,
|
|
40
40
|
publicRootNodeUid: string,
|
|
41
|
+
isAnonymousContext: boolean,
|
|
41
42
|
) {
|
|
42
43
|
const shares = new SharingPublicSharesManager(account, publicShareKey, publicRootNodeUid);
|
|
43
44
|
const nodes = initSharingPublicNodesModule(
|
|
@@ -52,6 +53,7 @@ export function initSharingPublicModule(
|
|
|
52
53
|
token,
|
|
53
54
|
publicShareKey,
|
|
54
55
|
publicRootNodeUid,
|
|
56
|
+
isAnonymousContext,
|
|
55
57
|
);
|
|
56
58
|
|
|
57
59
|
return {
|
|
@@ -78,6 +80,7 @@ export function initSharingPublicNodesModule(
|
|
|
78
80
|
token: string,
|
|
79
81
|
publicShareKey: PrivateKey,
|
|
80
82
|
publicRootNodeUid: string,
|
|
83
|
+
isAnonymousContext: boolean,
|
|
81
84
|
) {
|
|
82
85
|
const clientUid = undefined; // No client UID for public context yet.
|
|
83
86
|
const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService, clientUid);
|
|
@@ -96,8 +99,9 @@ export function initSharingPublicNodesModule(
|
|
|
96
99
|
token,
|
|
97
100
|
publicShareKey,
|
|
98
101
|
publicRootNodeUid,
|
|
102
|
+
isAnonymousContext,
|
|
99
103
|
);
|
|
100
|
-
const nodesManagement = new
|
|
104
|
+
const nodesManagement = new SharingPublicNodesManagement(api, cryptoCache, cryptoService, nodesAccess);
|
|
101
105
|
const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
|
|
102
106
|
|
|
103
107
|
return {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { ProtonDriveTelemetry } from '../../interface';
|
|
1
|
+
import { NodeResult, ProtonDriveTelemetry } from '../../interface';
|
|
2
2
|
import { NodeAPIService } from '../nodes/apiService';
|
|
3
3
|
import { NodesCache } from '../nodes/cache';
|
|
4
4
|
import { NodesCryptoCache } from '../nodes/cryptoCache';
|
|
5
5
|
import { NodesCryptoService } from '../nodes/cryptoService';
|
|
6
6
|
import { NodesAccess } from '../nodes/nodesAccess';
|
|
7
|
+
import { NodesManagement } from '../nodes/nodesManagement';
|
|
7
8
|
import { isProtonDocument, isProtonSheet } from '../nodes/mediaTypes';
|
|
8
9
|
import { splitNodeUid } from '../uids';
|
|
9
10
|
import { SharingPublicSharesManager } from './shares';
|
|
10
|
-
import { DecryptedNode, DecryptedNodeKeys } from '../nodes/interface';
|
|
11
|
+
import { DecryptedNode, DecryptedNodeKeys, NodeSigningKeys } from '../nodes/interface';
|
|
11
12
|
import { PrivateKey } from '../../crypto';
|
|
12
13
|
|
|
13
14
|
export class SharingPublicNodesAccess extends NodesAccess {
|
|
@@ -22,11 +23,13 @@ export class SharingPublicNodesAccess extends NodesAccess {
|
|
|
22
23
|
private token: string,
|
|
23
24
|
private publicShareKey: PrivateKey,
|
|
24
25
|
private publicRootNodeUid: string,
|
|
26
|
+
private isAnonymousContext: boolean,
|
|
25
27
|
) {
|
|
26
28
|
super(telemetry, apiService, cache, cryptoCache, cryptoService, sharesService);
|
|
27
29
|
this.token = token;
|
|
28
30
|
this.publicShareKey = publicShareKey;
|
|
29
31
|
this.publicRootNodeUid = publicRootNodeUid;
|
|
32
|
+
this.isAnonymousContext = isAnonymousContext;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
async getParentKeys(
|
|
@@ -56,4 +59,42 @@ export class SharingPublicNodesAccess extends NodesAccess {
|
|
|
56
59
|
// Public link doesn't support specific node URLs.
|
|
57
60
|
return this.url;
|
|
58
61
|
}
|
|
62
|
+
|
|
63
|
+
async getNodeSigningKeys(
|
|
64
|
+
uids: { nodeUid: string; parentNodeUid?: string } | { nodeUid?: string; parentNodeUid: string },
|
|
65
|
+
): Promise<NodeSigningKeys> {
|
|
66
|
+
if (this.isAnonymousContext) {
|
|
67
|
+
const nodeKeys = uids.nodeUid ? await this.getNodeKeys(uids.nodeUid) : { key: undefined };
|
|
68
|
+
const parentNodeKeys = uids.parentNodeUid ? await this.getNodeKeys(uids.parentNodeUid) : { key: undefined };
|
|
69
|
+
return {
|
|
70
|
+
type: 'nodeKey',
|
|
71
|
+
nodeKey: nodeKeys.key,
|
|
72
|
+
parentNodeKey: parentNodeKeys.key,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return super.getNodeSigningKeys(uids);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class SharingPublicNodesManagement extends NodesManagement {
|
|
81
|
+
constructor(
|
|
82
|
+
apiService: NodeAPIService,
|
|
83
|
+
cryptoCache: NodesCryptoCache,
|
|
84
|
+
cryptoService: NodesCryptoService,
|
|
85
|
+
nodesAccess: SharingPublicNodesAccess,
|
|
86
|
+
) {
|
|
87
|
+
super(apiService, cryptoCache, cryptoService, nodesAccess);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
91
|
+
// Public link does not support trashing and deleting trashed nodes.
|
|
92
|
+
// Instead, if user is owner, API allows directly deleting existing nodes.
|
|
93
|
+
for await (const result of this.apiService.deleteExistingNodes(nodeUids, signal)) {
|
|
94
|
+
if (result.ok) {
|
|
95
|
+
await this.nodesAccess.notifyNodeDeleted(result.uid);
|
|
96
|
+
}
|
|
97
|
+
yield result;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
59
100
|
}
|