@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.
Files changed (99) hide show
  1. package/dist/internal/apiService/apiService.d.ts +2 -2
  2. package/dist/internal/apiService/apiService.js.map +1 -1
  3. package/dist/internal/apiService/errors.js +4 -3
  4. package/dist/internal/apiService/errors.js.map +1 -1
  5. package/dist/internal/download/cryptoService.js +8 -6
  6. package/dist/internal/download/cryptoService.js.map +1 -1
  7. package/dist/internal/download/fileDownloader.d.ts +2 -1
  8. package/dist/internal/download/fileDownloader.js +6 -3
  9. package/dist/internal/download/fileDownloader.js.map +1 -1
  10. package/dist/internal/download/index.d.ts +1 -1
  11. package/dist/internal/download/index.js +3 -3
  12. package/dist/internal/download/index.js.map +1 -1
  13. package/dist/internal/nodes/apiService.d.ts +9 -8
  14. package/dist/internal/nodes/apiService.js +14 -5
  15. package/dist/internal/nodes/apiService.js.map +1 -1
  16. package/dist/internal/nodes/apiService.test.js +5 -5
  17. package/dist/internal/nodes/apiService.test.js.map +1 -1
  18. package/dist/internal/nodes/cryptoReporter.d.ts +3 -3
  19. package/dist/internal/nodes/cryptoReporter.js.map +1 -1
  20. package/dist/internal/nodes/cryptoService.d.ts +12 -21
  21. package/dist/internal/nodes/cryptoService.js +45 -14
  22. package/dist/internal/nodes/cryptoService.js.map +1 -1
  23. package/dist/internal/nodes/cryptoService.test.js +262 -17
  24. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  25. package/dist/internal/nodes/interface.d.ts +13 -3
  26. package/dist/internal/nodes/nodesAccess.d.ts +8 -1
  27. package/dist/internal/nodes/nodesAccess.js +13 -0
  28. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  29. package/dist/internal/nodes/nodesManagement.d.ts +4 -4
  30. package/dist/internal/nodes/nodesManagement.js +16 -20
  31. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  32. package/dist/internal/nodes/nodesManagement.test.js +21 -10
  33. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  34. package/dist/internal/photos/upload.d.ts +2 -2
  35. package/dist/internal/photos/upload.js.map +1 -1
  36. package/dist/internal/sharingPublic/index.d.ts +6 -6
  37. package/dist/internal/sharingPublic/index.js +8 -7
  38. package/dist/internal/sharingPublic/index.js.map +1 -1
  39. package/dist/internal/sharingPublic/nodes.d.ts +16 -3
  40. package/dist/internal/sharingPublic/nodes.js +34 -2
  41. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  42. package/dist/internal/sharingPublic/unauthApiService.d.ts +17 -0
  43. package/dist/internal/sharingPublic/unauthApiService.js +31 -0
  44. package/dist/internal/sharingPublic/unauthApiService.js.map +1 -0
  45. package/dist/internal/sharingPublic/unauthApiService.test.d.ts +1 -0
  46. package/dist/internal/sharingPublic/unauthApiService.test.js +27 -0
  47. package/dist/internal/sharingPublic/unauthApiService.test.js.map +1 -0
  48. package/dist/internal/upload/apiService.d.ts +4 -3
  49. package/dist/internal/upload/apiService.js.map +1 -1
  50. package/dist/internal/upload/cryptoService.d.ts +8 -3
  51. package/dist/internal/upload/cryptoService.js +45 -9
  52. package/dist/internal/upload/cryptoService.js.map +1 -1
  53. package/dist/internal/upload/fileUploader.test.js +1 -1
  54. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  55. package/dist/internal/upload/interface.d.ts +25 -13
  56. package/dist/internal/upload/manager.js +7 -4
  57. package/dist/internal/upload/manager.js.map +1 -1
  58. package/dist/internal/upload/manager.test.js +5 -4
  59. package/dist/internal/upload/manager.test.js.map +1 -1
  60. package/dist/internal/upload/streamUploader.js +9 -4
  61. package/dist/internal/upload/streamUploader.js.map +1 -1
  62. package/dist/internal/upload/streamUploader.test.js +8 -5
  63. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  64. package/dist/protonDriveClient.d.ts +1 -1
  65. package/dist/protonDriveClient.js +2 -1
  66. package/dist/protonDriveClient.js.map +1 -1
  67. package/dist/protonDrivePublicLinkClient.d.ts +2 -1
  68. package/dist/protonDrivePublicLinkClient.js +7 -5
  69. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/internal/apiService/apiService.ts +2 -2
  72. package/src/internal/apiService/errors.ts +5 -4
  73. package/src/internal/download/cryptoService.ts +13 -6
  74. package/src/internal/download/fileDownloader.ts +4 -2
  75. package/src/internal/download/index.ts +3 -0
  76. package/src/internal/nodes/apiService.test.ts +5 -5
  77. package/src/internal/nodes/apiService.ts +23 -10
  78. package/src/internal/nodes/cryptoReporter.ts +3 -3
  79. package/src/internal/nodes/cryptoService.test.ts +370 -18
  80. package/src/internal/nodes/cryptoService.ts +73 -32
  81. package/src/internal/nodes/interface.ts +16 -2
  82. package/src/internal/nodes/nodesAccess.ts +17 -0
  83. package/src/internal/nodes/nodesManagement.test.ts +21 -10
  84. package/src/internal/nodes/nodesManagement.ts +20 -24
  85. package/src/internal/photos/upload.ts +3 -3
  86. package/src/internal/sharingPublic/index.ts +7 -3
  87. package/src/internal/sharingPublic/nodes.ts +43 -2
  88. package/src/internal/sharingPublic/unauthApiService.test.ts +29 -0
  89. package/src/internal/sharingPublic/unauthApiService.ts +32 -0
  90. package/src/internal/upload/apiService.ts +4 -3
  91. package/src/internal/upload/cryptoService.ts +73 -12
  92. package/src/internal/upload/fileUploader.test.ts +1 -1
  93. package/src/internal/upload/interface.ts +24 -13
  94. package/src/internal/upload/manager.test.ts +5 -4
  95. package/src/internal/upload/manager.ts +7 -4
  96. package/src/internal/upload/streamUploader.test.ts +8 -5
  97. package/src/internal/upload/streamUploader.ts +10 -4
  98. package/src/protonDriveClient.ts +7 -2
  99. 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 { name, verified, verificationErrors } = await this.driveCrypto.decryptNodeName(
340
- node.encryptedName,
341
- parentKey,
342
- verificationKeys,
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 { hashKey, verified, verificationErrors } = await this.driveCrypto.decryptNodeHashKey(
442
- node.encryptedCrypto.folder.armoredHashKey,
443
- nodeKey,
444
- addressKeys,
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
- address: { email: string; addressKey: PrivateKey },
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 { email, addressKey } = address;
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], addressKey),
542
- this.driveCrypto.encryptNodeName(name, undefined, parentKeys.key, addressKey),
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(extendedAttributes, nodeKeys.decrypted.key, addressKey)
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
- address: { email: string; addressKey: PrivateKey },
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 { email, addressKey } = address;
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
- addressKey,
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
- address: { email: string; addressKey: PrivateKey },
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 { email, addressKey } = address;
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
- addressKey,
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
- addressKey,
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
- deleteNodes: jest.fn(async function* (uids) {
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
- getRootNodeEmailKey: jest.fn().mockResolvedValue({ email: 'root-email', addressKey: 'root-key' }),
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.getRootNodeEmailKey).toHaveBeenCalledWith('nodeUid');
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', addressKey: 'root-key' },
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.getRootNodeEmailKey).toHaveBeenCalledWith('newParentNodeUid');
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', addressKey: 'root-key' },
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', addressKey: 'root-key' },
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.getRootNodeEmailKey).toHaveBeenCalledWith('newParentNodeUid');
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', addressKey: 'root-key' },
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', addressKey: 'root-key' },
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
- private apiService: NodeAPIService,
32
- private cryptoCache: NodesCryptoCache,
33
- private cryptoService: NodesCryptoService,
34
- private nodesAccess: NodesAccess,
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 address = await this.nodesAccess.getRootNodeEmailKey(nodeUid);
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
- address,
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 [node, address] = await Promise.all([
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
- address,
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 [node, address] = await Promise.all([
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
- address,
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.deleteNodes(nodeUids, signal)) {
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 address = await this.nodesAccess.getRootNodeEmailKey(parentNodeUid);
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
- address,
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 NodesManagement(api, cryptoCache, cryptoService, nodesAccess);
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
  }