@protontech/drive-sdk 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/crypto/driveCrypto.d.ts +1 -1
  2. package/dist/crypto/driveCrypto.js.map +1 -1
  3. package/dist/crypto/interface.d.ts +6 -1
  4. package/dist/crypto/openPGPCrypto.d.ts +1 -1
  5. package/dist/crypto/openPGPCrypto.js +4 -1
  6. package/dist/crypto/openPGPCrypto.js.map +1 -1
  7. package/dist/diagnostic/httpClient.d.ts +3 -3
  8. package/dist/interface/httpClient.d.ts +5 -5
  9. package/dist/interface/index.d.ts +15 -5
  10. package/dist/internal/apiService/apiService.js +1 -1
  11. package/dist/internal/apiService/apiService.js.map +1 -1
  12. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  13. package/dist/internal/apiService/errorCodes.js.map +1 -1
  14. package/dist/internal/apiService/errors.d.ts +4 -3
  15. package/dist/internal/apiService/errors.js +7 -4
  16. package/dist/internal/apiService/errors.js.map +1 -1
  17. package/dist/internal/apiService/errors.test.js +2 -1
  18. package/dist/internal/apiService/errors.test.js.map +1 -1
  19. package/dist/internal/download/cryptoService.js +2 -2
  20. package/dist/internal/download/cryptoService.js.map +1 -1
  21. package/dist/internal/download/fileDownloader.js +2 -2
  22. package/dist/internal/download/fileDownloader.js.map +1 -1
  23. package/dist/internal/download/fileDownloader.test.js +3 -1
  24. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  25. package/dist/internal/events/index.d.ts +1 -1
  26. package/dist/internal/nodes/cache.js +3 -1
  27. package/dist/internal/nodes/cache.js.map +1 -1
  28. package/dist/internal/nodes/cryptoCache.js +6 -7
  29. package/dist/internal/nodes/cryptoCache.js.map +1 -1
  30. package/dist/internal/nodes/cryptoCache.test.js +4 -7
  31. package/dist/internal/nodes/cryptoCache.test.js.map +1 -1
  32. package/dist/internal/nodes/cryptoReporter.d.ts +20 -0
  33. package/dist/internal/nodes/cryptoReporter.js +96 -0
  34. package/dist/internal/nodes/cryptoReporter.js.map +1 -0
  35. package/dist/internal/nodes/cryptoService.d.ts +17 -12
  36. package/dist/internal/nodes/cryptoService.js +17 -97
  37. package/dist/internal/nodes/cryptoService.js.map +1 -1
  38. package/dist/internal/nodes/cryptoService.test.js +3 -1
  39. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  40. package/dist/internal/nodes/index.js +3 -1
  41. package/dist/internal/nodes/index.js.map +1 -1
  42. package/dist/internal/nodes/interface.d.ts +1 -1
  43. package/dist/internal/nodes/nodesAccess.d.ts +2 -2
  44. package/dist/internal/nodes/nodesAccess.js +52 -54
  45. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  46. package/dist/internal/shares/cryptoCache.d.ts +4 -3
  47. package/dist/internal/shares/cryptoCache.js +23 -6
  48. package/dist/internal/shares/cryptoCache.js.map +1 -1
  49. package/dist/internal/shares/cryptoCache.test.js +3 -2
  50. package/dist/internal/shares/cryptoCache.test.js.map +1 -1
  51. package/dist/internal/shares/index.js +1 -1
  52. package/dist/internal/shares/index.js.map +1 -1
  53. package/dist/internal/sharing/cache.d.ts +3 -0
  54. package/dist/internal/sharing/cache.js +17 -2
  55. package/dist/internal/sharing/cache.js.map +1 -1
  56. package/dist/internal/sharing/cryptoService.js +8 -6
  57. package/dist/internal/sharing/cryptoService.js.map +1 -1
  58. package/dist/internal/sharing/cryptoService.test.js +13 -0
  59. package/dist/internal/sharing/cryptoService.test.js.map +1 -1
  60. package/dist/internal/sharing/index.js +1 -1
  61. package/dist/internal/sharing/index.js.map +1 -1
  62. package/dist/internal/sharing/interface.d.ts +1 -1
  63. package/dist/internal/sharing/interface.js +1 -1
  64. package/dist/internal/sharing/sharingAccess.js +6 -0
  65. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  66. package/dist/internal/sharing/sharingAccess.test.js +242 -33
  67. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  68. package/dist/internal/sharing/sharingManagement.d.ts +3 -1
  69. package/dist/internal/sharing/sharingManagement.js +10 -1
  70. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  71. package/dist/internal/sharing/sharingManagement.test.js +32 -1
  72. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  73. package/dist/internal/sharingPublic/apiService.d.ts +19 -0
  74. package/dist/internal/sharingPublic/apiService.js +141 -0
  75. package/dist/internal/sharingPublic/apiService.js.map +1 -0
  76. package/dist/internal/sharingPublic/cryptoCache.d.ts +19 -0
  77. package/dist/internal/sharingPublic/cryptoCache.js +72 -0
  78. package/dist/internal/sharingPublic/cryptoCache.js.map +1 -0
  79. package/dist/internal/sharingPublic/cryptoService.d.ts +9 -0
  80. package/dist/internal/sharingPublic/cryptoService.js +57 -0
  81. package/dist/internal/sharingPublic/cryptoService.js.map +1 -0
  82. package/dist/internal/sharingPublic/index.d.ts +15 -0
  83. package/dist/internal/sharingPublic/index.js +27 -0
  84. package/dist/internal/sharingPublic/index.js.map +1 -0
  85. package/dist/internal/sharingPublic/interface.d.ts +6 -0
  86. package/dist/internal/sharingPublic/interface.js +3 -0
  87. package/dist/internal/sharingPublic/interface.js.map +1 -0
  88. package/dist/internal/sharingPublic/manager.d.ts +19 -0
  89. package/dist/internal/sharingPublic/manager.js +81 -0
  90. package/dist/internal/sharingPublic/manager.js.map +1 -0
  91. package/dist/internal/sharingPublic/session/apiService.d.ts +28 -0
  92. package/dist/internal/sharingPublic/session/apiService.js +55 -0
  93. package/dist/internal/sharingPublic/session/apiService.js.map +1 -0
  94. package/dist/internal/sharingPublic/session/httpClient.d.ts +16 -0
  95. package/dist/internal/sharingPublic/session/httpClient.js +41 -0
  96. package/dist/internal/sharingPublic/session/httpClient.js.map +1 -0
  97. package/dist/internal/sharingPublic/session/index.d.ts +1 -0
  98. package/dist/internal/sharingPublic/session/index.js +6 -0
  99. package/dist/internal/sharingPublic/session/index.js.map +1 -0
  100. package/dist/internal/sharingPublic/session/interface.d.ts +18 -0
  101. package/dist/internal/sharingPublic/session/interface.js +3 -0
  102. package/dist/internal/sharingPublic/session/interface.js.map +1 -0
  103. package/dist/internal/sharingPublic/session/manager.d.ts +49 -0
  104. package/dist/internal/sharingPublic/session/manager.js +75 -0
  105. package/dist/internal/sharingPublic/session/manager.js.map +1 -0
  106. package/dist/internal/sharingPublic/session/session.d.ts +34 -0
  107. package/dist/internal/sharingPublic/session/session.js +67 -0
  108. package/dist/internal/sharingPublic/session/session.js.map +1 -0
  109. package/dist/internal/sharingPublic/session/url.d.ts +12 -0
  110. package/dist/internal/sharingPublic/session/url.js +23 -0
  111. package/dist/internal/sharingPublic/session/url.js.map +1 -0
  112. package/dist/internal/sharingPublic/session/url.test.d.ts +1 -0
  113. package/dist/internal/sharingPublic/session/url.test.js +59 -0
  114. package/dist/internal/sharingPublic/session/url.test.js.map +1 -0
  115. package/dist/internal/upload/streamUploader.js +1 -1
  116. package/dist/internal/upload/streamUploader.js.map +1 -1
  117. package/dist/internal/upload/streamUploader.test.js +3 -1
  118. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  119. package/dist/protonDriveClient.d.ts +18 -3
  120. package/dist/protonDriveClient.js +31 -8
  121. package/dist/protonDriveClient.js.map +1 -1
  122. package/dist/protonDrivePublicLinkClient.d.ts +57 -0
  123. package/dist/protonDrivePublicLinkClient.js +73 -0
  124. package/dist/protonDrivePublicLinkClient.js.map +1 -0
  125. package/package.json +1 -1
  126. package/src/crypto/driveCrypto.ts +1 -1
  127. package/src/crypto/interface.ts +12 -1
  128. package/src/crypto/openPGPCrypto.ts +5 -2
  129. package/src/diagnostic/httpClient.ts +4 -4
  130. package/src/interface/httpClient.ts +5 -5
  131. package/src/interface/index.ts +18 -6
  132. package/src/internal/apiService/apiService.ts +1 -1
  133. package/src/internal/apiService/errorCodes.ts +1 -0
  134. package/src/internal/apiService/errors.test.ts +2 -1
  135. package/src/internal/apiService/errors.ts +15 -4
  136. package/src/internal/download/cryptoService.ts +2 -2
  137. package/src/internal/download/fileDownloader.test.ts +3 -1
  138. package/src/internal/download/fileDownloader.ts +2 -2
  139. package/src/internal/events/index.ts +1 -1
  140. package/src/internal/nodes/cache.ts +3 -1
  141. package/src/internal/nodes/cryptoCache.test.ts +4 -7
  142. package/src/internal/nodes/cryptoCache.ts +6 -7
  143. package/src/internal/nodes/cryptoReporter.ts +145 -0
  144. package/src/internal/nodes/cryptoService.test.ts +3 -1
  145. package/src/internal/nodes/cryptoService.ts +44 -137
  146. package/src/internal/nodes/index.ts +3 -1
  147. package/src/internal/nodes/interface.ts +3 -1
  148. package/src/internal/nodes/nodesAccess.ts +59 -61
  149. package/src/internal/shares/cryptoCache.test.ts +3 -2
  150. package/src/internal/shares/cryptoCache.ts +26 -7
  151. package/src/internal/shares/index.ts +1 -1
  152. package/src/internal/sharing/cache.ts +19 -2
  153. package/src/internal/sharing/cryptoService.test.ts +22 -1
  154. package/src/internal/sharing/cryptoService.ts +8 -6
  155. package/src/internal/sharing/index.ts +1 -0
  156. package/src/internal/sharing/interface.ts +1 -1
  157. package/src/internal/sharing/sharingAccess.test.ts +282 -34
  158. package/src/internal/sharing/sharingAccess.ts +6 -0
  159. package/src/internal/sharing/sharingManagement.test.ts +33 -0
  160. package/src/internal/sharing/sharingManagement.ts +9 -0
  161. package/src/internal/sharingPublic/apiService.ts +173 -0
  162. package/src/internal/sharingPublic/cryptoCache.ts +79 -0
  163. package/src/internal/sharingPublic/cryptoService.ts +98 -0
  164. package/src/internal/sharingPublic/index.ts +41 -0
  165. package/src/internal/sharingPublic/interface.ts +14 -0
  166. package/src/internal/sharingPublic/manager.ts +86 -0
  167. package/src/internal/sharingPublic/session/apiService.ts +74 -0
  168. package/src/internal/sharingPublic/session/httpClient.ts +48 -0
  169. package/src/internal/sharingPublic/session/index.ts +1 -0
  170. package/src/internal/sharingPublic/session/interface.ts +20 -0
  171. package/src/internal/sharingPublic/session/manager.ts +97 -0
  172. package/src/internal/sharingPublic/session/session.ts +78 -0
  173. package/src/internal/sharingPublic/session/url.test.ts +72 -0
  174. package/src/internal/sharingPublic/session/url.ts +23 -0
  175. package/src/internal/upload/streamUploader.test.ts +3 -1
  176. package/src/internal/upload/streamUploader.ts +1 -1
  177. package/src/protonDriveClient.ts +48 -11
  178. package/src/protonDrivePublicLinkClient.ts +135 -0
