@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.
Files changed (158) hide show
  1. package/dist/interface/index.d.ts +1 -0
  2. package/dist/interface/index.js.map +1 -1
  3. package/dist/interface/nodes.d.ts +14 -10
  4. package/dist/interface/nodes.js +5 -8
  5. package/dist/interface/nodes.js.map +1 -1
  6. package/dist/interface/photos.d.ts +62 -0
  7. package/dist/interface/photos.js +3 -0
  8. package/dist/interface/photos.js.map +1 -0
  9. package/dist/internal/apiService/apiService.d.ts +2 -2
  10. package/dist/internal/apiService/apiService.js.map +1 -1
  11. package/dist/internal/apiService/driveTypes.d.ts +1294 -517
  12. package/dist/internal/apiService/errors.js +4 -3
  13. package/dist/internal/apiService/errors.js.map +1 -1
  14. package/dist/internal/download/cryptoService.js +8 -6
  15. package/dist/internal/download/cryptoService.js.map +1 -1
  16. package/dist/internal/download/fileDownloader.d.ts +2 -1
  17. package/dist/internal/download/fileDownloader.js +6 -3
  18. package/dist/internal/download/fileDownloader.js.map +1 -1
  19. package/dist/internal/download/index.d.ts +1 -1
  20. package/dist/internal/download/index.js +3 -3
  21. package/dist/internal/download/index.js.map +1 -1
  22. package/dist/internal/errors.d.ts +1 -0
  23. package/dist/internal/errors.js +4 -0
  24. package/dist/internal/errors.js.map +1 -1
  25. package/dist/internal/nodes/apiService.d.ts +68 -16
  26. package/dist/internal/nodes/apiService.js +138 -85
  27. package/dist/internal/nodes/apiService.js.map +1 -1
  28. package/dist/internal/nodes/apiService.test.js +7 -5
  29. package/dist/internal/nodes/apiService.test.js.map +1 -1
  30. package/dist/internal/nodes/cache.d.ts +16 -8
  31. package/dist/internal/nodes/cache.js +19 -5
  32. package/dist/internal/nodes/cache.js.map +1 -1
  33. package/dist/internal/nodes/cache.test.js +1 -0
  34. package/dist/internal/nodes/cache.test.js.map +1 -1
  35. package/dist/internal/nodes/cryptoReporter.d.ts +3 -3
  36. package/dist/internal/nodes/cryptoReporter.js.map +1 -1
  37. package/dist/internal/nodes/cryptoService.d.ts +13 -22
  38. package/dist/internal/nodes/cryptoService.js +47 -16
  39. package/dist/internal/nodes/cryptoService.js.map +1 -1
  40. package/dist/internal/nodes/cryptoService.test.js +262 -17
  41. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  42. package/dist/internal/nodes/events.d.ts +2 -2
  43. package/dist/internal/nodes/events.js.map +1 -1
  44. package/dist/internal/nodes/index.test.js +1 -0
  45. package/dist/internal/nodes/index.test.js.map +1 -1
  46. package/dist/internal/nodes/interface.d.ts +14 -3
  47. package/dist/internal/nodes/nodesAccess.d.ts +36 -20
  48. package/dist/internal/nodes/nodesAccess.js +54 -29
  49. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  50. package/dist/internal/nodes/nodesManagement.d.ts +34 -14
  51. package/dist/internal/nodes/nodesManagement.js +44 -31
  52. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  53. package/dist/internal/nodes/nodesManagement.test.js +60 -14
  54. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  55. package/dist/internal/nodes/nodesRevisions.d.ts +2 -2
  56. package/dist/internal/nodes/nodesRevisions.js.map +1 -1
  57. package/dist/internal/photos/albums.d.ts +2 -2
  58. package/dist/internal/photos/albums.js.map +1 -1
  59. package/dist/internal/photos/index.d.ts +19 -3
  60. package/dist/internal/photos/index.js +38 -8
  61. package/dist/internal/photos/index.js.map +1 -1
  62. package/dist/internal/photos/interface.d.ts +18 -9
  63. package/dist/internal/photos/nodes.d.ts +57 -0
  64. package/dist/internal/photos/nodes.js +165 -0
  65. package/dist/internal/photos/nodes.js.map +1 -0
  66. package/dist/internal/photos/timeline.d.ts +2 -2
  67. package/dist/internal/photos/timeline.js.map +1 -1
  68. package/dist/internal/photos/timeline.test.js.map +1 -1
  69. package/dist/internal/photos/upload.d.ts +2 -2
  70. package/dist/internal/photos/upload.js.map +1 -1
  71. package/dist/internal/sharingPublic/index.d.ts +6 -6
  72. package/dist/internal/sharingPublic/index.js +8 -7
  73. package/dist/internal/sharingPublic/index.js.map +1 -1
  74. package/dist/internal/sharingPublic/nodes.d.ts +16 -3
  75. package/dist/internal/sharingPublic/nodes.js +34 -2
  76. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  77. package/dist/internal/sharingPublic/unauthApiService.d.ts +17 -0
  78. package/dist/internal/sharingPublic/unauthApiService.js +31 -0
  79. package/dist/internal/sharingPublic/unauthApiService.js.map +1 -0
  80. package/dist/internal/sharingPublic/unauthApiService.test.d.ts +1 -0
  81. package/dist/internal/sharingPublic/unauthApiService.test.js +27 -0
  82. package/dist/internal/sharingPublic/unauthApiService.test.js.map +1 -0
  83. package/dist/internal/upload/apiService.d.ts +4 -3
  84. package/dist/internal/upload/apiService.js.map +1 -1
  85. package/dist/internal/upload/cryptoService.d.ts +8 -3
  86. package/dist/internal/upload/cryptoService.js +45 -9
  87. package/dist/internal/upload/cryptoService.js.map +1 -1
  88. package/dist/internal/upload/fileUploader.test.js +1 -1
  89. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  90. package/dist/internal/upload/interface.d.ts +25 -13
  91. package/dist/internal/upload/manager.js +7 -4
  92. package/dist/internal/upload/manager.js.map +1 -1
  93. package/dist/internal/upload/manager.test.js +5 -4
  94. package/dist/internal/upload/manager.test.js.map +1 -1
  95. package/dist/internal/upload/streamUploader.js +9 -4
  96. package/dist/internal/upload/streamUploader.js.map +1 -1
  97. package/dist/internal/upload/streamUploader.test.js +8 -5
  98. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  99. package/dist/protonDriveClient.d.ts +11 -2
  100. package/dist/protonDriveClient.js +20 -4
  101. package/dist/protonDriveClient.js.map +1 -1
  102. package/dist/protonDrivePhotosClient.d.ts +8 -8
  103. package/dist/protonDrivePhotosClient.js +8 -9
  104. package/dist/protonDrivePhotosClient.js.map +1 -1
  105. package/dist/protonDrivePublicLinkClient.d.ts +9 -2
  106. package/dist/protonDrivePublicLinkClient.js +16 -5
  107. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  108. package/dist/transformers.d.ts +7 -2
  109. package/dist/transformers.js +37 -0
  110. package/dist/transformers.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/interface/index.ts +1 -0
  113. package/src/interface/nodes.ts +14 -11
  114. package/src/interface/photos.ts +67 -0
  115. package/src/internal/apiService/apiService.ts +2 -2
  116. package/src/internal/apiService/driveTypes.ts +1294 -517
  117. package/src/internal/apiService/errors.ts +5 -4
  118. package/src/internal/download/cryptoService.ts +13 -6
  119. package/src/internal/download/fileDownloader.ts +4 -2
  120. package/src/internal/download/index.ts +3 -0
  121. package/src/internal/errors.ts +4 -0
  122. package/src/internal/nodes/apiService.test.ts +7 -5
  123. package/src/internal/nodes/apiService.ts +210 -124
  124. package/src/internal/nodes/cache.test.ts +1 -0
  125. package/src/internal/nodes/cache.ts +32 -13
  126. package/src/internal/nodes/cryptoReporter.ts +3 -3
  127. package/src/internal/nodes/cryptoService.test.ts +380 -18
  128. package/src/internal/nodes/cryptoService.ts +77 -36
  129. package/src/internal/nodes/events.ts +2 -2
  130. package/src/internal/nodes/index.test.ts +1 -0
  131. package/src/internal/nodes/interface.ts +17 -2
  132. package/src/internal/nodes/nodesAccess.ts +99 -54
  133. package/src/internal/nodes/nodesManagement.test.ts +69 -14
  134. package/src/internal/nodes/nodesManagement.ts +94 -48
  135. package/src/internal/nodes/nodesRevisions.ts +3 -3
  136. package/src/internal/photos/albums.ts +2 -2
  137. package/src/internal/photos/index.ts +45 -3
  138. package/src/internal/photos/interface.ts +21 -9
  139. package/src/internal/photos/nodes.ts +233 -0
  140. package/src/internal/photos/timeline.test.ts +2 -2
  141. package/src/internal/photos/timeline.ts +2 -2
  142. package/src/internal/photos/upload.ts +3 -3
  143. package/src/internal/sharingPublic/index.ts +7 -3
  144. package/src/internal/sharingPublic/nodes.ts +43 -2
  145. package/src/internal/sharingPublic/unauthApiService.test.ts +29 -0
  146. package/src/internal/sharingPublic/unauthApiService.ts +32 -0
  147. package/src/internal/upload/apiService.ts +4 -3
  148. package/src/internal/upload/cryptoService.ts +73 -12
  149. package/src/internal/upload/fileUploader.test.ts +1 -1
  150. package/src/internal/upload/interface.ts +24 -13
  151. package/src/internal/upload/manager.test.ts +5 -4
  152. package/src/internal/upload/manager.ts +7 -4
  153. package/src/internal/upload/streamUploader.test.ts +8 -5
  154. package/src/internal/upload/streamUploader.ts +10 -4
  155. package/src/protonDriveClient.ts +27 -5
  156. package/src/protonDrivePhotosClient.ts +23 -23
  157. package/src/protonDrivePublicLinkClient.ts +19 -3
  158. package/src/transformers.ts +49 -2
