@protontech/drive-sdk 0.3.0 → 0.3.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 (121) hide show
  1. package/dist/crypto/interface.d.ts +5 -0
  2. package/dist/diagnostic/httpClient.d.ts +3 -3
  3. package/dist/interface/httpClient.d.ts +5 -5
  4. package/dist/interface/index.d.ts +15 -5
  5. package/dist/internal/apiService/apiService.js +1 -1
  6. package/dist/internal/apiService/apiService.js.map +1 -1
  7. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  8. package/dist/internal/apiService/errorCodes.js.map +1 -1
  9. package/dist/internal/apiService/errors.d.ts +4 -3
  10. package/dist/internal/apiService/errors.js +7 -4
  11. package/dist/internal/apiService/errors.js.map +1 -1
  12. package/dist/internal/apiService/errors.test.js +2 -1
  13. package/dist/internal/apiService/errors.test.js.map +1 -1
  14. package/dist/internal/events/index.d.ts +1 -1
  15. package/dist/internal/nodes/cryptoCache.js +6 -7
  16. package/dist/internal/nodes/cryptoCache.js.map +1 -1
  17. package/dist/internal/nodes/cryptoCache.test.js +4 -7
  18. package/dist/internal/nodes/cryptoCache.test.js.map +1 -1
  19. package/dist/internal/shares/cryptoCache.d.ts +4 -3
  20. package/dist/internal/shares/cryptoCache.js +23 -6
  21. package/dist/internal/shares/cryptoCache.js.map +1 -1
  22. package/dist/internal/shares/cryptoCache.test.js +3 -2
  23. package/dist/internal/shares/cryptoCache.test.js.map +1 -1
  24. package/dist/internal/shares/index.js +1 -1
  25. package/dist/internal/shares/index.js.map +1 -1
  26. package/dist/internal/sharing/cryptoService.js +8 -6
  27. package/dist/internal/sharing/cryptoService.js.map +1 -1
  28. package/dist/internal/sharing/cryptoService.test.js +13 -0
  29. package/dist/internal/sharing/cryptoService.test.js.map +1 -1
  30. package/dist/internal/sharing/index.js +1 -1
  31. package/dist/internal/sharing/index.js.map +1 -1
  32. package/dist/internal/sharing/sharingManagement.d.ts +3 -1
  33. package/dist/internal/sharing/sharingManagement.js +10 -1
  34. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  35. package/dist/internal/sharing/sharingManagement.test.js +32 -1
  36. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  37. package/dist/internal/sharingPublic/apiService.d.ts +19 -0
  38. package/dist/internal/sharingPublic/apiService.js +134 -0
  39. package/dist/internal/sharingPublic/apiService.js.map +1 -0
  40. package/dist/internal/sharingPublic/cryptoCache.d.ts +19 -0
  41. package/dist/internal/sharingPublic/cryptoCache.js +72 -0
  42. package/dist/internal/sharingPublic/cryptoCache.js.map +1 -0
  43. package/dist/internal/sharingPublic/cryptoService.d.ts +23 -0
  44. package/dist/internal/sharingPublic/cryptoService.js +120 -0
  45. package/dist/internal/sharingPublic/cryptoService.js.map +1 -0
  46. package/dist/internal/sharingPublic/index.d.ts +15 -0
  47. package/dist/internal/sharingPublic/index.js +27 -0
  48. package/dist/internal/sharingPublic/index.js.map +1 -0
  49. package/dist/internal/sharingPublic/interface.d.ts +48 -0
  50. package/dist/internal/sharingPublic/interface.js +3 -0
  51. package/dist/internal/sharingPublic/interface.js.map +1 -0
  52. package/dist/internal/sharingPublic/manager.d.ts +19 -0
  53. package/dist/internal/sharingPublic/manager.js +79 -0
  54. package/dist/internal/sharingPublic/manager.js.map +1 -0
  55. package/dist/internal/sharingPublic/session/apiService.d.ts +28 -0
  56. package/dist/internal/sharingPublic/session/apiService.js +55 -0
  57. package/dist/internal/sharingPublic/session/apiService.js.map +1 -0
  58. package/dist/internal/sharingPublic/session/httpClient.d.ts +16 -0
  59. package/dist/internal/sharingPublic/session/httpClient.js +41 -0
  60. package/dist/internal/sharingPublic/session/httpClient.js.map +1 -0
  61. package/dist/internal/sharingPublic/session/index.d.ts +1 -0
  62. package/dist/internal/sharingPublic/session/index.js +6 -0
  63. package/dist/internal/sharingPublic/session/index.js.map +1 -0
  64. package/dist/internal/sharingPublic/session/interface.d.ts +18 -0
  65. package/dist/internal/sharingPublic/session/interface.js +3 -0
  66. package/dist/internal/sharingPublic/session/interface.js.map +1 -0
  67. package/dist/internal/sharingPublic/session/manager.d.ts +49 -0
  68. package/dist/internal/sharingPublic/session/manager.js +75 -0
  69. package/dist/internal/sharingPublic/session/manager.js.map +1 -0
  70. package/dist/internal/sharingPublic/session/session.d.ts +34 -0
  71. package/dist/internal/sharingPublic/session/session.js +67 -0
  72. package/dist/internal/sharingPublic/session/session.js.map +1 -0
  73. package/dist/internal/sharingPublic/session/url.d.ts +12 -0
  74. package/dist/internal/sharingPublic/session/url.js +23 -0
  75. package/dist/internal/sharingPublic/session/url.js.map +1 -0
  76. package/dist/internal/sharingPublic/session/url.test.d.ts +1 -0
  77. package/dist/internal/sharingPublic/session/url.test.js +59 -0
  78. package/dist/internal/sharingPublic/session/url.test.js.map +1 -0
  79. package/dist/protonDriveClient.d.ts +18 -3
  80. package/dist/protonDriveClient.js +30 -8
  81. package/dist/protonDriveClient.js.map +1 -1
  82. package/dist/protonDrivePublicLinkClient.d.ts +48 -0
  83. package/dist/protonDrivePublicLinkClient.js +71 -0
  84. package/dist/protonDrivePublicLinkClient.js.map +1 -0
  85. package/package.json +1 -1
  86. package/src/crypto/interface.ts +11 -0
  87. package/src/diagnostic/httpClient.ts +4 -4
  88. package/src/interface/httpClient.ts +5 -5
  89. package/src/interface/index.ts +18 -6
  90. package/src/internal/apiService/apiService.ts +1 -1
  91. package/src/internal/apiService/errorCodes.ts +1 -0
  92. package/src/internal/apiService/errors.test.ts +2 -1
  93. package/src/internal/apiService/errors.ts +15 -4
  94. package/src/internal/events/index.ts +1 -1
  95. package/src/internal/nodes/cryptoCache.test.ts +4 -7
  96. package/src/internal/nodes/cryptoCache.ts +6 -7
  97. package/src/internal/nodes/interface.ts +2 -0
  98. package/src/internal/shares/cryptoCache.test.ts +3 -2
  99. package/src/internal/shares/cryptoCache.ts +26 -7
  100. package/src/internal/shares/index.ts +1 -1
  101. package/src/internal/sharing/cryptoService.test.ts +22 -1
  102. package/src/internal/sharing/cryptoService.ts +8 -6
  103. package/src/internal/sharing/index.ts +1 -0
  104. package/src/internal/sharing/sharingManagement.test.ts +33 -0
  105. package/src/internal/sharing/sharingManagement.ts +9 -0
  106. package/src/internal/sharingPublic/apiService.ts +164 -0
  107. package/src/internal/sharingPublic/cryptoCache.ts +79 -0
  108. package/src/internal/sharingPublic/cryptoService.ts +162 -0
  109. package/src/internal/sharingPublic/index.ts +40 -0
  110. package/src/internal/sharingPublic/interface.ts +59 -0
  111. package/src/internal/sharingPublic/manager.ts +85 -0
  112. package/src/internal/sharingPublic/session/apiService.ts +74 -0
  113. package/src/internal/sharingPublic/session/httpClient.ts +48 -0
  114. package/src/internal/sharingPublic/session/index.ts +1 -0
  115. package/src/internal/sharingPublic/session/interface.ts +20 -0
  116. package/src/internal/sharingPublic/session/manager.ts +97 -0
  117. package/src/internal/sharingPublic/session/session.ts +78 -0
  118. package/src/internal/sharingPublic/session/url.test.ts +72 -0
  119. package/src/internal/sharingPublic/session/url.ts +23 -0
  120. package/src/protonDriveClient.ts +47 -11
  121. package/src/protonDrivePublicLinkClient.ts +121 -0