@@ -0,0 +1,86 @@
1
+ import { PrivateKey } from '../../crypto';
2
+ import { Logger } from '../../interface';
3
+ import { parseNode } from '../nodes/nodesAccess';
4
+ import { SharingPublicAPIService } from './apiService';
5
+ import { SharingPublicCryptoCache } from './cryptoCache';
6
+ import { SharingPublicCryptoService } from './cryptoService';
7
+ import { EncryptedShareCrypto, EncryptedNode, DecryptedNode, DecryptedNodeKeys } from './interface';
8
+
9
+ // TODO: comment
10
+ export class SharingPublicManager {
11
+ constructor(
12
+ private logger: Logger,
13
+ private api: SharingPublicAPIService,
14
+ private cryptoCache: SharingPublicCryptoCache,
15
+ private cryptoService: SharingPublicCryptoService,
16
+ private token: string,
17
+ ) {
18
+ this.logger = logger;
19
+ this.api = api;
20
+ this.cryptoCache = cryptoCache;
21
+ this.cryptoService = cryptoService;
22
+ this.token = token;
23
+ }
24
+
25
+ async getRootNode(): Promise<DecryptedNode> {
26
+ const { encryptedNode, encryptedShare } = await this.api.getPublicLinkRoot(this.token);
27
+ await this.decryptShare(encryptedShare);
28
+ return this.decryptNode(encryptedNode);
29
+ }
30
+
31
+ async *iterateFolderChildren(parentUid: string, signal?: AbortSignal): AsyncGenerator<DecryptedNode> {
32
+ // TODO: optimise this - decrypt in parallel
33
+ for await (const node of this.api.iterateFolderChildren(parentUid, signal)) {
34
+ const decryptedNode = await this.decryptNode(node);
35
+ yield decryptedNode;
36
+ }
37
+ }
38
+
39
+ private async decryptShare(encryptedShare: EncryptedShareCrypto): Promise<void> {
40
+ const shareKey = await this.cryptoService.decryptPublicLinkShareKey(encryptedShare);
41
+ await this.cryptoCache.setShareKey(shareKey);
42
+ }
43
+
44
+ private async decryptNode(encryptedNode: EncryptedNode): Promise<DecryptedNode> {
45
+ const parentKey = await this.getParentKey(encryptedNode);
46
+
47
+ const { node: unparsedNode, keys } = await this.cryptoService.decryptNode(encryptedNode, parentKey);
48
+ const node = await parseNode(this.logger, unparsedNode);
49
+
50
+ // TODO: cache of metadata?
51
+ if (keys) {
52
+ try {
53
+ await this.cryptoCache.setNodeKeys(node.uid, keys);
54
+ } catch (error: unknown) {
55
+ this.logger.error(`Failed to cache node keys ${node.uid}`, error);
56
+ }
57
+ }
58
+
59
+ return node;
60
+ }
61
+
62
+ private async getParentKey(node: Pick<DecryptedNode, 'parentUid'>): Promise<PrivateKey> {
63
+ if (node.parentUid) {
64
+ // TODO: try-catch
65
+ const keys = await this.getNodeKeys(node.parentUid);
66
+ return keys.key;
67
+ }
68
+
69
+ try {
70
+ return await this.cryptoCache.getShareKey();
71
+ } catch {
72
+ await this.getRootNode();
73
+ return this.cryptoCache.getShareKey();
74
+ }
75
+ }
76
+
77
+ async getNodeKeys(nodeUid: string): Promise<DecryptedNodeKeys> {
78
+ try {
79
+ const keys = await this.cryptoCache.getNodeKeys(nodeUid);
80
+ return keys;
81
+ } catch {
82
+ // TODO: handle this
83
+ throw new Error('Node key not found in cache');
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,74 @@
1
+ import { DriveAPIService, drivePaths } from '../../apiService';
2
+ import { PublicLinkInfo, PublicLinkSrpAuth } from './interface';
3
+
4
+ type GetPublicLinkInfoResponse =
5
+ drivePaths['/drive/urls/{token}/info']['get']['responses']['200']['content']['application/json'];
6
+
7
+ type PostPublicLinkAuthRequest = Extract<
8
+ drivePaths['/drive/urls/{token}/auth']['post']['requestBody'],
9
+ { content: object }
10
+ >['content']['application/json'];
11
+ type PostPublicLinkAuthResponse =
12
+ drivePaths['/drive/urls/{token}/auth']['post']['responses']['200']['content']['application/json'];
13
+
14
+ /**
15
+ * Provides API communication for managing public link session (not data).
16
+ *
17
+ * The service is responsible for transforming local objects to API payloads
18
+ * and vice versa. It should not contain any business logic.
19
+ */
20
+ export class SharingPublicSessionAPIService {
21
+ constructor(private apiService: DriveAPIService) {
22
+ this.apiService = apiService;
23
+ }
24
+
25
+ /**
26
+ * Start a SRP handshake for public link session.
27
+ */
28
+ async initPublicLinkSession(token: string): Promise<PublicLinkInfo> {
29
+ const response = await this.apiService.get<GetPublicLinkInfoResponse>(`drive/urls/${token}/info`);
30
+ return {
31
+ srp: {
32
+ version: response.Version,
33
+ modulus: response.Modulus,
34
+ serverEphemeral: response.ServerEphemeral,
35
+ salt: response.UrlPasswordSalt,
36
+ srpSession: response.SRPSession,
37
+ },
38
+ isCustomPasswordProtected: (response.Flags & 1) === 1,
39
+ isLegacy: response.Flags === 0 || response.Flags === 1,
40
+ vendorType: response.VendorType,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Authenticate a public link session.
46
+ *
47
+ * It returns the server proof that must be validated, and the session uid
48
+ * with an optional access token. The access token is only returned if
49
+ * the session is newly created.
50
+ */
51
+ async authPublicLinkSession(
52
+ token: string,
53
+ srp: PublicLinkSrpAuth,
54
+ ): Promise<{
55
+ serverProof: string;
56
+ sessionUid: string;
57
+ sessionAccessToken?: string;
58
+ }> {
59
+ const response = await this.apiService.post<PostPublicLinkAuthRequest, PostPublicLinkAuthResponse>(
60
+ `drive/urls/${token}/auth`,
61
+ {
62
+ ClientProof: srp.clientProof,
63
+ ClientEphemeral: srp.clientEphemeral,
64
+ SRPSession: srp.srpSession,
65
+ },
66
+ );
67
+
68
+ return {
69
+ serverProof: response.ServerProof,
70
+ sessionUid: response.UID,
71
+ sessionAccessToken: response.AccessToken,
72
+ };
73
+ }
74
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ ProtonDriveHTTPClient,
3
+ ProtonDriveHTTPClientBlobRequest,
4
+ ProtonDriveHTTPClientJsonRequest,
5
+ } from '../../../interface';
6
+ import { HTTPErrorCode } from '../../apiService';
7
+ import { SharingPublicLinkSession } from './session';
8
+
9
+ /**
10
+ * HTTP client to get access to public link of given session.
11
+ *
12
+ * It is responsible for adding the session headers to the request if the session
13
+ * is authenticated, and re-authenticating the session if the session is expired.
14
+ */
15
+ export class SharingPublicSessionHttpClient implements ProtonDriveHTTPClient {
16
+ constructor(
17
+ private httpClient: ProtonDriveHTTPClient,
18
+ private session: SharingPublicLinkSession,
19
+ ) {
20
+ this.httpClient = httpClient;
21
+ this.session = session;
22
+ }
23
+
24
+ async fetchJson(options: ProtonDriveHTTPClientJsonRequest) {
25
+ const response = await this.httpClient.fetchJson(this.getOptionsWithSessionHeaders(options));
26
+
27
+ if (response.status === HTTPErrorCode.UNAUTHORIZED) {
28
+ await this.session.reauth();
29
+ return this.httpClient.fetchJson(this.getOptionsWithSessionHeaders(options));
30
+ }
31
+
32
+ return response;
33
+ }
34
+
35
+ async fetchBlob(options: ProtonDriveHTTPClientBlobRequest) {
36
+ return this.httpClient.fetchBlob(this.getOptionsWithSessionHeaders(options));
37
+ }
38
+
39
+ private getOptionsWithSessionHeaders(options: ProtonDriveHTTPClientJsonRequest) {
40
+ // Set headers if the session is newly created.
41
+ // This is needed only if the user is not logged in.
42
+ if (this.session.session.accessToken) {
43
+ options.headers.set('x-pm-uid', this.session.session.uid);
44
+ options.headers.set('Authorization', `Bearer ${this.session.session.accessToken}`);
45
+ }
46
+ return options;
47
+ }
48
+ }
@@ -0,0 +1 @@
1
+ export { SharingPublicSessionManager } from './manager';
@@ -0,0 +1,20 @@
1
+ export type PublicLinkInfo = {
2
+ srp: PublicLinkSrpInfo;
3
+ isCustomPasswordProtected: boolean;
4
+ isLegacy: boolean;
5
+ vendorType: number;
6
+ };
7
+
8
+ export type PublicLinkSrpInfo = {
9
+ version: number;
10
+ modulus: string;
11
+ serverEphemeral: string;
12
+ salt: string;
13
+ srpSession: string;
14
+ };
15
+
16
+ export type PublicLinkSrpAuth = {
17
+ clientProof: string;
18
+ clientEphemeral: string;
19
+ srpSession: string;
20
+ };
@@ -0,0 +1,97 @@
1
+ import { ProtonDriveHTTPClient } from '../../../interface';
2
+ import { SRPModule } from '../../../crypto';
3
+ import { DriveAPIService } from '../../apiService';
4
+ import { SharingPublicSessionAPIService } from './apiService';
5
+ import { SharingPublicSessionHttpClient } from './httpClient';
6
+ import { PublicLinkInfo } from './interface';
7
+ import { SharingPublicLinkSession } from './session';
8
+ import { getTokenAndPasswordFromUrl } from './url';
9
+
10
+ /**
11
+ * Manages sessions for public links.
12
+ *
13
+ * It can be used to get access to multiple public links.
14
+ */
15
+ export class SharingPublicSessionManager {
16
+ private api: SharingPublicSessionAPIService;
17
+
18
+ private infosPerToken: Map<string, PublicLinkInfo> = new Map();
19
+
20
+ constructor(
21
+ private httpClient: ProtonDriveHTTPClient,
22
+ apiService: DriveAPIService,
23
+ private srpModule: SRPModule,
24
+ ) {
25
+ this.httpClient = httpClient;
26
+ this.srpModule = srpModule;
27
+
28
+ this.api = new SharingPublicSessionAPIService(apiService);
29
+ }
30
+
31
+ /**
32
+ * Get the info for a public link.
33
+ *
34
+ * It returns the info for the public link, including if it is custom
35
+ * password protected, if it is legacy (not supported anymore), and
36
+ * the vendor type (whether it is Proton Docs, for example, and should
37
+ * be redirected to the public Docs app).
38
+ *
39
+ * @param url - The URL of the public link.
40
+ */
41
+ async getInfo(url: string): Promise<{
42
+ isCustomPasswordProtected: boolean;
43
+ isLegacy: boolean;
44
+ vendorType: number;
45
+ }> {
46
+ const { token } = getTokenAndPasswordFromUrl(url);
47
+
48
+ const info = await this.api.initPublicLinkSession(token);
49
+ this.infosPerToken.set(token, info);
50
+
51
+ return {
52
+ isCustomPasswordProtected: info.isCustomPasswordProtected,
53
+ isLegacy: info.isLegacy,
54
+ vendorType: info.vendorType,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Authenticate a public link session.
60
+ *
61
+ * It returns HTTP client that must be used for the endpoints to access the
62
+ * public link data.
63
+ *
64
+ * It returnes parsed token and full password (password from the URL +
65
+ * custom password) that can be used for decrypting the share key.
66
+ *
67
+ * @param url - The URL of the public link.
68
+ * @param customPassword - The custom password for the public link, if it is
69
+ * custom password protected.
70
+ */
71
+ async auth(
72
+ url: string,
73
+ customPassword?: string,
74
+ ): Promise<{
75
+ token: string;
76
+ password: string;
77
+ httpClient: SharingPublicSessionHttpClient;
78
+ }> {
79
+ const { token, password: urlPassword } = getTokenAndPasswordFromUrl(url);
80
+
81
+ let info = this.infosPerToken.get(token);
82
+ if (!info) {
83
+ info = await this.api.initPublicLinkSession(token);
84
+ }
85
+
86
+ const password = `${urlPassword}${customPassword || ''}`;
87
+
88
+ const session = new SharingPublicLinkSession(this.api, this.srpModule, token, password);
89
+ await session.auth(info.srp);
90
+
91
+ return {
92
+ token,
93
+ password,
94
+ httpClient: new SharingPublicSessionHttpClient(this.httpClient, session),
95
+ };
96
+ }
97
+ }
@@ -0,0 +1,78 @@
1
+ import { SRPModule } from "../../../crypto";
2
+ import { SharingPublicSessionAPIService } from "./apiService";
3
+ import { PublicLinkInfo, PublicLinkSrpInfo } from "./interface";
4
+
5
+ /**
6
+ * Session for a public link.
7
+ *
8
+ * It is responsible for initializing and authenticating the public link session
9
+ * with the SRP handshake. It also can re-authenticate the session if it is expired.
10
+ */
11
+ export class SharingPublicLinkSession {
12
+ private sessionUid?: string;
13
+ private sessionAccessToken?: string;
14
+
15
+ constructor(
16
+ private apiService: SharingPublicSessionAPIService,
17
+ private srpModule: SRPModule,
18
+ private token: string,
19
+ private password: string,
20
+ ) {
21
+ this.apiService = apiService;
22
+ this.srpModule = srpModule;
23
+ this.token = token;
24
+ this.password = password;
25
+ }
26
+
27
+ async reauth(): Promise<void> {
28
+ const info = await this.init();
29
+ await this.auth(info.srp);
30
+ }
31
+
32
+ async init(): Promise<PublicLinkInfo> {
33
+ return this.apiService.initPublicLinkSession(this.token);
34
+ }
35
+
36
+ async auth(srp: PublicLinkSrpInfo): Promise<void> {
37
+ const { expectedServerProof, clientProof, clientEphemeral } = await this.srpModule.getSrp(
38
+ srp.version,
39
+ srp.modulus,
40
+ srp.serverEphemeral,
41
+ srp.salt,
42
+ this.password,
43
+ );
44
+
45
+ const auth = await this.apiService.authPublicLinkSession(this.token, {
46
+ clientProof,
47
+ clientEphemeral,
48
+ srpSession: srp.srpSession,
49
+ });
50
+
51
+ if (auth.serverProof !== expectedServerProof) {
52
+ throw new Error('Invalid server proof');
53
+ }
54
+
55
+ this.sessionUid = auth.sessionUid;
56
+ this.sessionAccessToken = auth.sessionAccessToken;
57
+ }
58
+
59
+ /**
60
+ * Get the session uid and access token.
61
+ *
62
+ * The access token is only returned if the session is newly created.
63
+ * If the access token is not available, it means the existing session
64
+ * can be used to access the public link.
65
+ *
66
+ * @throws If the session is not initialized.
67
+ */
68
+ get session() {
69
+ if (!this.sessionUid) {
70
+ throw new Error('Session not initialized');
71
+ }
72
+
73
+ return {
74
+ uid: this.sessionUid,
75
+ accessToken: this.sessionAccessToken,
76
+ };
77
+ }
78
+ }
@@ -0,0 +1,72 @@
1
+ import { ValidationError } from '../../../errors';
2
+ import { getTokenAndPasswordFromUrl } from './url';
3
+
4
+ describe('getTokenAndPasswordFromUrl', () => {
5
+ describe('valid URLs', () => {
6
+ it('should extract token and password from a valid URL', () => {
7
+ const url = 'https://drive.proton.me/urls/abc123#def456';
8
+ const result = getTokenAndPasswordFromUrl(url);
9
+
10
+ expect(result).toEqual({
11
+ token: 'abc123',
12
+ password: 'def456'
13
+ });
14
+ });
15
+
16
+ it('should handle URLs with different domains', () => {
17
+ const url = 'https://example.com/urls/mytoken#mypassword';
18
+ const result = getTokenAndPasswordFromUrl(url);
19
+
20
+ expect(result).toEqual({
21
+ token: 'mytoken',
22
+ password: 'mypassword'
23
+ });
24
+ });
25
+
26
+ it('should handle URLs with query parameters', () => {
27
+ const url = 'https://drive.proton.me/urls/token123?param=value#password456';
28
+ const result = getTokenAndPasswordFromUrl(url);
29
+
30
+ expect(result).toEqual({
31
+ token: 'token123',
32
+ password: 'password456'
33
+ });
34
+ });
35
+ });
36
+
37
+ describe('should throw ValidationError', () => {
38
+ it('when token is missing (no path)', () => {
39
+ const url = 'https://drive.proton.me/#password123';
40
+
41
+ expect(() => getTokenAndPasswordFromUrl(url)).toThrow(ValidationError);
42
+ });
43
+
44
+ it('when token is missing (empty path segment)', () => {
45
+ const url = 'https://drive.proton.me/urls/#password123';
46
+
47
+ expect(() => getTokenAndPasswordFromUrl(url)).toThrow(ValidationError);
48
+ });
49
+
50
+ it('when password is missing (no hash)', () => {
51
+ const url = 'https://drive.proton.me/urls/token123';
52
+
53
+ expect(() => getTokenAndPasswordFromUrl(url)).toThrow(ValidationError);
54
+ expect(() => getTokenAndPasswordFromUrl(url)).toThrow('Invalid URL');
55
+ });
56
+
57
+ it('when password is empty (empty hash)', () => {
58
+ const url = 'https://drive.proton.me/urls/token123#';
59
+
60
+ expect(() => getTokenAndPasswordFromUrl(url)).toThrow(ValidationError);
61
+ expect(() => getTokenAndPasswordFromUrl(url)).toThrow('Invalid URL');
62
+ });
63
+
64
+ it('for empty string', () => {
65
+ expect(() => getTokenAndPasswordFromUrl('')).toThrow();
66
+ });
67
+
68
+ it('for invalid URL format', () => {
69
+ expect(() => getTokenAndPasswordFromUrl('not-a-url')).toThrow();
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,23 @@
1
+ import { c } from 'ttag';
2
+
3
+ import { ValidationError } from '../../../errors';
4
+
5
+ /**
6
+ * Parse the token and password from the URL.
7
+ *
8
+ * The URL format is: https://drive.proton.me/urls/token#password
9
+ *
10
+ * @param url - The URL of the public link.
11
+ * @returns The token and password.
12
+ */
13
+ export function getTokenAndPasswordFromUrl(url: string): { token: string; password: string } {
14
+ const urlObj = new URL(url);
15
+ const token = urlObj.pathname.split('/').pop();
16
+ const password = urlObj.hash.slice(1);
17
+
18
+ if (!token || !password) {
19
+ throw new ValidationError(c('Error').t`Invalid URL`);
20
+ }
21
+
22
+ return { token, password };
23
+ }
@@ -189,8 +189,10 @@ describe('StreamUploader', () => {
189
189
 
190
190
  const verifyOnProgress = async (uploadedBytes: number[]) => {
191
191
  expect(onProgress).toHaveBeenCalledTimes(uploadedBytes.length);
192
+ let fileProgress = 0;
192
193
  for (let i = 0; i < uploadedBytes.length; i++) {
193
- expect(onProgress).toHaveBeenNthCalledWith(i + 1, uploadedBytes[i]);
194
+ fileProgress += uploadedBytes[i];
195
+ expect(onProgress).toHaveBeenNthCalledWith(i + 1, fileProgress);
194
196
  }
195
197
  };
196
198
 
@@ -122,7 +122,7 @@ export class StreamUploader {
122
122
  this.logger.info(`Starting upload`);
123
123
  await this.encryptAndUploadBlocks(stream, thumbnails, (uploadedBytes) => {
124
124
  fileProgress += uploadedBytes;
125
- onProgress?.(uploadedBytes);
125
+ onProgress?.(fileProgress);
126
126
  });
127
127
 
128
128
  this.logger.debug(`All blocks uploaded, committing`);
@@ -1,3 +1,5 @@
1
+ import { getConfig } from './config';
2
+ import { DriveCrypto, SessionKey } from './crypto';
1
3
  import {
2
4
  Logger,
3
5
  ProtonDriveClientContructorParameters,
@@ -26,16 +28,6 @@ import {
26
28
  ThumbnailResult,
27
29
  SDKEvent,
28
30
  } from './interface';
29
- import { DriveCrypto, SessionKey } from './crypto';
30
- import { DriveAPIService } from './internal/apiService';
31
- import { initSharesModule } from './internal/shares';
32
- import { initNodesModule } from './internal/nodes';
33
- import { initSharingModule } from './internal/sharing';
34
- import { initDownloadModule } from './internal/download';
35
- import { initUploadModule } from './internal/upload';
36
- import { DriveEventsService, DriveListener } from './internal/events';
37
- import { SDKEvents } from './internal/sdkEvents';
38
- import { getConfig } from './config';
39
31
  import {
40
32
  getUid,
41
33
  getUids,
@@ -45,9 +37,18 @@ import {
45
37
  convertInternalNode,
46
38
  } from './transformers';
47
39
  import { Telemetry } from './telemetry';
40
+ import { DriveAPIService } from './internal/apiService';
48
41
  import { initDevicesModule } from './internal/devices';
42
+ import { initDownloadModule } from './internal/download';
43
+ import { DriveEventsService, DriveListener, EventSubscription } from './internal/events';
44
+ import { initNodesModule } from './internal/nodes';
45
+ import { SDKEvents } from './internal/sdkEvents';
46
+ import { initSharesModule } from './internal/shares';
47
+ import { initSharingModule } from './internal/sharing';
48
+ import { SharingPublicSessionManager } from './internal/sharingPublic';
49
+ import { initUploadModule } from './internal/upload';
49
50
  import { makeNodeUid } from './internal/uids';
50
- import { EventSubscription } from './internal/events/interface';
51
+ import { ProtonDrivePublicLinkClient } from './protonDrivePublicLinkClient';
51
52
 
52
53
  /**
53
54
  * ProtonDriveClient is the main interface for the ProtonDrive SDK.
@@ -66,6 +67,7 @@ export class ProtonDriveClient {
66
67
  private download: ReturnType<typeof initDownloadModule>;
67
68
  private upload: ReturnType<typeof initUploadModule>;
68
69
  private devices: ReturnType<typeof initDevicesModule>;
70
+ private sessionManager: SharingPublicSessionManager;
69
71
 
70
72
  public experimental: {
71
73
  /**
@@ -82,6 +84,20 @@ export class ProtonDriveClient {
82
84
  * This is used by Docs app to encrypt and decrypt document updates.
83
85
  */
84
86
  getDocsKey: (nodeUid: NodeOrUid) => Promise<SessionKey>;
87
+ /**
88
+ * Experimental feature to get the info for a public link
89
+ * required to authenticate the public link.
90
+ */
91
+ getPublicLinkInfo: (url: string) => Promise<{
92
+ isCustomPasswordProtected: boolean;
93
+ isLegacy: boolean;
94
+ vendorType: number;
95
+ }>;
96
+ /**
97
+ * Experimental feature to authenticate a public link and
98
+ * return the client for the public link to access it.
99
+ */
100
+ authPublicLink: (url: string, customPassword?: string) => Promise<ProtonDrivePublicLinkClient>;
85
101
  };
86
102
 
87
103
  constructor({
@@ -167,6 +183,8 @@ export class ProtonDriveClient {
167
183
  latestEventIdProvider,
168
184
  );
169
185
 
186
+ this.sessionManager = new SharingPublicSessionManager(httpClient, apiService, srpModule);
187
+
170
188
  this.experimental = {
171
189
  getNodeUrl: async (nodeUid: NodeOrUid) => {
172
190
  this.logger.debug(`Getting node URL for ${getUid(nodeUid)}`);
@@ -180,6 +198,25 @@ export class ProtonDriveClient {
180
198
  }
181
199
  return keys.contentKeyPacketSessionKey;
182
200
  },
201
+ getPublicLinkInfo: async (url: string) => {
202
+ this.logger.info(`Getting info for public link ${url}`);
203
+ return this.sessionManager.getInfo(url);
204
+ },
205
+ authPublicLink: async (url: string, customPassword?: string) => {
206
+ this.logger.info(`Authenticating public link ${url}`);
207
+ const { httpClient, token, password } = await this.sessionManager.auth(url, customPassword);
208
+ return new ProtonDrivePublicLinkClient({
209
+ httpClient,
210
+ cryptoCache,
211
+ account,
212
+ openPGPCryptoModule,
213
+ srpModule,
214
+ config,
215
+ telemetry,
216
+ token,
217
+ password,
218
+ });
219
+ },
183
220
  };
184
221
  }
185
222