@@ -0,0 +1,233 @@
1
+ import { PrivateKey } from '../../crypto';
2
+ import { DecryptionError } from '../../errors';
3
+ import { NodeType } from '../../interface';
4
+ import { drivePaths } from '../apiService';
5
+ import { NodeAPIServiceBase, linkToEncryptedNode, linkToEncryptedNodeBaseMetadata } from '../nodes/apiService';
6
+ import { NodesCacheBase, serialiseNode, deserialiseNode } from '../nodes/cache';
7
+ import { NodesCryptoService } from '../nodes/cryptoService';
8
+ import { DecryptedNodeKeys } from '../nodes/interface';
9
+ import { NodesAccessBase, parseNode as parseNodeBase } from '../nodes/nodesAccess';
10
+ import { NodesManagementBase } from '../nodes/nodesManagement';
11
+ import { makeNodeUid } from '../uids';
12
+ import { EncryptedPhotoNode, DecryptedPhotoNode, DecryptedUnparsedPhotoNode } from './interface';
13
+
14
+ type PostLoadLinksMetadataRequest = Extract<
15
+ drivePaths['/drive/photos/volumes/{volumeID}/links']['post']['requestBody'],
16
+ { content: object }
17
+ >['content']['application/json'];
18
+ type PostLoadLinksMetadataResponse =
19
+ drivePaths['/drive/photos/volumes/{volumeID}/links']['post']['responses']['200']['content']['application/json'];
20
+
21
+ export class PhotosNodesAPIService extends NodeAPIServiceBase<
22
+ EncryptedPhotoNode,
23
+ PostLoadLinksMetadataResponse['Links'][0]
24
+ > {
25
+ protected async fetchNodeMetadata(volumeId: string, linkIds: string[], signal?: AbortSignal) {
26
+ const response = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(
27
+ `drive/photos/volumes/${volumeId}/links`,
28
+ {
29
+ LinkIDs: linkIds,
30
+ },
31
+ signal,
32
+ );
33
+ return response.Links;
34
+ }
35
+
36
+ protected linkToEncryptedNode(
37
+ volumeId: string,
38
+ link: PostLoadLinksMetadataResponse['Links'][0],
39
+ isOwnVolumeId: boolean,
40
+ ): EncryptedPhotoNode {
41
+ const { baseNodeMetadata, baseCryptoNodeMetadata } = linkToEncryptedNodeBaseMetadata(
42
+ this.logger,
43
+ volumeId,
44
+ link,
45
+ isOwnVolumeId,
46
+ );
47
+
48
+ if (link.Link.Type === 2 && link.Photo && link.Photo.ActiveRevision) {
49
+ const node = linkToEncryptedNode(
50
+ this.logger,
51
+ volumeId,
52
+ { ...link, File: link.Photo, Folder: null },
53
+ isOwnVolumeId,
54
+ );
55
+ return {
56
+ ...node,
57
+ type: NodeType.Photo,
58
+ photo: {
59
+ captureTime: new Date(link.Photo.CaptureTime * 1000),
60
+ mainPhotoNodeUid: link.Photo.MainPhotoLinkID
61
+ ? makeNodeUid(volumeId, link.Photo.MainPhotoLinkID)
62
+ : undefined,
63
+ relatedPhotoNodeUids: link.Photo.RelatedPhotosLinkIDs.map((relatedLinkId) =>
64
+ makeNodeUid(volumeId, relatedLinkId),
65
+ ),
66
+ contentHash: link.Photo.ContentHash || undefined,
67
+ tags: link.Photo.Tags,
68
+ albums: link.Photo.Albums.map((album) => ({
69
+ nodeUid: makeNodeUid(volumeId, album.AlbumLinkID),
70
+ additionTime: new Date(album.AddedTime * 1000),
71
+ nameHash: album.Hash,
72
+ contentHash: album.ContentHash,
73
+ })),
74
+ },
75
+ };
76
+ }
77
+
78
+ if (link.Link.Type === 3) {
79
+ return {
80
+ ...baseNodeMetadata,
81
+ encryptedCrypto: {
82
+ ...baseCryptoNodeMetadata,
83
+ },
84
+ };
85
+ }
86
+
87
+ const baseLink = {
88
+ Link: link.Link,
89
+ Membership: link.Membership,
90
+ Sharing: link.Sharing,
91
+ // @ts-expect-error The photo link can have a folder type, but not always. If not set, it will use other paths.
92
+ Folder: link.Folder,
93
+ File: null, // The photo link metadata never returns a file type.
94
+ };
95
+ return linkToEncryptedNode(this.logger, volumeId, baseLink, isOwnVolumeId);
96
+ }
97
+ }
98
+
99
+ export class PhotosNodesCache extends NodesCacheBase<DecryptedPhotoNode> {
100
+ serialiseNode(node: DecryptedPhotoNode): string {
101
+ return serialiseNode(node);
102
+ }
103
+
104
+ // TODO: use better deserialisation with validation
105
+ deserialiseNode(nodeData: string): DecryptedPhotoNode {
106
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
+ const node = deserialiseNode(nodeData) as any;
108
+
109
+ if (
110
+ !node ||
111
+ typeof node !== 'object' ||
112
+ (typeof node.photo !== 'object' && node.photo !== undefined) ||
113
+ (typeof node.photo?.captureTime !== 'string' && node.folder?.captureTime !== undefined) ||
114
+ (typeof node.photo?.albums !== 'object' && node.photo?.albums !== undefined)
115
+ ) {
116
+ throw new Error(`Invalid node data: ${nodeData}`);
117
+ }
118
+
119
+ return {
120
+ ...node,
121
+ photo: !node.photo
122
+ ? undefined
123
+ : {
124
+ captureTime: new Date(node.photo.captureTime),
125
+ mainPhotoNodeUid: node.photo.mainPhotoNodeUid,
126
+ relatedPhotoNodeUids: node.photo.relatedPhotoNodeUids,
127
+ contentHash: node.photo.contentHash,
128
+ tags: node.photo.tags,
129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
+ albums: node.photo.albums?.map((album: any) => ({
131
+ nodeUid: album.nodeUid,
132
+ additionTime: new Date(album.additionTime),
133
+ })),
134
+ },
135
+ } as DecryptedPhotoNode;
136
+ }
137
+ }
138
+
139
+ export class PhotosNodesAccess extends NodesAccessBase<EncryptedPhotoNode, DecryptedPhotoNode, PhotosNodesCryptoService> {
140
+ async getParentKeys(
141
+ node: Pick<EncryptedPhotoNode, 'uid' | 'parentUid' | 'shareId' | 'photo'>,
142
+ ): Promise<Pick<DecryptedNodeKeys, 'key' | 'hashKey'>> {
143
+ if (node.parentUid || node.shareId) {
144
+ return super.getParentKeys(node);
145
+ }
146
+
147
+ if (node.photo?.albums.length) {
148
+ // If photo is in multiple albums, we just need to get keys for one of them.
149
+ // Prefer to find a cached key first.
150
+ for (const album of node.photo.albums) {
151
+ try {
152
+ const keys = await this.cryptoCache.getNodeKeys(album.nodeUid);
153
+ return {
154
+ key: keys.key,
155
+ hashKey: keys.hashKey,
156
+ };
157
+ } catch {
158
+ // We ignore missing or invalid keys here, its just optimization.
159
+ // If it cannot be fixed, it will bubble up later when requesting
160
+ // the node keys for one of the albums.
161
+ }
162
+ }
163
+
164
+ const albumNodeUid = node.photo.albums[0].nodeUid;
165
+ return this.getNodeKeys(albumNodeUid);
166
+ }
167
+
168
+ // This is bug that should not happen.
169
+ // API cannot provide node without parent or share or album.
170
+ throw new Error('Node has neither parent node nor share nor album');
171
+ }
172
+
173
+ protected getDegradedUndecryptableNode(
174
+ encryptedNode: EncryptedPhotoNode,
175
+ error: DecryptionError,
176
+ ): DecryptedPhotoNode {
177
+ return this.getDegradedUndecryptableNodeBase(encryptedNode, error);
178
+ }
179
+
180
+ protected parseNode(unparsedNode: DecryptedUnparsedPhotoNode): DecryptedPhotoNode {
181
+ if (unparsedNode.type === NodeType.Photo) {
182
+ const node = parseNodeBase(this.logger, {
183
+ ...unparsedNode,
184
+ type: NodeType.File,
185
+ });
186
+ return {
187
+ ...node,
188
+ photo: unparsedNode.photo,
189
+ };
190
+ }
191
+
192
+ return parseNodeBase(this.logger, unparsedNode);
193
+ }
194
+ }
195
+
196
+ export class PhotosNodesCryptoService extends NodesCryptoService {
197
+ async decryptNode(
198
+ encryptedNode: EncryptedPhotoNode,
199
+ parentKey: PrivateKey,
200
+ ): Promise<{ node: DecryptedUnparsedPhotoNode; keys?: DecryptedNodeKeys }> {
201
+ const decryptedNode = await super.decryptNode(encryptedNode, parentKey);
202
+
203
+ if (decryptedNode.node.type === NodeType.Photo) {
204
+ return {
205
+ node: {
206
+ ...decryptedNode.node,
207
+ photo: encryptedNode.photo,
208
+ },
209
+ };
210
+ }
211
+
212
+ return decryptedNode;
213
+ }
214
+ }
215
+
216
+ export class PhotosNodesManagement extends NodesManagementBase<
217
+ EncryptedPhotoNode,
218
+ DecryptedPhotoNode,
219
+ PhotosNodesCryptoService
220
+ > {
221
+ protected generateNodeFolder(
222
+ nodeUid: string,
223
+ parentNodeUid: string,
224
+ name: string,
225
+ encryptedCrypto: {
226
+ hash: string;
227
+ encryptedName: string;
228
+ signatureEmail: string | null;
229
+ },
230
+ ): DecryptedPhotoNode {
231
+ return this.generateNodeFolderBase(nodeUid, parentNodeUid, name, encryptedCrypto);
232
+ }
233
+ }
@@ -2,7 +2,7 @@ import { getMockLogger } from '../../tests/logger';
2
2
  import { DriveCrypto } from '../../crypto';
