@protontech/drive-sdk 0.2.1 → 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.
- package/dist/crypto/interface.d.ts +5 -0
- package/dist/diagnostic/httpClient.d.ts +3 -3
- package/dist/diagnostic/interface.d.ts +26 -29
- package/dist/diagnostic/sdkDiagnostic.js +50 -24
- package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +7 -7
- package/dist/errors.js.map +1 -1
- package/dist/interface/author.d.ts +1 -1
- package/dist/interface/events.d.ts +1 -1
- package/dist/interface/events.js.map +1 -1
- package/dist/interface/httpClient.d.ts +5 -5
- package/dist/interface/index.d.ts +15 -5
- package/dist/internal/apiService/apiService.js +12 -4
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/errorCodes.d.ts +1 -0
- package/dist/internal/apiService/errorCodes.js.map +1 -1
- package/dist/internal/apiService/errors.d.ts +4 -3
- package/dist/internal/apiService/errors.js +7 -4
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/apiService/errors.test.js +2 -1
- package/dist/internal/apiService/errors.test.js.map +1 -1
- package/dist/internal/events/index.d.ts +1 -1
- package/dist/internal/nodes/apiService.js +3 -0
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +18 -0
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cryptoCache.js +6 -7
- package/dist/internal/nodes/cryptoCache.js.map +1 -1
- package/dist/internal/nodes/cryptoCache.test.js +4 -7
- package/dist/internal/nodes/cryptoCache.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.js +44 -20
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.js +2 -2
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.js +0 -2
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/shares/cryptoCache.d.ts +4 -3
- package/dist/internal/shares/cryptoCache.js +23 -6
- package/dist/internal/shares/cryptoCache.js.map +1 -1
- package/dist/internal/shares/cryptoCache.test.js +3 -2
- package/dist/internal/shares/cryptoCache.test.js.map +1 -1
- package/dist/internal/shares/index.js +1 -1
- package/dist/internal/shares/index.js.map +1 -1
- package/dist/internal/sharing/cryptoService.js +8 -6
- package/dist/internal/sharing/cryptoService.js.map +1 -1
- package/dist/internal/sharing/cryptoService.test.js +13 -0
- package/dist/internal/sharing/cryptoService.test.js.map +1 -1
- package/dist/internal/sharing/index.js +1 -1
- package/dist/internal/sharing/index.js.map +1 -1
- package/dist/internal/sharing/interface.d.ts +0 -4
- package/dist/internal/sharing/sharingAccess.d.ts +1 -0
- package/dist/internal/sharing/sharingAccess.js +6 -1
- package/dist/internal/sharing/sharingAccess.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.test.js +3 -3
- package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.d.ts +3 -1
- package/dist/internal/sharing/sharingManagement.js +37 -17
- package/dist/internal/sharing/sharingManagement.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.test.js +61 -14
- package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
- package/dist/internal/sharingPublic/apiService.d.ts +19 -0
- package/dist/internal/sharingPublic/apiService.js +134 -0
- package/dist/internal/sharingPublic/apiService.js.map +1 -0
- package/dist/internal/sharingPublic/cryptoCache.d.ts +19 -0
- package/dist/internal/sharingPublic/cryptoCache.js +72 -0
- package/dist/internal/sharingPublic/cryptoCache.js.map +1 -0
- package/dist/internal/sharingPublic/cryptoService.d.ts +23 -0
- package/dist/internal/sharingPublic/cryptoService.js +120 -0
- package/dist/internal/sharingPublic/cryptoService.js.map +1 -0
- package/dist/internal/sharingPublic/index.d.ts +15 -0
- package/dist/internal/sharingPublic/index.js +27 -0
- package/dist/internal/sharingPublic/index.js.map +1 -0
- package/dist/internal/sharingPublic/interface.d.ts +48 -0
- package/dist/internal/sharingPublic/interface.js +3 -0
- package/dist/internal/sharingPublic/interface.js.map +1 -0
- package/dist/internal/sharingPublic/manager.d.ts +19 -0
- package/dist/internal/sharingPublic/manager.js +79 -0
- package/dist/internal/sharingPublic/manager.js.map +1 -0
- package/dist/internal/sharingPublic/session/apiService.d.ts +28 -0
- package/dist/internal/sharingPublic/session/apiService.js +55 -0
- package/dist/internal/sharingPublic/session/apiService.js.map +1 -0
- package/dist/internal/sharingPublic/session/httpClient.d.ts +16 -0
- package/dist/internal/sharingPublic/session/httpClient.js +41 -0
- package/dist/internal/sharingPublic/session/httpClient.js.map +1 -0
- package/dist/internal/sharingPublic/session/index.d.ts +1 -0
- package/dist/internal/sharingPublic/session/index.js +6 -0
- package/dist/internal/sharingPublic/session/index.js.map +1 -0
- package/dist/internal/sharingPublic/session/interface.d.ts +18 -0
- package/dist/internal/sharingPublic/session/interface.js +3 -0
- package/dist/internal/sharingPublic/session/interface.js.map +1 -0
- package/dist/internal/sharingPublic/session/manager.d.ts +49 -0
- package/dist/internal/sharingPublic/session/manager.js +75 -0
- package/dist/internal/sharingPublic/session/manager.js.map +1 -0
- package/dist/internal/sharingPublic/session/session.d.ts +34 -0
- package/dist/internal/sharingPublic/session/session.js +67 -0
- package/dist/internal/sharingPublic/session/session.js.map +1 -0
- package/dist/internal/sharingPublic/session/url.d.ts +12 -0
- package/dist/internal/sharingPublic/session/url.js +23 -0
- package/dist/internal/sharingPublic/session/url.js.map +1 -0
- package/dist/internal/sharingPublic/session/url.test.d.ts +1 -0
- package/dist/internal/sharingPublic/session/url.test.js +59 -0
- package/dist/internal/sharingPublic/session/url.test.js.map +1 -0
- package/dist/internal/upload/manager.js +1 -3
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +2 -2
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +25 -10
- package/dist/protonDriveClient.js +44 -22
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +48 -0
- package/dist/protonDrivePublicLinkClient.js +71 -0
- package/dist/protonDrivePublicLinkClient.js.map +1 -0
- package/package.json +1 -1
- package/src/crypto/interface.ts +11 -0
- package/src/diagnostic/httpClient.ts +4 -4
- package/src/diagnostic/interface.ts +27 -29
- package/src/diagnostic/sdkDiagnostic.ts +58 -30
- package/src/errors.ts +5 -5
- package/src/interface/author.ts +1 -1
- package/src/interface/events.ts +1 -7
- package/src/interface/httpClient.ts +5 -5
- package/src/interface/index.ts +18 -6
- package/src/internal/apiService/apiService.ts +13 -4
- package/src/internal/apiService/errorCodes.ts +1 -0
- package/src/internal/apiService/errors.test.ts +2 -1
- package/src/internal/apiService/errors.ts +15 -4
- package/src/internal/events/index.ts +1 -1
- package/src/internal/nodes/apiService.test.ts +28 -0
- package/src/internal/nodes/apiService.ts +3 -0
- package/src/internal/nodes/cryptoCache.test.ts +4 -7
- package/src/internal/nodes/cryptoCache.ts +6 -7
- package/src/internal/nodes/cryptoService.ts +68 -34
- package/src/internal/nodes/interface.ts +2 -0
- package/src/internal/nodes/nodesAccess.ts +2 -2
- package/src/internal/nodes/nodesManagement.ts +0 -3
- package/src/internal/shares/cryptoCache.test.ts +3 -2
- package/src/internal/shares/cryptoCache.ts +26 -7
- package/src/internal/shares/index.ts +1 -1
- package/src/internal/sharing/cryptoService.test.ts +22 -1
- package/src/internal/sharing/cryptoService.ts +8 -6
- package/src/internal/sharing/index.ts +1 -0
- package/src/internal/sharing/interface.ts +0 -4
- package/src/internal/sharing/sharingAccess.test.ts +4 -4
- package/src/internal/sharing/sharingAccess.ts +6 -0
- package/src/internal/sharing/sharingManagement.test.ts +87 -24
- package/src/internal/sharing/sharingManagement.ts +56 -16
- package/src/internal/sharingPublic/apiService.ts +164 -0
- package/src/internal/sharingPublic/cryptoCache.ts +79 -0
- package/src/internal/sharingPublic/cryptoService.ts +162 -0
- package/src/internal/sharingPublic/index.ts +40 -0
- package/src/internal/sharingPublic/interface.ts +59 -0
- package/src/internal/sharingPublic/manager.ts +85 -0
- package/src/internal/sharingPublic/session/apiService.ts +74 -0
- package/src/internal/sharingPublic/session/httpClient.ts +48 -0
- package/src/internal/sharingPublic/session/index.ts +1 -0
- package/src/internal/sharingPublic/session/interface.ts +20 -0
- package/src/internal/sharingPublic/session/manager.ts +97 -0
- package/src/internal/sharingPublic/session/session.ts +78 -0
- package/src/internal/sharingPublic/session/url.test.ts +72 -0
- package/src/internal/sharingPublic/session/url.ts +23 -0
- package/src/internal/upload/manager.test.ts +2 -2
- package/src/internal/upload/manager.ts +2 -4
- package/src/protonDriveClient.ts +64 -27
- package/src/protonDrivePublicLinkClient.ts +121 -0
|
@@ -61,6 +61,8 @@ export class NodesCryptoService {
|
|
|
61
61
|
node: EncryptedNode,
|
|
62
62
|
parentKey: PrivateKey,
|
|
63
63
|
): Promise<{ node: DecryptedUnparsedNode; keys?: DecryptedNodeKeys }> {
|
|
64
|
+
const start = Date.now();
|
|
65
|
+
|
|
64
66
|
const commonNodeMetadata = {
|
|
65
67
|
...node,
|
|
66
68
|
encryptedCrypto: undefined,
|
|
@@ -89,16 +91,17 @@ export class NodesCryptoService {
|
|
|
89
91
|
: nodeParentKeys;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
// Start promises early, but await them only when required to do
|
|
95
|
+
// as much work as possible in parallel.
|
|
96
|
+
const [membershipPromise, namePromise, keyPromise] = [
|
|
97
|
+
node.membership ? this.decryptMembership(node) : undefined,
|
|
98
|
+
this.decryptName(node, parentKey, nameVerificationKeys),
|
|
99
|
+
this.decryptKey(node, parentKey, keyVerificationKeys),
|
|
100
|
+
];
|
|
98
101
|
|
|
99
102
|
let passphrase, key, passphraseSessionKey, keyAuthor;
|
|
100
103
|
try {
|
|
101
|
-
const keyResult = await
|
|
104
|
+
const keyResult = await keyPromise;
|
|
102
105
|
passphrase = keyResult.passphrase;
|
|
103
106
|
key = keyResult.key;
|
|
104
107
|
passphraseSessionKey = keyResult.passphraseSessionKey;
|
|
@@ -107,12 +110,17 @@ export class NodesCryptoService {
|
|
|
107
110
|
void this.reportDecryptionError(node, 'nodeKey', error);
|
|
108
111
|
const message = getErrorMessage(error);
|
|
109
112
|
const errorMessage = c('Error').t`Failed to decrypt node key: ${message}`;
|
|
113
|
+
const { name, author: nameAuthor } = await namePromise;
|
|
114
|
+
const membership = await membershipPromise;
|
|
110
115
|
return {
|
|
111
116
|
node: {
|
|
112
117
|
...commonNodeMetadata,
|
|
113
118
|
name,
|
|
114
119
|
keyAuthor: resultError({
|
|
115
|
-
claimedAuthor:
|
|
120
|
+
claimedAuthor: getClaimedAuthor(
|
|
121
|
+
node.encryptedCrypto.signatureEmail,
|
|
122
|
+
keyVerificationKeys.length === 0,
|
|
123
|
+
),
|
|
116
124
|
error: errorMessage,
|
|
117
125
|
}),
|
|
118
126
|
nameAuthor,
|
|
@@ -131,8 +139,23 @@ export class NodesCryptoService {
|
|
|
131
139
|
let folder;
|
|
132
140
|
let folderExtendedAttributesAuthor;
|
|
133
141
|
if ('folder' in node.encryptedCrypto) {
|
|
142
|
+
const folderExtendedAttributesVerificationKeys = node.encryptedCrypto.signatureEmail
|
|
143
|
+
? signatureEmailKeys
|
|
144
|
+
: [key];
|
|
145
|
+
|
|
146
|
+
const [hashKeyPromise, folderExtendedAttributesPromise] = [
|
|
147
|
+
this.decryptHashKey(node, key, signatureEmailKeys),
|
|
148
|
+
this.decryptExtendedAttributes(
|
|
149
|
+
node,
|
|
150
|
+
node.encryptedCrypto.folder.armoredExtendedAttributes,
|
|
151
|
+
key,
|
|
152
|
+
folderExtendedAttributesVerificationKeys,
|
|
153
|
+
node.encryptedCrypto.signatureEmail,
|
|
154
|
+
),
|
|
155
|
+
];
|
|
156
|
+
|
|
134
157
|
try {
|
|
135
|
-
const hashKeyResult = await
|
|
158
|
+
const hashKeyResult = await hashKeyPromise;
|
|
136
159
|
hashKey = hashKeyResult.hashKey;
|
|
137
160
|
hashKeyAuthor = hashKeyResult.author;
|
|
138
161
|
} catch (error: unknown) {
|
|
@@ -141,16 +164,7 @@ export class NodesCryptoService {
|
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
try {
|
|
144
|
-
const
|
|
145
|
-
? signatureEmailKeys
|
|
146
|
-
: [key];
|
|
147
|
-
const extendedAttributesResult = await this.decryptExtendedAttributes(
|
|
148
|
-
node,
|
|
149
|
-
node.encryptedCrypto.folder.armoredExtendedAttributes,
|
|
150
|
-
key,
|
|
151
|
-
folderExtendedAttributesVerificationKeys,
|
|
152
|
-
node.encryptedCrypto.signatureEmail,
|
|
153
|
-
);
|
|
167
|
+
const extendedAttributesResult = await folderExtendedAttributesPromise;
|
|
154
168
|
folder = {
|
|
155
169
|
extendedAttributes: extendedAttributesResult.extendedAttributes,
|
|
156
170
|
};
|
|
@@ -165,10 +179,20 @@ export class NodesCryptoService {
|
|
|
165
179
|
let contentKeyPacketSessionKey;
|
|
166
180
|
let contentKeyPacketAuthor;
|
|
167
181
|
if ('file' in node.encryptedCrypto) {
|
|
182
|
+
const [activeRevisionPromise, contentKeyPacketSessionKeyPromise] = [
|
|
183
|
+
this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
|
|
184
|
+
this.driveCrypto.decryptAndVerifySessionKey(
|
|
185
|
+
node.encryptedCrypto.file.base64ContentKeyPacket,
|
|
186
|
+
node.encryptedCrypto.file.armoredContentKeyPacketSignature,
|
|
187
|
+
key,
|
|
188
|
+
// Content key packet is signed with the node key, but
|
|
189
|
+
// in the past some clients signed with the address key.
|
|
190
|
+
[key, ...keyVerificationKeys],
|
|
191
|
+
),
|
|
192
|
+
];
|
|
193
|
+
|
|
168
194
|
try {
|
|
169
|
-
activeRevision = resultOk(
|
|
170
|
-
await this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
|
|
171
|
-
);
|
|
195
|
+
activeRevision = resultOk(await activeRevisionPromise);
|
|
172
196
|
} catch (error: unknown) {
|
|
173
197
|
void this.reportDecryptionError(node, 'nodeExtendedAttributes', error);
|
|
174
198
|
const message = getErrorMessage(error);
|
|
@@ -177,18 +201,10 @@ export class NodesCryptoService {
|
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
try {
|
|
180
|
-
const keySessionKeyResult = await
|
|
181
|
-
node.encryptedCrypto.file.base64ContentKeyPacket,
|
|
182
|
-
node.encryptedCrypto.file.armoredContentKeyPacketSignature,
|
|
183
|
-
key,
|
|
184
|
-
// Content key packet is signed with the node key, but
|
|
185
|
-
// in the past some clients signed with the address key.
|
|
186
|
-
[key, ...keyVerificationKeys],
|
|
187
|
-
);
|
|
188
|
-
|
|
204
|
+
const keySessionKeyResult = await contentKeyPacketSessionKeyPromise;
|
|
189
205
|
contentKeyPacketSessionKey = keySessionKeyResult.sessionKey;
|
|
190
206
|
contentKeyPacketAuthor =
|
|
191
|
-
keySessionKeyResult.verified &&
|
|
207
|
+
keySessionKeyResult.verified !== undefined &&
|
|
192
208
|
(await this.handleClaimedAuthor(
|
|
193
209
|
node,
|
|
194
210
|
'nodeContentKey',
|
|
@@ -230,6 +246,13 @@ export class NodesCryptoService {
|
|
|
230
246
|
finalKeyAuthor = keyAuthor;
|
|
231
247
|
}
|
|
232
248
|
|
|
249
|
+
const { name, author: nameAuthor } = await namePromise;
|
|
250
|
+
const membership = await membershipPromise;
|
|
251
|
+
|
|
252
|
+
const end = Date.now();
|
|
253
|
+
const duration = end - start;
|
|
254
|
+
this.logger.debug(`Node ${node.uid} decrypted in ${duration}ms`);
|
|
255
|
+
|
|
233
256
|
return {
|
|
234
257
|
node: {
|
|
235
258
|
...commonNodeMetadata,
|
|
@@ -319,7 +342,7 @@ export class NodesCryptoService {
|
|
|
319
342
|
return {
|
|
320
343
|
name: resultError(new Error(errorMessage)),
|
|
321
344
|
author: resultError({
|
|
322
|
-
claimedAuthor: nameSignatureEmail,
|
|
345
|
+
claimedAuthor: getClaimedAuthor(nameSignatureEmail, verificationKeys.length === 0),
|
|
323
346
|
error: errorMessage,
|
|
324
347
|
}),
|
|
325
348
|
};
|
|
@@ -635,6 +658,7 @@ export class NodesCryptoService {
|
|
|
635
658
|
if (this.reportedVerificationErrors.has(node.uid)) {
|
|
636
659
|
return;
|
|
637
660
|
}
|
|
661
|
+
this.reportedVerificationErrors.add(node.uid);
|
|
638
662
|
|
|
639
663
|
const fromBefore2024 = node.creationTime < new Date('2024-01-01');
|
|
640
664
|
|
|
@@ -661,7 +685,6 @@ export class NodesCryptoService {
|
|
|
661
685
|
error: verificationErrors?.map((e) => e.message).join(', '),
|
|
662
686
|
uid: node.uid,
|
|
663
687
|
});
|
|
664
|
-
this.reportedVerificationErrors.add(node.uid);
|
|
665
688
|
}
|
|
666
689
|
|
|
667
690
|
private async reportDecryptionError(node: EncryptedNode, field: MetricsDecryptionErrorField, error: unknown) {
|
|
@@ -716,3 +739,14 @@ function handleClaimedAuthor(
|
|
|
716
739
|
error: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
|
|
717
740
|
});
|
|
718
741
|
}
|
|
742
|
+
|
|
743
|
+
function getClaimedAuthor(
|
|
744
|
+
claimedAuthor?: string,
|
|
745
|
+
notAvailableVerificationKeys = false,
|
|
746
|
+
): string | AnonymousUser | undefined {
|
|
747
|
+
if (!claimedAuthor && notAvailableVerificationKeys) {
|
|
748
|
+
return null as AnonymousUser;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return claimedAuthor;
|
|
752
|
+
}
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
interface BaseNode {
|
|
19
19
|
// Internal metadata
|
|
20
20
|
hash?: string; // root node doesn't have any hash
|
|
21
|
+
// ecnryptedName should not be needed to keep, nameSessionKey should be enough.
|
|
22
|
+
// We will improve this in the future.
|
|
21
23
|
encryptedName: string;
|
|
22
24
|
|
|
23
25
|
// Basic node metadata
|
|
@@ -24,7 +24,7 @@ const BATCH_LOADING_SIZE = 30;
|
|
|
24
24
|
// It is a trade-off between performance and memory usage.
|
|
25
25
|
// Higher number means more memory usage, but faster decryption.
|
|
26
26
|
// Lower number means less memory usage, but slower decryption.
|
|
27
|
-
const DECRYPTION_CONCURRENCY =
|
|
27
|
+
const DECRYPTION_CONCURRENCY = 30;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Provides access to node metadata.
|
|
@@ -234,7 +234,7 @@ export class NodesAccess {
|
|
|
234
234
|
|
|
235
235
|
if (errors.length > 0) {
|
|
236
236
|
this.logger.error(`Failed to decrypt ${errors.length} nodes`, errors);
|
|
237
|
-
throw new
|
|
237
|
+
throw new DecryptionError(c('Error').t`Failed to decrypt some nodes`, { cause: errors });
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
const missingNodeUids = nodeUids.filter((nodeUid) => !returnedNodeUids.includes(nodeUid));
|
|
@@ -189,11 +189,8 @@ export class NodesManagement {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
192
|
-
const deletedNodeUids = [];
|
|
193
|
-
|
|
194
192
|
for await (const result of this.apiService.deleteNodes(nodeUids, signal)) {
|
|
195
193
|
if (result.ok) {
|
|
196
|
-
deletedNodeUids.push(result.uid);
|
|
197
194
|
await this.nodesAccess.notifyNodeDeleted(result.uid);
|
|
198
195
|
}
|
|
199
196
|
yield result;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PrivateKey, SessionKey } from '../../crypto';
|
|
2
2
|
import { MemoryCache } from '../../cache';
|
|
3
3
|
import { CachedCryptoMaterial } from '../../interface';
|
|
4
|
+
import { getMockLogger } from '../../tests/logger';
|
|
4
5
|
import { SharesCryptoCache } from './cryptoCache';
|
|
5
6
|
|
|
6
7
|
describe('sharesCryptoCache', () => {
|
|
@@ -17,7 +18,7 @@ describe('sharesCryptoCache', () => {
|
|
|
17
18
|
|
|
18
19
|
beforeEach(() => {
|
|
19
20
|
memoryCache = new MemoryCache();
|
|
20
|
-
cache = new SharesCryptoCache(memoryCache);
|
|
21
|
+
cache = new SharesCryptoCache(getMockLogger(), memoryCache);
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
it('should store and retrieve keys', async () => {
|
|
@@ -53,7 +54,7 @@ describe('sharesCryptoCache', () => {
|
|
|
53
54
|
const keys = { key: generatePrivateKey('privateKey'), passphraseSessionKey: generateSessionKey('sessionKey') };
|
|
54
55
|
|
|
55
56
|
await cache.setShareKey(shareId, keys);
|
|
56
|
-
await cache.
|
|
57
|
+
await cache.removeShareKeys([shareId]);
|
|
57
58
|
|
|
58
59
|
try {
|
|
59
60
|
await cache.getShareKey(shareId);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProtonDriveCryptoCache } from '../../interface';
|
|
1
|
+
import { Logger, ProtonDriveCryptoCache } from '../../interface';
|
|
2
2
|
import { DecryptedShareKey } from './interface';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -13,23 +13,42 @@ import { DecryptedShareKey } from './interface';
|
|
|
13
13
|
* only the root node, thus share cache is not needed.
|
|
14
14
|
*/
|
|
15
15
|
export class SharesCryptoCache {
|
|
16
|
-
constructor(
|
|
16
|
+
constructor(
|
|
17
|
+
private logger: Logger,
|
|
18
|
+
private driveCache: ProtonDriveCryptoCache,
|
|
19
|
+
) {
|
|
20
|
+
this.logger = logger;
|
|
17
21
|
this.driveCache = driveCache;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
async setShareKey(shareId: string, key: DecryptedShareKey): Promise<void> {
|
|
21
|
-
await this.driveCache.setEntity(
|
|
25
|
+
await this.driveCache.setEntity(getCacheKey(shareId), {
|
|
26
|
+
shareKey: key,
|
|
27
|
+
});
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
async getShareKey(shareId: string): Promise<DecryptedShareKey> {
|
|
25
|
-
|
|
31
|
+
const nodeKeysData = await this.driveCache.getEntity(getCacheKey(shareId));
|
|
32
|
+
if (!nodeKeysData.shareKey) {
|
|
33
|
+
try {
|
|
34
|
+
await this.removeShareKeys([shareId]);
|
|
35
|
+
} catch (removingError: unknown) {
|
|
36
|
+
// The node keys will not be returned, thus SDK will re-fetch
|
|
37
|
+
// and re-cache it. Setting it again should then fix the problem.
|
|
38
|
+
this.logger.warn(
|
|
39
|
+
`Failed to remove corrupted node keys from the cache: ${removingError instanceof Error ? removingError.message : removingError}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Failed to deserialize node keys`);
|
|
43
|
+
}
|
|
44
|
+
return nodeKeysData.shareKey;
|
|
26
45
|
}
|
|
27
46
|
|
|
28
|
-
async
|
|
29
|
-
await this.driveCache.removeEntities(shareIds.map(
|
|
47
|
+
async removeShareKeys(shareIds: string[]): Promise<void> {
|
|
48
|
+
await this.driveCache.removeEntities(shareIds.map(getCacheKey));
|
|
30
49
|
}
|
|
31
50
|
}
|
|
32
51
|
|
|
33
|
-
function
|
|
52
|
+
function getCacheKey(shareId: string) {
|
|
34
53
|
return `shareKey-${shareId}`;
|
|
35
54
|
}
|
|
@@ -33,7 +33,7 @@ export function initSharesModule(
|
|
|
33
33
|
) {
|
|
34
34
|
const api = new SharesAPIService(apiService);
|
|
35
35
|
const cache = new SharesCache(telemetry.getLogger('shares-cache'), driveEntitiesCache);
|
|
36
|
-
const cryptoCache = new SharesCryptoCache(driveCryptoCache);
|
|
36
|
+
const cryptoCache = new SharesCryptoCache(telemetry.getLogger('shares-cache'), driveCryptoCache);
|
|
37
37
|
const cryptoService = new SharesCryptoService(telemetry, crypto, account);
|
|
38
38
|
const sharesManager = new SharesManager(
|
|
39
39
|
telemetry.getLogger('shares'),
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '../../interface';
|
|
10
10
|
import { getMockTelemetry } from '../../tests/telemetry';
|
|
11
11
|
import { SharesService } from './interface';
|
|
12
|
-
import { SharingCryptoService } from './cryptoService';
|
|
12
|
+
import { PUBLIC_LINK_GENERATED_PASSWORD_LENGTH, SharingCryptoService } from './cryptoService';
|
|
13
13
|
|
|
14
14
|
describe('SharingCryptoService', () => {
|
|
15
15
|
let telemetry: ProtonDriveTelemetry;
|
|
@@ -87,6 +87,27 @@ describe('SharingCryptoService', () => {
|
|
|
87
87
|
expect(telemetry.recordMetric).not.toHaveBeenCalled();
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it('should decrypt bookmark with custom password', async () => {
|
|
91
|
+
// First 12 characters are the generated password. Anything beyond is the custom password.
|
|
92
|
+
driveCrypto.decryptShareUrlPassword = jest.fn().mockResolvedValue('urlPassword1WithCustomPassword');
|
|
93
|
+
|
|
94
|
+
const result = await cryptoService.decryptBookmark(encryptedBookmark);
|
|
95
|
+
|
|
96
|
+
expect(result).toMatchObject({
|
|
97
|
+
url: resultOk('https://drive.proton.me/urls/tokenId#urlPassword1'),
|
|
98
|
+
nodeName: resultOk('nodeName'),
|
|
99
|
+
});
|
|
100
|
+
expect(driveCrypto.decryptShareUrlPassword).toHaveBeenCalledWith('encryptedUrlPassword', ['addressKey']);
|
|
101
|
+
expect(driveCrypto.decryptKeyWithSrpPassword).toHaveBeenCalledWith(
|
|
102
|
+
'urlPassword1WithCustomPassword',
|
|
103
|
+
'base64SharePasswordSalt',
|
|
104
|
+
'armoredKey',
|
|
105
|
+
'armoredPassphrase',
|
|
106
|
+
);
|
|
107
|
+
expect(driveCrypto.decryptNodeName).toHaveBeenCalledWith('encryptedName', 'decryptedKey', []);
|
|
108
|
+
expect(telemetry.recordMetric).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
90
111
|
it('should handle undecryptable URL password', async () => {
|
|
91
112
|
const error = new Error('Failed to decrypt URL password');
|
|
92
113
|
driveCrypto.decryptShareUrlPassword = jest.fn().mockRejectedValue(error);
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
EncryptedBookmark,
|
|
38
38
|
SharesService,
|
|
39
39
|
} from './interface';
|
|
40
|
+
import { DecryptionError } from '../../errors';
|
|
40
41
|
|
|
41
42
|
// Version 2 of bcrypt with 2**10 rounds.
|
|
42
43
|
// https://en.wikipedia.org/wiki/Bcrypt#Description
|
|
@@ -425,10 +426,11 @@ export class SharingCryptoService {
|
|
|
425
426
|
// TODO: Signatures are not checked and not specified in the interface.
|
|
426
427
|
// In the future, we will need to add authorship verification.
|
|
427
428
|
|
|
429
|
+
let password: string;
|
|
428
430
|
let urlPassword: string;
|
|
429
431
|
let customPassword: Result<string | undefined, Error>;
|
|
430
432
|
try {
|
|
431
|
-
|
|
433
|
+
password = await this.decryptBookmarkUrlPassword(encryptedBookmark);
|
|
432
434
|
const result = splitGeneratedAndCustomPassword(password);
|
|
433
435
|
urlPassword = result.password;
|
|
434
436
|
customPassword = resultOk(result.customPassword);
|
|
@@ -446,7 +448,7 @@ export class SharingCryptoService {
|
|
|
446
448
|
|
|
447
449
|
let shareKey: PrivateKey;
|
|
448
450
|
try {
|
|
449
|
-
shareKey = await this.decryptBookmarkKey(encryptedBookmark,
|
|
451
|
+
shareKey = await this.decryptBookmarkKey(encryptedBookmark, password);
|
|
450
452
|
} catch (originalError: unknown) {
|
|
451
453
|
const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
|
|
452
454
|
return {
|
|
@@ -493,15 +495,15 @@ export class SharingCryptoService {
|
|
|
493
495
|
|
|
494
496
|
const message = getErrorMessage(error);
|
|
495
497
|
const errorMessage = c('Error').t`Failed to decrypt bookmark password: ${message}`;
|
|
496
|
-
throw new
|
|
498
|
+
throw new DecryptionError(errorMessage, { cause: error });
|
|
497
499
|
}
|
|
498
500
|
}
|
|
499
501
|
|
|
500
|
-
private async decryptBookmarkKey(encryptedBookmark: EncryptedBookmark,
|
|
502
|
+
private async decryptBookmarkKey(encryptedBookmark: EncryptedBookmark, password: string): Promise<PrivateKey> {
|
|
501
503
|
try {
|
|
502
504
|
// Use the password to decrypt the share key.
|
|
503
505
|
const { key: shareKey } = await this.driveCrypto.decryptKeyWithSrpPassword(
|
|
504
|
-
|
|
506
|
+
password,
|
|
505
507
|
encryptedBookmark.url.base64SharePasswordSalt,
|
|
506
508
|
encryptedBookmark.share.armoredKey,
|
|
507
509
|
encryptedBookmark.share.armoredPassphrase,
|
|
@@ -519,7 +521,7 @@ export class SharingCryptoService {
|
|
|
519
521
|
|
|
520
522
|
const message = getErrorMessage(error);
|
|
521
523
|
const errorMessage = c('Error').t`Failed to decrypt bookmark key: ${message}`;
|
|
522
|
-
throw new
|
|
524
|
+
throw new DecryptionError(errorMessage, { cause: error });
|
|
523
525
|
}
|
|
524
526
|
}
|
|
525
527
|
|
|
@@ -145,10 +145,6 @@ export interface SharesService {
|
|
|
145
145
|
getMyFilesIDs(): Promise<{ volumeId: string }>;
|
|
146
146
|
loadEncryptedShare(shareId: string): Promise<EncryptedShare>;
|
|
147
147
|
getMyFilesShareMemberEmailKey(): Promise<{
|
|
148
|
-
addressId: string;
|
|
149
|
-
addressKey: PrivateKey;
|
|
150
|
-
}>;
|
|
151
|
-
getContextShareMemberEmailKey(shareId: string): Promise<{
|
|
152
148
|
email: string;
|
|
153
149
|
addressId: string;
|
|
154
150
|
addressKey: PrivateKey;
|
|
@@ -3,7 +3,7 @@ import { SharingAPIService } from './apiService';
|
|
|
3
3
|
import { SharingCache } from './cache';
|
|
4
4
|
import { SharingCryptoService } from './cryptoService';
|
|
5
5
|
import { SharesService, NodesService } from './interface';
|
|
6
|
-
import { SharingAccess } from './sharingAccess';
|
|
6
|
+
import { SharingAccess, BATCH_LOADING_SIZE } from './sharingAccess';
|
|
7
7
|
|
|
8
8
|
describe('SharingAccess', () => {
|
|
9
9
|
let apiService: SharingAPIService;
|
|
@@ -14,7 +14,7 @@ describe('SharingAccess', () => {
|
|
|
14
14
|
|
|
15
15
|
let sharingAccess: SharingAccess;
|
|
16
16
|
|
|
17
|
-
const nodeUids = Array.from({ length:
|
|
17
|
+
const nodeUids = Array.from({ length: BATCH_LOADING_SIZE + 5 }, (_, i) => `nodeUid${i}`);
|
|
18
18
|
const nodes = nodeUids.map((nodeUid) => ({ nodeUid }));
|
|
19
19
|
const nodeUidsIterator = async function* () {
|
|
20
20
|
for (const nodeUid of nodeUids) {
|
|
@@ -84,7 +84,7 @@ describe('SharingAccess', () => {
|
|
|
84
84
|
|
|
85
85
|
expect(result).toEqual(nodes);
|
|
86
86
|
expect(apiService.iterateSharedNodeUids).toHaveBeenCalledWith('volumeId', undefined);
|
|
87
|
-
expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); //
|
|
87
|
+
expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
|
|
88
88
|
expect(cache.setSharedByMeNodeUids).toHaveBeenCalledWith(nodeUids);
|
|
89
89
|
});
|
|
90
90
|
});
|
|
@@ -107,7 +107,7 @@ describe('SharingAccess', () => {
|
|
|
107
107
|
|
|
108
108
|
expect(result).toEqual(nodes);
|
|
109
109
|
expect(apiService.iterateSharedWithMeNodeUids).toHaveBeenCalledWith(undefined);
|
|
110
|
-
expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); //
|
|
110
|
+
expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
|
|
111
111
|
expect(cache.setSharedWithMeNodeUids).toHaveBeenCalledWith(nodeUids);
|
|
112
112
|
});
|
|
113
113
|
});
|
|
@@ -9,6 +9,10 @@ import { SharingCache } from './cache';
|
|
|
9
9
|
import { SharingCryptoService } from './cryptoService';
|
|
10
10
|
import { SharesService, NodesService } from './interface';
|
|
11
11
|
|
|
12
|
+
// This is the number of nodes that are loaded in parallel.
|
|
13
|
+
// It is a trade-off between initial wait time and overhead of API calls.
|
|
14
|
+
export const BATCH_LOADING_SIZE = 30;
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Provides high-level actions for access shared nodes.
|
|
14
18
|
*
|
|
@@ -66,6 +70,7 @@ export class SharingAccess {
|
|
|
66
70
|
): AsyncGenerator<DecryptedNode> {
|
|
67
71
|
const batchLoading = new BatchLoading<string, DecryptedNode>({
|
|
68
72
|
iterateItems: (nodeUids) => this.iterateNodesAndIgnoreMissingOnes(nodeUids, signal),
|
|
73
|
+
batchSize: BATCH_LOADING_SIZE,
|
|
69
74
|
});
|
|
70
75
|
for (const nodeUid of nodeUids) {
|
|
71
76
|
yield* batchLoading.load(nodeUid);
|
|
@@ -81,6 +86,7 @@ export class SharingAccess {
|
|
|
81
86
|
const loadedNodeUids = [];
|
|
82
87
|
const batchLoading = new BatchLoading<string, DecryptedNode>({
|
|
83
88
|
iterateItems: (nodeUids) => this.iterateNodesAndIgnoreMissingOnes(nodeUids, signal),
|
|
89
|
+
batchSize: BATCH_LOADING_SIZE,
|
|
84
90
|
});
|
|
85
91
|
for await (const nodeUid of nodeUidsIterator) {
|
|
86
92
|
loadedNodeUids.push(nodeUid);
|