@protontech/drive-sdk 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/internal/apiService/apiService.js +11 -3
- package/dist/internal/apiService/apiService.js.map +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/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/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.js +27 -16
- package/dist/internal/sharing/sharingManagement.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.test.js +29 -13
- package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
- 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 +8 -8
- package/dist/protonDriveClient.js +14 -14
- package/dist/protonDriveClient.js.map +1 -1
- package/package.json +1 -1
- 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/internal/apiService/apiService.ts +12 -3
- package/src/internal/nodes/apiService.test.ts +28 -0
- package/src/internal/nodes/apiService.ts +3 -0
- package/src/internal/nodes/cryptoService.ts +68 -34
- package/src/internal/nodes/nodesAccess.ts +2 -2
- package/src/internal/nodes/nodesManagement.ts +0 -3
- 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 +54 -24
- package/src/internal/sharing/sharingManagement.ts +47 -16
- package/src/internal/upload/manager.test.ts +2 -2
- package/src/internal/upload/manager.ts +2 -4
- package/src/protonDriveClient.ts +17 -16
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -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);
|
|
@@ -58,11 +58,9 @@ describe('SharingManagement', () => {
|
|
|
58
58
|
};
|
|
59
59
|
// @ts-expect-error No need to implement all methods for mocking
|
|
60
60
|
cryptoService = {
|
|
61
|
-
generateShareKeys: jest
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
shareKey: { encrypted: 'encrypted-key', decrypted: { passphraseSessionKey: 'pass-session-key' } },
|
|
65
|
-
}),
|
|
61
|
+
generateShareKeys: jest.fn().mockResolvedValue({
|
|
62
|
+
shareKey: { encrypted: 'encrypted-key', decrypted: { passphraseSessionKey: 'pass-session-key' } },
|
|
63
|
+
}),
|
|
66
64
|
decryptShare: jest.fn().mockImplementation((share) => share),
|
|
67
65
|
decryptInvitation: jest.fn().mockImplementation((invitation) => invitation),
|
|
68
66
|
decryptExternalInvitation: jest.fn().mockImplementation((invitation) => invitation),
|
|
@@ -70,7 +68,7 @@ describe('SharingManagement', () => {
|
|
|
70
68
|
encryptInvitation: jest.fn().mockImplementation(() => {}),
|
|
71
69
|
encryptExternalInvitation: jest.fn().mockImplementation((invitation) => ({
|
|
72
70
|
...invitation,
|
|
73
|
-
base64ExternalInvitationSignature: '
|
|
71
|
+
base64ExternalInvitationSignature: 'external-signature',
|
|
74
72
|
})),
|
|
75
73
|
decryptPublicLink: jest.fn().mockImplementation((publicLink) => publicLink),
|
|
76
74
|
generatePublicLinkPassword: jest.fn().mockResolvedValue('generatedPassword'),
|
|
@@ -85,17 +83,12 @@ describe('SharingManagement', () => {
|
|
|
85
83
|
};
|
|
86
84
|
// @ts-expect-error No need to implement all methods for mocking
|
|
87
85
|
sharesService = {
|
|
88
|
-
loadEncryptedShare: jest
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
passphraseSessionKey: 'sharePassphraseSessionKey',
|
|
95
|
-
}),
|
|
96
|
-
getContextShareMemberEmailKey: jest
|
|
97
|
-
.fn()
|
|
98
|
-
.mockResolvedValue({ email: 'volume-email', addressId: 'addressId', addressKey: 'volume-key' }),
|
|
86
|
+
loadEncryptedShare: jest.fn().mockResolvedValue({
|
|
87
|
+
id: 'shareId',
|
|
88
|
+
addressId: 'addressId',
|
|
89
|
+
creatorEmail: 'address@example.com',
|
|
90
|
+
passphraseSessionKey: 'sharePassphraseSessionKey',
|
|
91
|
+
}),
|
|
99
92
|
};
|
|
100
93
|
// @ts-expect-error No need to implement all methods for mocking
|
|
101
94
|
nodesService = {
|
|
@@ -196,13 +189,11 @@ describe('SharingManagement', () => {
|
|
|
196
189
|
const nodeUid = 'volumeId~nodeUid';
|
|
197
190
|
|
|
198
191
|
it('should create share if no exists', async () => {
|
|
199
|
-
nodesService.getNode = jest
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
name: { ok: true, value: 'name' },
|
|
205
|
-
}));
|
|
192
|
+
nodesService.getNode = jest.fn().mockImplementation((nodeUid) => ({
|
|
193
|
+
nodeUid,
|
|
194
|
+
parentUid: 'parentUid',
|
|
195
|
+
name: { ok: true, value: 'name' },
|
|
196
|
+
}));
|
|
206
197
|
nodesService.notifyNodeChanged = jest.fn();
|
|
207
198
|
|
|
208
199
|
const sharingInfo = await sharingManagement.shareNode(nodeUid, { users: ['email'] });
|
|
@@ -347,6 +338,24 @@ describe('SharingManagement', () => {
|
|
|
347
338
|
expect(apiService.updateInvitation).not.toHaveBeenCalled();
|
|
348
339
|
expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
|
|
349
340
|
});
|
|
341
|
+
|
|
342
|
+
it('should use address from the root node context share', async () => {
|
|
343
|
+
nodesService.getRootNodeEmailKey = jest
|
|
344
|
+
.fn()
|
|
345
|
+
.mockResolvedValue({ email: 'my-volume-email', addressKey: 'my-volume-key' });
|
|
346
|
+
|
|
347
|
+
await sharingManagement.shareNode(nodeUid, { users: ['email'] });
|
|
348
|
+
|
|
349
|
+
expect(apiService.inviteProtonUser).toHaveBeenCalledWith(
|
|
350
|
+
'shareId',
|
|
351
|
+
{
|
|
352
|
+
addedByEmail: 'my-volume-email',
|
|
353
|
+
inviteeEmail: 'email',
|
|
354
|
+
role: 'viewer',
|
|
355
|
+
},
|
|
356
|
+
expect.anything(),
|
|
357
|
+
);
|
|
358
|
+
});
|
|
350
359
|
});
|
|
351
360
|
|
|
352
361
|
describe('external invitations', () => {
|
|
@@ -434,6 +443,27 @@ describe('SharingManagement', () => {
|
|
|
434
443
|
expect(apiService.updateExternalInvitation).not.toHaveBeenCalled();
|
|
435
444
|
expect(apiService.inviteExternalUser).not.toHaveBeenCalled();
|
|
436
445
|
});
|
|
446
|
+
|
|
447
|
+
it('should use address from the root node context share', async () => {
|
|
448
|
+
nodesService.getRootNodeEmailKey = jest.fn().mockResolvedValue({
|
|
449
|
+
email: 'my-volume-email',
|
|
450
|
+
addressId: 'my-volume-addressId',
|
|
451
|
+
addressKey: 'my-volume-key',
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
await sharingManagement.shareNode(nodeUid, { users: ['email'] });
|
|
455
|
+
|
|
456
|
+
expect(apiService.inviteExternalUser).toHaveBeenCalledWith(
|
|
457
|
+
'shareId',
|
|
458
|
+
{
|
|
459
|
+
inviterAddressId: 'my-volume-addressId',
|
|
460
|
+
inviteeEmail: 'email',
|
|
461
|
+
role: 'viewer',
|
|
462
|
+
base64Signature: 'external-signature',
|
|
463
|
+
},
|
|
464
|
+
expect.anything(),
|
|
465
|
+
);
|
|
466
|
+
});
|
|
437
467
|
});
|
|
438
468
|
|
|
439
469
|
describe('mix of internal and external invitations', () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
|
-
import { SessionKey } from '../../crypto';
|
|
3
|
+
import { PrivateKey, SessionKey } from '../../crypto';
|
|
4
4
|
import { ValidationError } from '../../errors';
|
|
5
5
|
import {
|
|
6
6
|
Logger,
|
|
@@ -33,6 +33,12 @@ interface Share {
|
|
|
33
33
|
passphraseSessionKey: SessionKey;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
interface ContextShareAddress {
|
|
37
|
+
addressId: string;
|
|
38
|
+
addressKey: PrivateKey;
|
|
39
|
+
email: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
interface EmailOptions {
|
|
37
43
|
message?: string;
|
|
38
44
|
nodeName?: string;
|
|
@@ -138,18 +144,22 @@ export class SharingManagement {
|
|
|
138
144
|
throw new ValidationError(c('Error').t`Expiration date cannot be in the past`);
|
|
139
145
|
}
|
|
140
146
|
|
|
147
|
+
let contextShareAddress: ContextShareAddress;
|
|
141
148
|
let currentSharing = await this.getInternalSharingInfo(nodeUid);
|
|
142
|
-
if (
|
|
149
|
+
if (currentSharing) {
|
|
150
|
+
contextShareAddress = await this.nodesService.getRootNodeEmailKey(nodeUid);
|
|
151
|
+
} else {
|
|
143
152
|
const node = await this.nodesService.getNode(nodeUid);
|
|
144
|
-
const
|
|
153
|
+
const result = await this.createShare(nodeUid);
|
|
145
154
|
currentSharing = {
|
|
146
|
-
share,
|
|
155
|
+
share: result.share,
|
|
147
156
|
nodeName: node.name.ok ? node.name.value : node.name.error.name,
|
|
148
157
|
protonInvitations: [],
|
|
149
158
|
nonProtonInvitations: [],
|
|
150
159
|
members: [],
|
|
151
160
|
publicLink: undefined,
|
|
152
161
|
};
|
|
162
|
+
contextShareAddress = result.contextShareAddress;
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
const emailOptions: EmailOptions = {
|
|
@@ -187,7 +197,13 @@ export class SharingManagement {
|
|
|
187
197
|
}
|
|
188
198
|
|
|
189
199
|
this.logger.info(`Inviting user ${email} with role ${role} to node ${nodeUid}`);
|
|
190
|
-
const invitation = await this.inviteProtonUser(
|
|
200
|
+
const invitation = await this.inviteProtonUser(
|
|
201
|
+
contextShareAddress,
|
|
202
|
+
currentSharing.share,
|
|
203
|
+
email,
|
|
204
|
+
role,
|
|
205
|
+
emailOptions,
|
|
206
|
+
);
|
|
191
207
|
currentSharing.protonInvitations.push(invitation);
|
|
192
208
|
}
|
|
193
209
|
|
|
@@ -225,7 +241,13 @@ export class SharingManagement {
|
|
|
225
241
|
}
|
|
226
242
|
|
|
227
243
|
this.logger.info(`Inviting external user ${email} with role ${role} to node ${nodeUid}`);
|
|
228
|
-
const invitation = await this.inviteExternalUser(
|
|
244
|
+
const invitation = await this.inviteExternalUser(
|
|
245
|
+
contextShareAddress,
|
|
246
|
+
currentSharing.share,
|
|
247
|
+
email,
|
|
248
|
+
role,
|
|
249
|
+
emailOptions,
|
|
250
|
+
);
|
|
229
251
|
currentSharing.nonProtonInvitations.push(invitation);
|
|
230
252
|
}
|
|
231
253
|
|
|
@@ -241,7 +263,7 @@ export class SharingManagement {
|
|
|
241
263
|
);
|
|
242
264
|
} else {
|
|
243
265
|
this.logger.info(`Sharing via public link with role ${options.role} to node ${nodeUid}`);
|
|
244
|
-
currentSharing.publicLink = await this.shareViaLink(currentSharing.share, options);
|
|
266
|
+
currentSharing.publicLink = await this.shareViaLink(contextShareAddress, currentSharing.share, options);
|
|
245
267
|
}
|
|
246
268
|
}
|
|
247
269
|
|
|
@@ -371,7 +393,7 @@ export class SharingManagement {
|
|
|
371
393
|
};
|
|
372
394
|
}
|
|
373
395
|
|
|
374
|
-
private async createShare(nodeUid: string): Promise<Share> {
|
|
396
|
+
private async createShare(nodeUid: string): Promise<{ share: Share; contextShareAddress: ContextShareAddress }> {
|
|
375
397
|
const node = await this.nodesService.getNode(nodeUid);
|
|
376
398
|
if (!node.parentUid) {
|
|
377
399
|
throw new ValidationError(c('Error').t`Cannot share root folder`);
|
|
@@ -387,12 +409,22 @@ export class SharingManagement {
|
|
|
387
409
|
base64NameKeyPacket: keys.base64NameKeyPacket,
|
|
388
410
|
});
|
|
389
411
|
await this.nodesService.notifyNodeChanged(nodeUid);
|
|
390
|
-
|
|
412
|
+
|
|
413
|
+
const share = {
|
|
391
414
|
volumeId,
|
|
392
415
|
shareId,
|
|
393
416
|
creatorEmail: email,
|
|
394
417
|
passphraseSessionKey: keys.shareKey.decrypted.passphraseSessionKey,
|
|
395
418
|
};
|
|
419
|
+
const contextShareAddress = {
|
|
420
|
+
email,
|
|
421
|
+
addressId,
|
|
422
|
+
addressKey,
|
|
423
|
+
};
|
|
424
|
+
return {
|
|
425
|
+
share,
|
|
426
|
+
contextShareAddress,
|
|
427
|
+
};
|
|
396
428
|
}
|
|
397
429
|
|
|
398
430
|
private async deleteShare(shareId: string, nodeUid: string): Promise<void> {
|
|
@@ -401,12 +433,12 @@ export class SharingManagement {
|
|
|
401
433
|
}
|
|
402
434
|
|
|
403
435
|
private async inviteProtonUser(
|
|
436
|
+
inviter: ContextShareAddress,
|
|
404
437
|
share: Share,
|
|
405
438
|
inviteeEmail: string,
|
|
406
439
|
role: MemberRole,
|
|
407
440
|
emailOptions: EmailOptions,
|
|
408
441
|
): Promise<ProtonInvitation> {
|
|
409
|
-
const inviter = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
|
|
410
442
|
const invitationCrypto = await this.cryptoService.encryptInvitation(
|
|
411
443
|
share.passphraseSessionKey,
|
|
412
444
|
inviter.addressKey,
|
|
@@ -461,12 +493,12 @@ export class SharingManagement {
|
|
|
461
493
|
}
|
|
462
494
|
|
|
463
495
|
private async inviteExternalUser(
|
|
496
|
+
inviter: ContextShareAddress,
|
|
464
497
|
share: Share,
|
|
465
498
|
inviteeEmail: string,
|
|
466
499
|
role: MemberRole,
|
|
467
500
|
emailOptions: EmailOptions,
|
|
468
501
|
): Promise<NonProtonInvitation> {
|
|
469
|
-
const inviter = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
|
|
470
502
|
const invitationCrypto = await this.cryptoService.encryptExternalInvitation(
|
|
471
503
|
share.passphraseSessionKey,
|
|
472
504
|
inviter.addressKey,
|
|
@@ -515,21 +547,20 @@ export class SharingManagement {
|
|
|
515
547
|
}
|
|
516
548
|
|
|
517
549
|
private async shareViaLink(
|
|
550
|
+
inviter: ContextShareAddress,
|
|
518
551
|
share: Share,
|
|
519
552
|
options: SharePublicLinkSettingsObject,
|
|
520
553
|
): Promise<PublicLinkWithCreatorEmail> {
|
|
521
|
-
const { email: creatorEmail } = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
|
|
522
|
-
|
|
523
554
|
const generatedPassword = await this.cryptoService.generatePublicLinkPassword();
|
|
524
555
|
const password = options.customPassword ? `${generatedPassword}${options.customPassword}` : generatedPassword;
|
|
525
556
|
|
|
526
557
|
const { crypto, srp } = await this.cryptoService.encryptPublicLink(
|
|
527
|
-
|
|
558
|
+
inviter.email,
|
|
528
559
|
share.passphraseSessionKey,
|
|
529
560
|
password,
|
|
530
561
|
);
|
|
531
562
|
const publicLink = await this.apiService.createPublicLink(share.shareId, {
|
|
532
|
-
creatorEmail,
|
|
563
|
+
creatorEmail: inviter.email,
|
|
533
564
|
role: options.role,
|
|
534
565
|
includesCustomPassword: !!options.customPassword,
|
|
535
566
|
expirationTime: options.expiration ? Math.floor(options.expiration.getTime() / 1000) : undefined,
|
|
@@ -545,7 +576,7 @@ export class SharingManagement {
|
|
|
545
576
|
customPassword: options.customPassword,
|
|
546
577
|
expirationTime: options.expiration,
|
|
547
578
|
numberOfInitializedDownloads: 0,
|
|
548
|
-
creatorEmail,
|
|
579
|
+
creatorEmail: inviter.email,
|
|
549
580
|
};
|
|
550
581
|
}
|
|
551
582
|
|
|
@@ -206,7 +206,7 @@ describe('UploadManager', () => {
|
|
|
206
206
|
await promise;
|
|
207
207
|
} catch (error: any) {
|
|
208
208
|
expect(error.message).toBe('Draft already exists');
|
|
209
|
-
expect(error.
|
|
209
|
+
expect(error.isUnfinishedUpload).toBe(true);
|
|
210
210
|
}
|
|
211
211
|
expect(apiService.deleteDraft).not.toHaveBeenCalled();
|
|
212
212
|
});
|
|
@@ -237,7 +237,7 @@ describe('UploadManager', () => {
|
|
|
237
237
|
await promise;
|
|
238
238
|
} catch (error: any) {
|
|
239
239
|
expect(error.message).toBe('Draft already exists');
|
|
240
|
-
expect(error.
|
|
240
|
+
expect(error.isUnfinishedUpload).toBe(true);
|
|
241
241
|
}
|
|
242
242
|
expect(apiService.deleteDraft).not.toHaveBeenCalled();
|
|
243
243
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
3
|
import { Logger, ProtonDriveTelemetry, UploadMetadata } from '../../interface';
|
|
4
|
-
import { ValidationError,
|
|
4
|
+
import { ValidationError, NodeWithSameNameExistsValidationError } from '../../errors';
|
|
5
5
|
import { ErrorCode } from '../apiService';
|
|
6
6
|
import { generateFileExtendedAttributes } from '../nodes';
|
|
7
7
|
import { UploadAPIService } from './apiService';
|
|
@@ -158,9 +158,7 @@ export class UploadManager {
|
|
|
158
158
|
? makeNodeUid(splitNodeUid(parentFolderUid).volumeId, typedDetails.ConflictLinkID)
|
|
159
159
|
: undefined;
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
// that includes the available name the client can use.
|
|
163
|
-
throw new NodeAlreadyExistsValidationError(
|
|
161
|
+
throw new NodeWithSameNameExistsValidationError(
|
|
164
162
|
error.message,
|
|
165
163
|
error.code,
|
|
166
164
|
existingNodeUid,
|