3
3
  import { makeNodeUid } from '../uids';
4
4
  import { PhotosAPIService } from './apiService';
5
- import { NodesService } from './interface';
5
+ import { PhotosNodesAccess } from './nodes';
6
6
  import { PhotoSharesManager } from './shares';
7
7
  import { PhotosTimeline } from './timeline';
8
8
 
@@ -11,7 +11,7 @@ describe('PhotosTimeline', () => {
11
11
  let apiService: PhotosAPIService;
12
12
  let driveCrypto: DriveCrypto;
13
13
  let photoShares: PhotoSharesManager;
14
- let nodesService: NodesService;
14
+ let nodesService: PhotosNodesAccess;
15
15
  let timeline: PhotosTimeline;
16
16
 
17
17
  const volumeId = 'volumeId';
@@ -2,7 +2,7 @@ import { DriveCrypto } from '../../crypto';
2
2
  import { Logger } from '../../interface';
3
3
  import { makeNodeUid } from '../uids';
4
4
  import { PhotosAPIService } from './apiService';
5
- import { NodesService } from './interface';
5
+ import { PhotosNodesAccess } from './nodes';
6
6
  import { PhotoSharesManager } from './shares';
7
7
 
8
8
  /**
@@ -14,7 +14,7 @@ export class PhotosTimeline {
14
14
  private apiService: PhotosAPIService,
15
15
  private driveCrypto: DriveCrypto,
16
16
  private photoShares: PhotoSharesManager,
17
- private nodesService: NodesService,
17
+ private nodesService: PhotosNodesAccess,
18
18
  ) {
19
19
  this.logger = logger;
20
20
  this.apiService = apiService;
@@ -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
  }
@@ -0,0 +1,29 @@
1
+ import { getUnauthEndpoint } from './unauthApiService';
2
+
3
+ describe('getUnauthEndpoint', () => {
4
+ it('should not change urls endpoints', () => {
5
+ expect(getUnauthEndpoint('drive/urls/anything')).toBe('drive/urls/anything');
6
+ expect(getUnauthEndpoint('drive/urls/drive/anything')).toBe('drive/urls/drive/anything');
7
+ expect(getUnauthEndpoint('drive/urls/drive/v2/anything')).toBe('drive/urls/drive/v2/anything');
8
+ });
9
+
10
+ it('should not change v2/urls endpoints', () => {
11
+ expect(getUnauthEndpoint('drive/v2/urls/anything')).toBe('drive/v2/urls/anything');
12
+ expect(getUnauthEndpoint('drive/v2/urls/drive/anything')).toBe('drive/v2/urls/drive/anything');
13
+ expect(getUnauthEndpoint('drive/v2/urls/drive/v2/anything')).toBe('drive/v2/urls/drive/v2/anything');
14
+ });
15
+
16
+ it('should put unauth prefix for v2 endpoints', () => {
17
+ expect(getUnauthEndpoint('drive/v2/anything')).toBe('drive/unauth/v2/anything');
18
+ expect(getUnauthEndpoint('drive/v2/drive/anything')).toBe('drive/unauth/v2/drive/anything');
19
+ expect(getUnauthEndpoint('drive/v2/drive/v2/anything')).toBe('drive/unauth/v2/drive/v2/anything');
20
+ });
21
+
22
+ it('should put unauth prefix for non-v2 endpoints', () => {
23
+ expect(getUnauthEndpoint('drive/anything')).toBe('drive/unauth/anything');
24
+ expect(getUnauthEndpoint('drive/anything/v2/anything')).toBe('drive/unauth/anything/v2/anything');
25
+ expect(getUnauthEndpoint('drive/anything/drive/anything')).toBe('drive/unauth/anything/drive/anything');
26
+ expect(getUnauthEndpoint('drive/anything/drive/v2/anything')).toBe('drive/unauth/anything/drive/v2/anything');
27
+ });
28
+ });
29
+
@@ -0,0 +1,32 @@
1
+ import { DriveAPIService } from '../apiService';
2
+
3
+ /**
4
+ * Drive API Service for public links.
5
+ *
6
+ * This service is used to make requests to the Drive API without
7
+ * authentication. The unauth context uses the same endpoint, but
8
+ * with an `unauth` prefix. The goal is to avoid the need to use
9
+ * different path and use the exact endpoint for both contexts.
10
+ * However, API has global logic for handling expired sessions that
11
+ * is not compatible with the unauth context. For this reason, this
12
+ * service is used to make requests to the Drive API for public
13
+ * link context in the mean time.
14
+ */
15
+ export class UnauthDriveAPIService extends DriveAPIService {
16
+ protected async makeRequest<RequestPayload, ResponsePayload>(
17
+ url: string,
18
+ method = 'GET',
19
+ data?: RequestPayload,
20
+ signal?: AbortSignal,
21
+ ): Promise<ResponsePayload> {
22
+ const unauthUrl = getUnauthEndpoint(url);
23
+ return super.makeRequest(unauthUrl, method, data, signal);
24
+ }
25
+ }
26
+
27
+ export function getUnauthEndpoint(url: string): string {
28
+ if (url.startsWith('drive/urls/') || url.startsWith('drive/v2/urls/')) {
29
+ return url;
30
+ }
31
+ return url.replace(/^drive\//, 'drive/unauth/');
32
+ }
@@ -1,6 +1,7 @@
1
1
  import { c } from 'ttag';
2
2
 
3
3
  import { base64StringToUint8Array, uint8ArrayToBase64String } from '../../crypto';
4
+ import { AnonymousUser } from '../../interface';
4
5
  import { APICodeError, DriveAPIService, drivePaths, isCodeOk } from '../apiService';
5
6
  import { splitNodeUid, makeNodeUid, splitNodeRevisionUid, makeNodeRevisionUid } from '../uids';
6
7
  import { UploadTokens } from './interface';
@@ -65,7 +66,7 @@ export class UploadAPIService {
65
66
  armoredNodePassphraseSignature: string;
66
67
  base64ContentKeyPacket: string;
67
68
  armoredContentKeyPacketSignature: string;
68
- signatureEmail: string;
69
+ signatureEmail: string | AnonymousUser;
69
70
  },
70
71
  ): Promise<{
71
72
  nodeUid: string;
@@ -150,7 +151,7 @@ export class UploadAPIService {
150
151
 
151
152
  async requestBlockUpload(
152
153
  draftNodeRevisionUid: string,
153
- addressId: string,
154
+ addressId: string | AnonymousUser,
154
155
  blocks: {
155
156
  contentBlocks: {
156
157
  index: number;
@@ -211,7 +212,7 @@ export class UploadAPIService {
211
212
  draftNodeRevisionUid: string,
212
213
  options: {
213
214
  armoredManifestSignature: string;
214
- signatureEmail: string;
215
+ signatureEmail: string | AnonymousUser;
215
216
  armoredExtendedAttributes?: string;
216
217
  },
217
218
  ): Promise<void> {
@@ -2,8 +2,15 @@ import { c } from 'ttag';
2
2
 
3
3
  import { DriveCrypto, PrivateKey, SessionKey } from '../../crypto';
4
4
  import { IntegrityError } from '../../errors';
5
- import { Thumbnail } from '../../interface';
6
- import { EncryptedBlock, EncryptedThumbnail, NodeCrypto, NodeRevisionDraftKeys, NodesService } from './interface';
5
+ import { Thumbnail, AnonymousUser } from '../../interface';
6
+ import {
7
+ EncryptedBlock,
8
+ EncryptedThumbnail,
9
+ NodeCrypto,
10
+ NodeCryptoSigningKeys,
11
+ NodeRevisionDraftKeys,
12
+ NodesService,
13
+ } from './interface';
7
14
 
8
15
  export class UploadCryptoService {
9
16
  constructor(
@@ -19,11 +26,15 @@ export class UploadCryptoService {
19
26
  parentKeys: { key: PrivateKey; hashKey: Uint8Array },
20
27
  name: string,
21
28
  ): Promise<NodeCrypto> {
22
- const signatureAddress = await this.nodesService.getRootNodeEmailKey(parentUid);
29
+ const signingKeys = await this.getSigningKeys({ parentNodeUid: parentUid });
30
+
31
+ if (!signingKeys.nameAndPassphraseSigningKey) {
32
+ throw new Error('Cannot create new node without a name and passphrase signing key');
33
+ }
23
34
 
24
35
  const [nodeKeys, { armoredNodeName }, hash] = await Promise.all([
25
- this.driveCrypto.generateKey([parentKeys.key], signatureAddress.addressKey),
26
- this.driveCrypto.encryptNodeName(name, undefined, parentKeys.key, signatureAddress.addressKey),
36
+ this.driveCrypto.generateKey([parentKeys.key], signingKeys.nameAndPassphraseSigningKey),
37
+ this.driveCrypto.encryptNodeName(name, undefined, parentKeys.key, signingKeys.nameAndPassphraseSigningKey),
27
38
  this.driveCrypto.generateLookupHash(name, parentKeys.hashKey),
28
39
  ]);
29
40
 
@@ -36,7 +47,57 @@ export class UploadCryptoService {
36
47
  encryptedName: armoredNodeName,
37
48
  hash,
38
49
  },
39
- signatureAddress,
50
+ signingKeys: {
51
+ email: signingKeys.email,
52
+ addressId: signingKeys.addressId,
53
+ nameAndPassphraseSigningKey: signingKeys.nameAndPassphraseSigningKey,
54
+ contentSigningKey: signingKeys.contentSigningKey || nodeKeys.decrypted.key,
55
+ },
56
+ };
57
+ }
58
+
59
+ async getSigningKeysForExistingNode(uids: {
60
+ nodeUid: string;
61
+ parentNodeUid?: string;
62
+ }): Promise<NodeCryptoSigningKeys> {
63
+ const signingKeys = await this.getSigningKeys(uids);
64
+
65
+ if (!signingKeys.nameAndPassphraseSigningKey) {
66
+ throw new Error('Cannot get name and passphrase signing key for existing node');
67
+ }
68
+ if (!signingKeys.contentSigningKey) {
69
+ throw new Error('Cannot get content signing key for existing node');
70
+ }
71
+
72
+ return {
73
+ email: signingKeys.email,
74
+ addressId: signingKeys.addressId,
75
+ nameAndPassphraseSigningKey: signingKeys.nameAndPassphraseSigningKey,
76
+ contentSigningKey: signingKeys.contentSigningKey,
77
+ };
78
+ }
79
+
80
+ private async getSigningKeys(
81
+ uids: { nodeUid: string; parentNodeUid?: string } | { nodeUid?: string; parentNodeUid: string },
82
+ ): Promise<
83
+ Omit<NodeCryptoSigningKeys, 'nameAndPassphraseSigningKey' | 'contentSigningKey'> & {
84
+ nameAndPassphraseSigningKey?: PrivateKey;
85
+ contentSigningKey?: PrivateKey;
86
+ }
87
+ > {
88
+ const signingKeys = await this.nodesService.getNodeSigningKeys(uids);
89
+
90
+ const email = signingKeys.type === 'userAddress' ? signingKeys.email : null;
91
+ const addressId = signingKeys.type === 'userAddress' ? signingKeys.addressId : null;
92
+ const nameAndPassphraseSigningKey =
93
+ signingKeys.type === 'userAddress' ? signingKeys.key : signingKeys.parentNodeKey;
94
+ const contentSigningKey = signingKeys.type === 'userAddress' ? signingKeys.key : signingKeys.nodeKey;
95
+
96
+ return {
97
+ email,
98
+ addressId,
99
+ nameAndPassphraseSigningKey,
100
+ contentSigningKey,
40
101
  };
41
102
  }
42
103
 
@@ -47,7 +108,7 @@ export class UploadCryptoService {
47
108
  const { encryptedData } = await this.driveCrypto.encryptThumbnailBlock(
48
109
  thumbnail.thumbnail,
49
110
  nodeRevisionDraftKeys.contentKeyPacketSessionKey,
50
- nodeRevisionDraftKeys.signatureAddress.addressKey,
111
+ nodeRevisionDraftKeys.signingKeys.contentSigningKey,
51
112
  );
52
113
 
53
114
  const digest = await crypto.subtle.digest('SHA-256', encryptedData);
@@ -71,7 +132,7 @@ export class UploadCryptoService {
71
132
  block,
72
133
  nodeRevisionDraftKeys.key,
73
134
  nodeRevisionDraftKeys.contentKeyPacketSessionKey,
74
- nodeRevisionDraftKeys.signatureAddress.addressKey,
135
+ nodeRevisionDraftKeys.signingKeys.contentSigningKey,
75
136
  );
76
137
 
77
138
  const digest = await crypto.subtle.digest('SHA-256', encryptedData);
@@ -94,25 +155,25 @@ export class UploadCryptoService {
94
155
  extendedAttributes?: string,
95
156
  ): Promise<{
96
157
  armoredManifestSignature: string;
97
- signatureEmail: string;
158
+ signatureEmail: string | AnonymousUser;
98
159
  armoredExtendedAttributes?: string;
99
160
  }> {
100
161
  const { armoredManifestSignature } = await this.driveCrypto.signManifest(
101
162
  manifest,
102
- nodeRevisionDraftKeys.signatureAddress.addressKey,
163
+ nodeRevisionDraftKeys.signingKeys.contentSigningKey,
103
164
  );
104
165
 
105
166
  const { armoredExtendedAttributes } = extendedAttributes
106
167
  ? await this.driveCrypto.encryptExtendedAttributes(
107
168
  extendedAttributes,
108
169
  nodeRevisionDraftKeys.key,
109
- nodeRevisionDraftKeys.signatureAddress.addressKey,
170
+ nodeRevisionDraftKeys.signingKeys.contentSigningKey,
110
171
  )
111
172
  : { armoredExtendedAttributes: undefined };
112
173
 
113
174
  return {
114
175
  armoredManifestSignature,
115
- signatureEmail: nodeRevisionDraftKeys.signatureAddress.email,
176
+ signatureEmail: nodeRevisionDraftKeys.signingKeys.email,
116
177
  armoredExtendedAttributes,
117
178
  };
118
179
  }
@@ -110,7 +110,7 @@ describe('FileUploader', () => {
110
110
  nodeRevisionUid: 'revisionUid',
111
111
  nodeUid: 'nodeUid',
112
112
  nodeKeys: {
113
- signatureAddress: { addressId: 'addressId' },
113
+ signingKeys: { addressId: 'addressId' },
114
114
  },
115
115
  } as NodeRevisionDraft;
116
116