@@ -0,0 +1,162 @@
1
+ import { DriveCrypto, PrivateKey } from '../../crypto';
2
+ import { resultOk, resultError, Result } from '../../interface';
3
+ import { getErrorMessage } from '../errors';
4
+ import { EncryptedShareCrypto, EncryptedNode, DecryptedNode, DecryptedNodeKeys } from './interface';
5
+
6
+ /**
7
+ * Provides crypto operations for public link data.
8
+ *
9
+ * The public link crypto service is responsible for decrypting and encrypting
10
+ * public link data. It should export high-level actions only, such as "decrypt
11
+ * share key" instead of low-level operations like "decrypt key". Low-level
12
+ * operations should be kept private to the module.
13
+ */
14
+ export class SharingPublicCryptoService {
15
+ constructor(
16
+ private driveCrypto: DriveCrypto,
17
+ private password: string,
18
+ ) {
19
+ this.driveCrypto = driveCrypto;
20
+ this.password = password;
21
+ }
22
+
23
+ async decryptShareKey(encryptedShare: EncryptedShareCrypto): Promise<PrivateKey> {
24
+ const { key: shareKey } = await this.driveCrypto.decryptKeyWithSrpPassword(
25
+ this.password,
26
+ encryptedShare.base64UrlPasswordSalt,
27
+ encryptedShare.armoredKey,
28
+ encryptedShare.armoredPassphrase,
29
+ );
30
+ return shareKey;
31
+ }
32
+
33
+ // TODO: verfiy it has all needed
34
+ async decryptNode(
35
+ node: EncryptedNode,
36
+ parentKey: PrivateKey,
37
+ ): Promise<{ node: DecryptedNode; keys?: DecryptedNodeKeys }> {
38
+ const commonNodeMetadata = {
39
+ ...node,
40
+ encryptedCrypto: undefined,
41
+ };
42
+
43
+ const { name } = await this.decryptName(node, parentKey);
44
+
45
+ let passphrase, key, passphraseSessionKey;
46
+ try {
47
+ const keyResult = await this.decryptKey(node, parentKey);
48
+ passphrase = keyResult.passphrase;
49
+ key = keyResult.key;
50
+ passphraseSessionKey = keyResult.passphraseSessionKey;
51
+ } catch (error: unknown) {
52
+ return {
53
+ node: {
54
+ ...commonNodeMetadata,
55
+ name,
56
+ errors: [error],
57
+ },
58
+ };
59
+ }
60
+
61
+ const errors = [];
62
+
63
+ let hashKey;
64
+ if ('folder' in node.encryptedCrypto) {
65
+ try {
66
+ const hashKeyResult = await this.decryptHashKey(node, key);
67
+ hashKey = hashKeyResult.hashKey;
68
+ } catch (error: unknown) {
69
+ errors.push(error);
70
+ }
71
+ }
72
+
73
+ let contentKeyPacketSessionKey;
74
+ if ('file' in node.encryptedCrypto) {
75
+ try {
76
+ const keySessionKeyResult = await this.driveCrypto.decryptAndVerifySessionKey(
77
+ node.encryptedCrypto.file.base64ContentKeyPacket,
78
+ '',
79
+ key,
80
+ [],
81
+ );
82
+
83
+ contentKeyPacketSessionKey = keySessionKeyResult.sessionKey;
84
+ } catch (error: unknown) {
85
+ errors.push(error);
86
+ }
87
+ }
88
+
89
+ return {
90
+ node: {
91
+ ...commonNodeMetadata,
92
+ name,
93
+ errors: errors.length ? errors : undefined,
94
+ },
95
+ keys: {
96
+ passphrase,
97
+ key,
98
+ passphraseSessionKey,
99
+ contentKeyPacketSessionKey,
100
+ hashKey,
101
+ },
102
+ };
103
+ }
104
+
105
+ private async decryptKey(node: EncryptedNode, parentKey: PrivateKey): Promise<DecryptedNodeKeys> {
106
+ const key = await this.driveCrypto.decryptKey(
107
+ node.encryptedCrypto.armoredKey,
108
+ node.encryptedCrypto.armoredNodePassphrase,
109
+ '',
110
+ [parentKey],
111
+ [],
112
+ );
113
+
114
+ return {
115
+ passphrase: key.passphrase,
116
+ key: key.key,
117
+ passphraseSessionKey: key.passphraseSessionKey,
118
+ };
119
+ }
120
+
121
+ private async decryptName(
122
+ node: EncryptedNode,
123
+ parentKey: PrivateKey,
124
+ ): Promise<{
125
+ name: Result<string, Error>;
126
+ }> {
127
+ try {
128
+ const { name } = await this.driveCrypto.decryptNodeName(node.encryptedName, parentKey, []);
129
+
130
+ return {
131
+ name: resultOk(name),
132
+ };
133
+ } catch (error: unknown) {
134
+ const errorMessage = getErrorMessage(error);
135
+ return {
136
+ name: resultError(new Error(errorMessage)),
137
+ };
138
+ }
139
+ }
140
+
141
+ private async decryptHashKey(
142
+ node: EncryptedNode,
143
+ nodeKey: PrivateKey,
144
+ ): Promise<{
145
+ hashKey: Uint8Array;
146
+ }> {
147
+ if (!('folder' in node.encryptedCrypto)) {
148
+ // This is developer error.
149
+ throw new Error('Node is not a folder');
150
+ }
151
+
152
+ const { hashKey } = await this.driveCrypto.decryptNodeHashKey(
153
+ node.encryptedCrypto.folder.armoredHashKey,
154
+ nodeKey,
155
+ [],
156
+ );
157
+
158
+ return {
159
+ hashKey,
160
+ };
161
+ }
162
+ }
@@ -0,0 +1,40 @@
1
+ import { DriveCrypto } from '../../crypto';
2
+ import { ProtonDriveCryptoCache, ProtonDriveTelemetry } from '../../interface';
3
+ import { DriveAPIService } from '../apiService';
4
+ import { SharingPublicAPIService } from './apiService';
5
+ import { SharingPublicCryptoCache } from './cryptoCache';
6
+ import { SharingPublicCryptoService } from './cryptoService';
7
+ import { SharingPublicManager } from './manager';
8
+
9
+ export { SharingPublicSessionManager } from './session/manager';
10
+
11
+ /**
12
+ * Provides facade for the whole sharing public module.
13
+ *
14
+ * The sharing public module is responsible for handling public link data, including
15
+ * API communication, encryption, decryption, and caching.
16
+ *
17
+ * This facade provides internal interface that other modules can use to
18
+ * interact with the public links.
19
+ */
20
+ export function initSharingPublicModule(
21
+ telemetry: ProtonDriveTelemetry,
22
+ apiService: DriveAPIService,
23
+ driveCryptoCache: ProtonDriveCryptoCache,
24
+ driveCrypto: DriveCrypto,
25
+ token: string,
26
+ password: string,
27
+ ) {
28
+ const api = new SharingPublicAPIService(telemetry.getLogger('sharingPublic-api'), apiService);
29
+ const cryptoCache = new SharingPublicCryptoCache(telemetry.getLogger('sharingPublic-crypto'), driveCryptoCache);
30
+ const cryptoService = new SharingPublicCryptoService(driveCrypto, password);
31
+ const manager = new SharingPublicManager(
32
+ telemetry.getLogger('sharingPublic-nodes'),
33
+ api,
34
+ cryptoCache,
35
+ cryptoService,
36
+ token,
37
+ );
38
+
39
+ return manager;
40
+ }
@@ -0,0 +1,59 @@
1
+ import { PrivateKey, SessionKey } from "../../crypto";
2
+ import { NodeType, Result, InvalidNameError } from "../../interface";
3
+
4
+ export interface EncryptedShareCrypto {
5
+ base64UrlPasswordSalt: string;
6
+ armoredKey: string;
7
+ armoredPassphrase: string;
8
+ }
9
+
10
+ // TODO: reuse node entity, or keep custom?
11
+ interface BaseNode {
12
+ // Internal metadata
13
+ hash?: string; // root node doesn't have any hash
14
+ encryptedName: string;
15
+
16
+ // Basic node metadata
17
+ uid: string;
18
+ parentUid?: string;
19
+ type: NodeType;
20
+ mediaType?: string;
21
+ totalStorageSize?: number;
22
+ }
23
+
24
+ export interface EncryptedNode extends BaseNode {
25
+ encryptedCrypto: EncryptedNodeFolderCrypto | EncryptedNodeFileCrypto;
26
+ }
27
+
28
+ export interface EncryptedNodeCrypto {
29
+ signatureEmail?: string;
30
+ armoredKey: string;
31
+ armoredNodePassphrase: string;
32
+ armoredNodePassphraseSignature?: string;
33
+ }
34
+
35
+ export interface EncryptedNodeFileCrypto extends EncryptedNodeCrypto {
36
+ file: {
37
+ base64ContentKeyPacket: string;
38
+ };
39
+ }
40
+
41
+ export interface EncryptedNodeFolderCrypto extends EncryptedNodeCrypto {
42
+ folder: {
43
+ armoredExtendedAttributes?: string;
44
+ armoredHashKey: string;
45
+ };
46
+ }
47
+
48
+ export interface DecryptedNode extends BaseNode {
49
+ name: Result<string, Error | InvalidNameError>;
50
+ errors?: unknown[];
51
+ }
52
+
53
+ export interface DecryptedNodeKeys {
54
+ passphrase: string;
55
+ key: PrivateKey;
56
+ passphraseSessionKey: SessionKey;
57
+ contentKeyPacketSessionKey?: SessionKey;
58
+ hashKey?: Uint8Array;
59
+ }
@@ -0,0 +1,85 @@
1
+ import { PrivateKey } from '../../crypto';
2
+ import { Logger } from '../../interface';
3
+ import { SharingPublicAPIService } from './apiService';
4
+ import { SharingPublicCryptoCache } from './cryptoCache';
5
+ import { SharingPublicCryptoService } from './cryptoService';
6
+ import { EncryptedShareCrypto, EncryptedNode, DecryptedNode, DecryptedNodeKeys } from './interface';
7
+
8
+ // TODO: comment
9
+ export class SharingPublicManager {
10
+ constructor(
11
+ private logger: Logger,
12
+ private api: SharingPublicAPIService,
13
+ private cryptoCache: SharingPublicCryptoCache,
14
+ private cryptoService: SharingPublicCryptoService,
15
+ private token: string,
16
+ ) {
17
+ this.logger = logger;
18
+ this.api = api;
19
+ this.cryptoCache = cryptoCache;
20
+ this.cryptoService = cryptoService;
21
+ this.token = token;
22
+ }
23
+
24
+ async getRootNode(): Promise<DecryptedNode> {
25
+ const { encryptedNode, encryptedShare } = await this.api.getPublicLinkRoot(this.token);
26
+ await this.decryptShare(encryptedShare);
27
+ return this.decryptNode(encryptedNode);
28
+ }
29
+
30
+ async *iterateChildren(parentUid: string): AsyncGenerator<DecryptedNode> {
31
+ // TODO: optimise this - decrypt in parallel
32
+ for await (const node of this.api.iterateChildren(parentUid)) {
33
+ const decryptedNode = await this.decryptNode(node);
34
+ yield decryptedNode;
35
+ }
36
+ }
37
+
38
+ private async decryptShare(encryptedShare: EncryptedShareCrypto): Promise<void> {
39
+ const shareKey = await this.cryptoService.decryptShareKey(encryptedShare);
40
+ await this.cryptoCache.setShareKey(shareKey);
41
+ }
42
+
43
+ private async decryptNode(encryptedNode: EncryptedNode): Promise<DecryptedNode> {
44
+ const parentKey = await this.getParentKey(encryptedNode);
45
+
46
+ const { node: decryptedNode, keys } = await this.cryptoService.decryptNode(encryptedNode, parentKey);
47
+
48
+ // TODO: cache of metadata?
49
+
50
+ if (keys) {
51
+ try {
52
+ await this.cryptoCache.setNodeKeys(decryptedNode.uid, keys);
53
+ } catch (error: unknown) {
54
+ this.logger.error(`Failed to cache node keys ${decryptedNode.uid}`, error);
55
+ }
56
+ }
57
+
58
+ return decryptedNode;
59
+ }
60
+
61
+ private async getParentKey(node: Pick<DecryptedNode, 'parentUid'>): Promise<PrivateKey> {
62
+ if (node.parentUid) {
63
+ // TODO: try-catch
64
+ const keys = await this.getNodeKeys(node.parentUid);
65
+ return keys.key;
66
+ }
67
+
68
+ try {
69
+ return await this.cryptoCache.getShareKey();
70
+ } catch {
71
+ await this.getRootNode();
72
+ return this.cryptoCache.getShareKey();
73
+ }
74
+ }
75
+
76
+ async getNodeKeys(nodeUid: string): Promise<DecryptedNodeKeys> {
77
+ try {
78
+ const keys = await this.cryptoCache.getNodeKeys(nodeUid);
79
+ return keys;
80
+ } catch {
81
+ // TODO: handle this
82
+ throw new Error('Node key not found in cache');
83
+ }
84
+ }
85
+ }
@@ -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
+ }