@protontech/drive-sdk 0.4.0 → 0.5.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/sdkDiagnostic.js +1 -1
- package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
- package/dist/interface/download.d.ts +4 -4
- package/dist/interface/nodes.d.ts +4 -0
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/upload.d.ts +6 -3
- package/dist/internal/apiService/apiService.d.ts +3 -0
- package/dist/internal/apiService/apiService.js +25 -2
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +38 -0
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +31 -48
- package/dist/internal/apiService/errors.js +3 -0
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/apiService/errors.test.js +15 -7
- package/dist/internal/apiService/errors.test.js.map +1 -1
- package/dist/internal/asyncIteratorMap.d.ts +1 -1
- package/dist/internal/asyncIteratorMap.js +6 -1
- package/dist/internal/asyncIteratorMap.js.map +1 -1
- package/dist/internal/asyncIteratorMap.test.js +9 -0
- package/dist/internal/asyncIteratorMap.test.js.map +1 -1
- package/dist/internal/download/fileDownloader.d.ts +3 -3
- package/dist/internal/download/fileDownloader.js +5 -5
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +8 -8
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/nodes/apiService.d.ts +6 -1
- package/dist/internal/nodes/apiService.js +45 -32
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +164 -17
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.test.js +1 -0
- package/dist/internal/nodes/cache.test.js.map +1 -1
- package/dist/internal/nodes/debouncer.d.ts +23 -0
- package/dist/internal/nodes/debouncer.js +80 -0
- package/dist/internal/nodes/debouncer.js.map +1 -0
- package/dist/internal/nodes/debouncer.test.d.ts +1 -0
- package/dist/internal/nodes/debouncer.test.js +100 -0
- package/dist/internal/nodes/debouncer.test.js.map +1 -0
- package/dist/internal/nodes/extendedAttributes.d.ts +2 -2
- package/dist/internal/nodes/extendedAttributes.js +15 -11
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.test.js +19 -1
- package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
- package/dist/internal/nodes/index.test.js +1 -0
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +1 -0
- package/dist/internal/nodes/nodesAccess.d.ts +2 -1
- package/dist/internal/nodes/nodesAccess.js +24 -5
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +2 -2
- package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.js +1 -0
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/photos/index.d.ts +11 -0
- package/dist/internal/photos/index.js +27 -0
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +60 -0
- package/dist/internal/photos/upload.js +104 -0
- package/dist/internal/photos/upload.js.map +1 -0
- package/dist/internal/sharingPublic/apiService.d.ts +2 -2
- package/dist/internal/sharingPublic/apiService.js +2 -62
- package/dist/internal/sharingPublic/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -4
- package/dist/internal/sharingPublic/cryptoCache.js +0 -28
- package/dist/internal/sharingPublic/cryptoCache.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoReporter.d.ts +16 -0
- package/dist/internal/sharingPublic/cryptoReporter.js +44 -0
- package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -0
- package/dist/internal/sharingPublic/cryptoService.d.ts +3 -4
- package/dist/internal/sharingPublic/cryptoService.js +5 -43
- package/dist/internal/sharingPublic/cryptoService.js.map +1 -1
- package/dist/internal/sharingPublic/index.d.ts +21 -3
- package/dist/internal/sharingPublic/index.js +43 -12
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/interface.d.ts +0 -1
- package/dist/internal/sharingPublic/nodes.d.ts +13 -0
- package/dist/internal/sharingPublic/nodes.js +28 -0
- package/dist/internal/sharingPublic/nodes.js.map +1 -0
- package/dist/internal/sharingPublic/session/session.d.ts +3 -3
- package/dist/internal/sharingPublic/session/url.test.js +3 -3
- package/dist/internal/sharingPublic/shares.d.ts +34 -0
- package/dist/internal/sharingPublic/shares.js +69 -0
- package/dist/internal/sharingPublic/shares.js.map +1 -0
- package/dist/internal/upload/apiService.d.ts +2 -2
- package/dist/internal/upload/apiService.js +11 -2
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/controller.d.ts +8 -2
- package/dist/internal/upload/controller.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +2 -2
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/fileUploader.d.ts +7 -3
- package/dist/internal/upload/fileUploader.js +6 -3
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/fileUploader.test.js +23 -11
- package/dist/internal/upload/fileUploader.test.js.map +1 -1
- package/dist/internal/upload/interface.d.ts +3 -0
- package/dist/internal/upload/manager.d.ts +12 -11
- package/dist/internal/upload/manager.js +8 -2
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +8 -0
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/streamUploader.d.ts +40 -26
- package/dist/internal/upload/streamUploader.js +15 -8
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +11 -7
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +3 -3
- package/dist/protonDriveClient.js +4 -4
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +18 -2
- package/dist/protonDrivePhotosClient.js +19 -2
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +31 -4
- package/dist/protonDrivePublicLinkClient.js +52 -9
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/transformers.d.ts +1 -1
- package/dist/transformers.js +1 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/diagnostic/sdkDiagnostic.ts +1 -1
- package/src/interface/download.ts +4 -4
- package/src/interface/nodes.ts +4 -0
- package/src/interface/upload.ts +3 -3
- package/src/internal/apiService/apiService.test.ts +50 -0
- package/src/internal/apiService/apiService.ts +33 -2
- package/src/internal/apiService/driveTypes.ts +31 -48
- package/src/internal/apiService/errors.test.ts +10 -0
- package/src/internal/apiService/errors.ts +5 -1
- package/src/internal/asyncIteratorMap.test.ts +12 -0
- package/src/internal/asyncIteratorMap.ts +8 -0
- package/src/internal/download/fileDownloader.test.ts +8 -8
- package/src/internal/download/fileDownloader.ts +5 -5
- package/src/internal/nodes/apiService.test.ts +222 -16
- package/src/internal/nodes/apiService.ts +63 -49
- package/src/internal/nodes/cache.test.ts +1 -0
- package/src/internal/nodes/debouncer.test.ts +129 -0
- package/src/internal/nodes/debouncer.ts +93 -0
- package/src/internal/nodes/extendedAttributes.test.ts +23 -1
- package/src/internal/nodes/extendedAttributes.ts +26 -18
- package/src/internal/nodes/index.test.ts +1 -0
- package/src/internal/nodes/interface.ts +1 -0
- package/src/internal/nodes/nodesAccess.test.ts +2 -2
- package/src/internal/nodes/nodesAccess.ts +30 -5
- package/src/internal/nodes/nodesManagement.ts +1 -0
- package/src/internal/photos/index.ts +62 -0
- package/src/internal/photos/upload.ts +212 -0
- package/src/internal/sharingPublic/apiService.ts +5 -86
- package/src/internal/sharingPublic/cryptoCache.ts +0 -34
- package/src/internal/sharingPublic/cryptoReporter.ts +73 -0
- package/src/internal/sharingPublic/cryptoService.ts +4 -80
- package/src/internal/sharingPublic/index.ts +68 -6
- package/src/internal/sharingPublic/interface.ts +0 -9
- package/src/internal/sharingPublic/nodes.ts +37 -0
- package/src/internal/sharingPublic/session/apiService.ts +1 -1
- package/src/internal/sharingPublic/session/session.ts +3 -3
- package/src/internal/sharingPublic/session/url.test.ts +3 -3
- package/src/internal/sharingPublic/shares.ts +86 -0
- package/src/internal/upload/apiService.ts +15 -4
- package/src/internal/upload/controller.ts +2 -2
- package/src/internal/upload/cryptoService.ts +2 -2
- package/src/internal/upload/fileUploader.test.ts +25 -11
- package/src/internal/upload/fileUploader.ts +16 -3
- package/src/internal/upload/interface.ts +3 -0
- package/src/internal/upload/manager.test.ts +8 -0
- package/src/internal/upload/manager.ts +20 -10
- package/src/internal/upload/streamUploader.test.ts +32 -15
- package/src/internal/upload/streamUploader.ts +43 -30
- package/src/protonDriveClient.ts +4 -4
- package/src/protonDrivePhotosClient.ts +46 -6
- package/src/protonDrivePublicLinkClient.ts +93 -12
- package/src/transformers.ts +2 -0
- package/dist/internal/sharingPublic/manager.d.ts +0 -19
- package/dist/internal/sharingPublic/manager.js +0 -81
- package/dist/internal/sharingPublic/manager.js.map +0 -1
- package/src/internal/sharingPublic/manager.ts +0 -86
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { DriveAPIService, drivePaths, nodeTypeNumberToNodeType } from '../apiService';
|
|
2
2
|
import { Logger, MemberRole } from '../../interface';
|
|
3
|
-
import { makeNodeUid
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const PAGE_SIZE = 50;
|
|
3
|
+
import { makeNodeUid } from '../uids';
|
|
4
|
+
import { EncryptedNode } from '../nodes/interface';
|
|
5
|
+
import { EncryptedShareCrypto } from './interface';
|
|
7
6
|
|
|
8
7
|
type GetTokenInfoResponse = drivePaths['/drive/urls/{token}']['get']['responses']['200']['content']['application/json'];
|
|
9
8
|
|
|
10
|
-
type GetTokenFolderChildrenResponse =
|
|
11
|
-
drivePaths['/drive/urls/{token}/folders/{linkID}/children']['get']['responses']['200']['content']['application/json'];
|
|
12
|
-
|
|
13
9
|
/**
|
|
14
10
|
* Provides API communication for accessing public link data.
|
|
15
11
|
*
|
|
@@ -41,27 +37,6 @@ export class SharingPublicAPIService {
|
|
|
41
37
|
},
|
|
42
38
|
};
|
|
43
39
|
}
|
|
44
|
-
|
|
45
|
-
async *iterateFolderChildren(parentUid: string, signal?: AbortSignal): AsyncGenerator<EncryptedNode> {
|
|
46
|
-
const { volumeId: token, nodeId } = splitNodeUid(parentUid);
|
|
47
|
-
|
|
48
|
-
let page = 0;
|
|
49
|
-
while (true) {
|
|
50
|
-
const response = await this.apiService.get<GetTokenFolderChildrenResponse>(
|
|
51
|
-
`drive/urls/${token}/folders/${nodeId}/children?Page=${page}&PageSize=${PAGE_SIZE}`,
|
|
52
|
-
signal,
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
for (const link of response.Links) {
|
|
56
|
-
yield linkToEncryptedNode(this.logger, token, link);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (response.Links.length < PAGE_SIZE) {
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
page++;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
40
|
}
|
|
66
41
|
|
|
67
42
|
function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token']): EncryptedNode {
|
|
@@ -70,12 +45,13 @@ function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token
|
|
|
70
45
|
encryptedName: token.Name,
|
|
71
46
|
|
|
72
47
|
// Basic node metadata
|
|
73
|
-
uid: makeNodeUid(token.
|
|
48
|
+
uid: makeNodeUid(token.VolumeID, token.LinkID),
|
|
74
49
|
parentUid: undefined,
|
|
75
50
|
type: nodeTypeNumberToNodeType(logger, token.LinkType),
|
|
76
51
|
creationTime: new Date(), // TODO
|
|
77
52
|
|
|
78
53
|
isShared: false,
|
|
54
|
+
isSharedPublicly: false,
|
|
79
55
|
directRole: MemberRole.Viewer, // TODO
|
|
80
56
|
};
|
|
81
57
|
|
|
@@ -114,60 +90,3 @@ function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token
|
|
|
114
90
|
|
|
115
91
|
throw new Error(`Unknown node type: ${token.LinkType}`);
|
|
116
92
|
}
|
|
117
|
-
|
|
118
|
-
function linkToEncryptedNode(
|
|
119
|
-
logger: Logger,
|
|
120
|
-
token: string,
|
|
121
|
-
link: GetTokenFolderChildrenResponse['Links'][0],
|
|
122
|
-
): EncryptedNode {
|
|
123
|
-
const baseNodeMetadata = {
|
|
124
|
-
// Internal metadata
|
|
125
|
-
hash: link.Hash || undefined,
|
|
126
|
-
encryptedName: link.Name,
|
|
127
|
-
|
|
128
|
-
// Basic node metadata
|
|
129
|
-
uid: makeNodeUid(token, link.LinkID),
|
|
130
|
-
parentUid: link.ParentLinkID ? makeNodeUid(token, link.ParentLinkID) : undefined,
|
|
131
|
-
type: nodeTypeNumberToNodeType(logger, link.Type),
|
|
132
|
-
creationTime: new Date(), // TODO
|
|
133
|
-
totalStorageSize: link.TotalSize,
|
|
134
|
-
|
|
135
|
-
isShared: false,
|
|
136
|
-
directRole: MemberRole.Viewer, // TODO
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const baseCryptoNodeMetadata = {
|
|
140
|
-
signatureEmail: link.SignatureEmail || undefined,
|
|
141
|
-
armoredKey: link.NodeKey,
|
|
142
|
-
armoredNodePassphrase: link.NodePassphrase,
|
|
143
|
-
armoredNodePassphraseSignature: link.NodePassphraseSignature || undefined,
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
if (link.Type === 1 && link.FolderProperties) {
|
|
147
|
-
return {
|
|
148
|
-
...baseNodeMetadata,
|
|
149
|
-
encryptedCrypto: {
|
|
150
|
-
...baseCryptoNodeMetadata,
|
|
151
|
-
folder: {
|
|
152
|
-
armoredHashKey: link.FolderProperties.NodeHashKey as string,
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (link.Type === 2 && link.FileProperties?.ContentKeyPacket) {
|
|
159
|
-
return {
|
|
160
|
-
...baseNodeMetadata,
|
|
161
|
-
totalStorageSize: link.FileProperties.ActiveRevision?.Size || undefined,
|
|
162
|
-
mediaType: link.MIMEType || undefined,
|
|
163
|
-
encryptedCrypto: {
|
|
164
|
-
...baseCryptoNodeMetadata,
|
|
165
|
-
file: {
|
|
166
|
-
base64ContentKeyPacket: link.FileProperties.ContentKeyPacket,
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
throw new Error(`Unknown node type: ${link.Type}`);
|
|
173
|
-
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { PrivateKey } from '../../crypto';
|
|
2
2
|
import { ProtonDriveCryptoCache, Logger } from '../../interface';
|
|
3
|
-
import { DecryptedNodeKeys } from './interface';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Provides caching for public link crypto material.
|
|
@@ -39,41 +38,8 @@ export class SharingPublicCryptoCache {
|
|
|
39
38
|
}
|
|
40
39
|
return shareKeyData.publicShareKey.key;
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
async setNodeKeys(nodeUid: string, keys: DecryptedNodeKeys): Promise<void> {
|
|
44
|
-
const cacheUid = getNodeCacheKey(nodeUid);
|
|
45
|
-
await this.driveCache.setEntity(cacheUid, {
|
|
46
|
-
nodeKeys: keys,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async getNodeKeys(nodeUid: string): Promise<DecryptedNodeKeys> {
|
|
51
|
-
const nodeKeysData = await this.driveCache.getEntity(getNodeCacheKey(nodeUid));
|
|
52
|
-
if (!nodeKeysData.nodeKeys) {
|
|
53
|
-
try {
|
|
54
|
-
await this.removeNodeKeys([nodeUid]);
|
|
55
|
-
} catch (removingError: unknown) {
|
|
56
|
-
// The node keys will not be returned, thus SDK will re-fetch
|
|
57
|
-
// and re-cache it. Setting it again should then fix the problem.
|
|
58
|
-
this.logger.warn(
|
|
59
|
-
`Failed to remove corrupted public node keys from the cache: ${removingError instanceof Error ? removingError.message : removingError}`,
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
throw new Error(`Failed to deserialize public node keys`);
|
|
63
|
-
}
|
|
64
|
-
return nodeKeysData.nodeKeys;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async removeNodeKeys(nodeUids: string[]): Promise<void> {
|
|
68
|
-
const cacheUids = nodeUids.map(getNodeCacheKey);
|
|
69
|
-
await this.driveCache.removeEntities(cacheUids);
|
|
70
|
-
}
|
|
71
41
|
}
|
|
72
42
|
|
|
73
43
|
function getShareKeyCacheKey() {
|
|
74
44
|
return 'publicShareKey';
|
|
75
45
|
}
|
|
76
|
-
|
|
77
|
-
function getNodeCacheKey(nodeUid: string) {
|
|
78
|
-
return `publicNodeKeys-${nodeUid}`;
|
|
79
|
-
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { c } from 'ttag';
|
|
2
|
+
|
|
3
|
+
import { VERIFICATION_STATUS } from '../../crypto';
|
|
4
|
+
import { getVerificationMessage } from '../errors';
|
|
5
|
+
import {
|
|
6
|
+
resultOk,
|
|
7
|
+
resultError,
|
|
8
|
+
Author,
|
|
9
|
+
AnonymousUser,
|
|
10
|
+
ProtonDriveTelemetry,
|
|
11
|
+
MetricVerificationErrorField,
|
|
12
|
+
MetricVolumeType,
|
|
13
|
+
MetricsDecryptionErrorField,
|
|
14
|
+
Logger,
|
|
15
|
+
} from '../../interface';
|
|
16
|
+
|
|
17
|
+
export class SharingPublicCryptoReporter {
|
|
18
|
+
private logger: Logger;
|
|
19
|
+
private telemetry: ProtonDriveTelemetry;
|
|
20
|
+
|
|
21
|
+
constructor(telemetry: ProtonDriveTelemetry) {
|
|
22
|
+
this.telemetry = telemetry;
|
|
23
|
+
this.logger = telemetry.getLogger('sharingPublic-crypto');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async handleClaimedAuthor(
|
|
27
|
+
node: { uid: string; creationTime: Date },
|
|
28
|
+
field: MetricVerificationErrorField,
|
|
29
|
+
signatureType: string,
|
|
30
|
+
verified: VERIFICATION_STATUS,
|
|
31
|
+
verificationErrors?: Error[],
|
|
32
|
+
claimedAuthor?: string,
|
|
33
|
+
notAvailableVerificationKeys = false,
|
|
34
|
+
): Promise<Author> {
|
|
35
|
+
if (verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
|
|
36
|
+
return resultOk(claimedAuthor || (null as AnonymousUser));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return resultError({
|
|
40
|
+
claimedAuthor,
|
|
41
|
+
error: !claimedAuthor
|
|
42
|
+
? c('Info').t`Author is not provided on public link`
|
|
43
|
+
: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
reportDecryptionError(
|
|
48
|
+
node: { uid: string; creationTime: Date },
|
|
49
|
+
field: MetricsDecryptionErrorField,
|
|
50
|
+
error: unknown,
|
|
51
|
+
) {
|
|
52
|
+
const fromBefore2024 = node.creationTime < new Date('2024-01-01');
|
|
53
|
+
|
|
54
|
+
this.logger.error(
|
|
55
|
+
`Failed to decrypt public link node ${node.uid} (from before 2024: ${fromBefore2024})`,
|
|
56
|
+
error,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
this.telemetry.recordMetric({
|
|
60
|
+
eventName: 'decryptionError',
|
|
61
|
+
volumeType: MetricVolumeType.SharedPublic,
|
|
62
|
+
field,
|
|
63
|
+
fromBefore2024,
|
|
64
|
+
error,
|
|
65
|
+
uid: node.uid,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
reportVerificationError() {
|
|
70
|
+
// Authors or signatures are not provided on public links.
|
|
71
|
+
// We do not report any signature verification errors at this moment.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,30 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { DriveCrypto, PrivateKey, VERIFICATION_STATUS } from '../../crypto';
|
|
4
|
-
import { getVerificationMessage } from '../errors';
|
|
5
|
-
import {
|
|
6
|
-
resultOk,
|
|
7
|
-
resultError,
|
|
8
|
-
Author,
|
|
9
|
-
AnonymousUser,
|
|
10
|
-
ProtonDriveTelemetry,
|
|
11
|
-
MetricVerificationErrorField,
|
|
12
|
-
MetricVolumeType,
|
|
13
|
-
MetricsDecryptionErrorField,
|
|
14
|
-
Logger,
|
|
15
|
-
ProtonDriveAccount,
|
|
16
|
-
} from '../../interface';
|
|
17
|
-
import { NodesCryptoService } from '../nodes/cryptoService';
|
|
1
|
+
import { DriveCrypto, PrivateKey } from '../../crypto';
|
|
18
2
|
import { EncryptedShareCrypto } from './interface';
|
|
19
3
|
|
|
20
|
-
export class SharingPublicCryptoService
|
|
4
|
+
export class SharingPublicCryptoService {
|
|
21
5
|
constructor(
|
|
22
|
-
|
|
23
|
-
driveCrypto: DriveCrypto,
|
|
24
|
-
account: ProtonDriveAccount,
|
|
6
|
+
private driveCrypto: DriveCrypto,
|
|
25
7
|
private password: string,
|
|
26
8
|
) {
|
|
27
|
-
|
|
9
|
+
this.driveCrypto = driveCrypto;
|
|
28
10
|
this.password = password;
|
|
29
11
|
}
|
|
30
12
|
|
|
@@ -38,61 +20,3 @@ export class SharingPublicCryptoService extends NodesCryptoService {
|
|
|
38
20
|
return shareKey;
|
|
39
21
|
}
|
|
40
22
|
}
|
|
41
|
-
|
|
42
|
-
class SharingPublicCryptoReporter {
|
|
43
|
-
private logger: Logger;
|
|
44
|
-
private telemetry: ProtonDriveTelemetry;
|
|
45
|
-
|
|
46
|
-
constructor(telemetry: ProtonDriveTelemetry) {
|
|
47
|
-
this.telemetry = telemetry;
|
|
48
|
-
this.logger = telemetry.getLogger('sharingPublic-crypto');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async handleClaimedAuthor(
|
|
52
|
-
node: { uid: string; creationTime: Date },
|
|
53
|
-
field: MetricVerificationErrorField,
|
|
54
|
-
signatureType: string,
|
|
55
|
-
verified: VERIFICATION_STATUS,
|
|
56
|
-
verificationErrors?: Error[],
|
|
57
|
-
claimedAuthor?: string,
|
|
58
|
-
notAvailableVerificationKeys = false,
|
|
59
|
-
): Promise<Author> {
|
|
60
|
-
if (verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
|
|
61
|
-
return resultOk(claimedAuthor || (null as AnonymousUser));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return resultError({
|
|
65
|
-
claimedAuthor,
|
|
66
|
-
error: !claimedAuthor
|
|
67
|
-
? c('Info').t`Author is not provided on public link`
|
|
68
|
-
: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
reportDecryptionError(
|
|
73
|
-
node: { uid: string; creationTime: Date },
|
|
74
|
-
field: MetricsDecryptionErrorField,
|
|
75
|
-
error: unknown,
|
|
76
|
-
) {
|
|
77
|
-
const fromBefore2024 = node.creationTime < new Date('2024-01-01');
|
|
78
|
-
|
|
79
|
-
this.logger.error(
|
|
80
|
-
`Failed to decrypt public link node ${node.uid} (from before 2024: ${fromBefore2024})`,
|
|
81
|
-
error,
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
this.telemetry.recordMetric({
|
|
85
|
-
eventName: 'decryptionError',
|
|
86
|
-
volumeType: MetricVolumeType.SharedPublic,
|
|
87
|
-
field,
|
|
88
|
-
fromBefore2024,
|
|
89
|
-
error,
|
|
90
|
-
uid: node.uid,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
reportVerificationError() {
|
|
95
|
-
// Authors or signatures are not provided on public links.
|
|
96
|
-
// We do not report any signature verification errors at this moment.
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { DriveCrypto } from '../../crypto';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ProtonDriveCryptoCache,
|
|
4
|
+
ProtonDriveTelemetry,
|
|
5
|
+
ProtonDriveAccount,
|
|
6
|
+
ProtonDriveEntitiesCache,
|
|
7
|
+
} from '../../interface';
|
|
3
8
|
import { DriveAPIService } from '../apiService';
|
|
9
|
+
import { NodeAPIService } from '../nodes/apiService';
|
|
10
|
+
import { NodesCache } from '../nodes/cache';
|
|
11
|
+
import { NodesCryptoCache } from '../nodes/cryptoCache';
|
|
12
|
+
import { NodesCryptoService } from '../nodes/cryptoService';
|
|
13
|
+
import { NodesRevisons } from '../nodes/nodesRevisions';
|
|
4
14
|
import { SharingPublicAPIService } from './apiService';
|
|
5
15
|
import { SharingPublicCryptoCache } from './cryptoCache';
|
|
16
|
+
import { SharingPublicCryptoReporter } from './cryptoReporter';
|
|
6
17
|
import { SharingPublicCryptoService } from './cryptoService';
|
|
7
|
-
import {
|
|
18
|
+
import { SharingPublicNodesAccess } from './nodes';
|
|
19
|
+
import { SharingPublicSharesManager } from './shares';
|
|
8
20
|
|
|
9
21
|
export { SharingPublicSessionManager } from './session/manager';
|
|
10
22
|
|
|
@@ -20,22 +32,72 @@ export { SharingPublicSessionManager } from './session/manager';
|
|
|
20
32
|
export function initSharingPublicModule(
|
|
21
33
|
telemetry: ProtonDriveTelemetry,
|
|
22
34
|
apiService: DriveAPIService,
|
|
35
|
+
driveEntitiesCache: ProtonDriveEntitiesCache,
|
|
23
36
|
driveCryptoCache: ProtonDriveCryptoCache,
|
|
24
37
|
driveCrypto: DriveCrypto,
|
|
25
38
|
account: ProtonDriveAccount,
|
|
39
|
+
url: string,
|
|
26
40
|
token: string,
|
|
27
41
|
password: string,
|
|
28
42
|
) {
|
|
29
43
|
const api = new SharingPublicAPIService(telemetry.getLogger('sharingPublic-api'), apiService);
|
|
30
44
|
const cryptoCache = new SharingPublicCryptoCache(telemetry.getLogger('sharingPublic-crypto'), driveCryptoCache);
|
|
31
|
-
const cryptoService = new SharingPublicCryptoService(
|
|
32
|
-
const
|
|
33
|
-
|
|
45
|
+
const cryptoService = new SharingPublicCryptoService(driveCrypto, password);
|
|
46
|
+
const shares = new SharingPublicSharesManager(api, cryptoCache, cryptoService, account, token);
|
|
47
|
+
const nodes = initSharingPublicNodesModule(
|
|
48
|
+
telemetry,
|
|
49
|
+
apiService,
|
|
50
|
+
driveEntitiesCache,
|
|
51
|
+
driveCryptoCache,
|
|
52
|
+
driveCrypto,
|
|
53
|
+
account,
|
|
54
|
+
shares,
|
|
55
|
+
url,
|
|
56
|
+
token,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
shares,
|
|
61
|
+
nodes,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Provides facade for the public link nodes module.
|
|
67
|
+
*
|
|
68
|
+
* The public link nodes initializes the core nodes module, but uses public
|
|
69
|
+
* link shares or crypto reporter instead.
|
|
70
|
+
*/
|
|
71
|
+
export function initSharingPublicNodesModule(
|
|
72
|
+
telemetry: ProtonDriveTelemetry,
|
|
73
|
+
apiService: DriveAPIService,
|
|
74
|
+
driveEntitiesCache: ProtonDriveEntitiesCache,
|
|
75
|
+
driveCryptoCache: ProtonDriveCryptoCache,
|
|
76
|
+
driveCrypto: DriveCrypto,
|
|
77
|
+
account: ProtonDriveAccount,
|
|
78
|
+
sharesService: SharingPublicSharesManager,
|
|
79
|
+
url: string,
|
|
80
|
+
token: string,
|
|
81
|
+
) {
|
|
82
|
+
const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService);
|
|
83
|
+
const cache = new NodesCache(telemetry.getLogger('nodes-cache'), driveEntitiesCache);
|
|
84
|
+
const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
|
|
85
|
+
const cryptoReporter = new SharingPublicCryptoReporter(telemetry);
|
|
86
|
+
const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
|
|
87
|
+
const nodesAccess = new SharingPublicNodesAccess(
|
|
88
|
+
telemetry.getLogger('nodes'),
|
|
34
89
|
api,
|
|
90
|
+
cache,
|
|
35
91
|
cryptoCache,
|
|
36
92
|
cryptoService,
|
|
93
|
+
sharesService,
|
|
94
|
+
url,
|
|
37
95
|
token,
|
|
38
96
|
);
|
|
97
|
+
const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
|
|
39
98
|
|
|
40
|
-
return
|
|
99
|
+
return {
|
|
100
|
+
access: nodesAccess,
|
|
101
|
+
revisions: nodesRevisions,
|
|
102
|
+
};
|
|
41
103
|
}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
// TODO: use them directly, or avoid them completely
|
|
2
|
-
export type {
|
|
3
|
-
EncryptedNode,
|
|
4
|
-
EncryptedNodeFolderCrypto,
|
|
5
|
-
EncryptedNodeFileCrypto,
|
|
6
|
-
DecryptedNode,
|
|
7
|
-
DecryptedNodeKeys,
|
|
8
|
-
} from '../nodes/interface';
|
|
9
|
-
|
|
10
1
|
export interface EncryptedShareCrypto {
|
|
11
2
|
base64UrlPasswordSalt: string;
|
|
12
3
|
armoredKey: string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Logger } from "../../interface";
|
|
2
|
+
import { NodeAPIService } from "../nodes/apiService";
|
|
3
|
+
import { NodesCache } from "../nodes/cache";
|
|
4
|
+
import { NodesCryptoCache } from "../nodes/cryptoCache";
|
|
5
|
+
import { NodesCryptoService } from "../nodes/cryptoService";
|
|
6
|
+
import { NodesAccess } from "../nodes/nodesAccess";
|
|
7
|
+
import { isProtonDocument, isProtonSheet } from "../nodes/mediaTypes";
|
|
8
|
+
import { splitNodeUid } from "../uids";
|
|
9
|
+
import { SharingPublicSharesManager } from "./shares";
|
|
10
|
+
|
|
11
|
+
export class SharingPublicNodesAccess extends NodesAccess {
|
|
12
|
+
constructor(
|
|
13
|
+
logger: Logger,
|
|
14
|
+
apiService: NodeAPIService,
|
|
15
|
+
cache: NodesCache,
|
|
16
|
+
cryptoCache: NodesCryptoCache,
|
|
17
|
+
cryptoService: NodesCryptoService,
|
|
18
|
+
sharesService: SharingPublicSharesManager,
|
|
19
|
+
private url: string,
|
|
20
|
+
private token: string,
|
|
21
|
+
) {
|
|
22
|
+
super(logger, apiService, cache, cryptoCache, cryptoService, sharesService);
|
|
23
|
+
this.token = token;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getNodeUrl(nodeUid: string): Promise<string> {
|
|
27
|
+
const node = await this.getNode(nodeUid);
|
|
28
|
+
if (isProtonDocument(node.mediaType) || isProtonSheet(node.mediaType)) {
|
|
29
|
+
const { nodeId } = splitNodeUid(nodeUid);
|
|
30
|
+
const type = isProtonDocument(node.mediaType) ? 'doc' : 'sheet';
|
|
31
|
+
return `https://docs.proton.me/doc?type=${type}&mode=open-url&token=${this.token}&linkId=${nodeId}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Public link doesn't support specific node URLs.
|
|
35
|
+
return this.url;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -9,7 +9,7 @@ type PostPublicLinkAuthRequest = Extract<
|
|
|
9
9
|
{ content: object }
|
|
10
10
|
>['content']['application/json'];
|
|
11
11
|
type PostPublicLinkAuthResponse =
|
|
12
|
-
|
|
12
|
+
drivePaths['/drive/urls/{token}/auth']['post']['responses']['200']['content']['application/json'];
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Provides API communication for managing public link session (not data).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { SRPModule } from
|
|
2
|
-
import { SharingPublicSessionAPIService } from
|
|
3
|
-
import { PublicLinkInfo, PublicLinkSrpInfo } from
|
|
1
|
+
import { SRPModule } from '../../../crypto';
|
|
2
|
+
import { SharingPublicSessionAPIService } from './apiService';
|
|
3
|
+
import { PublicLinkInfo, PublicLinkSrpInfo } from './interface';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Session for a public link.
|
|
@@ -9,7 +9,7 @@ describe('getTokenAndPasswordFromUrl', () => {
|
|
|
9
9
|
|
|
10
10
|
expect(result).toEqual({
|
|
11
11
|
token: 'abc123',
|
|
12
|
-
password: 'def456'
|
|
12
|
+
password: 'def456',
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ describe('getTokenAndPasswordFromUrl', () => {
|
|
|
19
19
|
|
|
20
20
|
expect(result).toEqual({
|
|
21
21
|
token: 'mytoken',
|
|
22
|
-
password: 'mypassword'
|
|
22
|
+
password: 'mypassword',
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -29,7 +29,7 @@ describe('getTokenAndPasswordFromUrl', () => {
|
|
|
29
29
|
|
|
30
30
|
expect(result).toEqual({
|
|
31
31
|
token: 'token123',
|
|
32
|
-
password: 'password456'
|
|
32
|
+
password: 'password456',
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
35
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { PrivateKey } from '../../crypto';
|
|
2
|
+
import { MetricVolumeType, ProtonDriveAccount } from '../../interface';
|
|
3
|
+
import { splitNodeUid } from '../uids';
|
|
4
|
+
import { SharingPublicAPIService } from './apiService';
|
|
5
|
+
import { SharingPublicCryptoCache } from './cryptoCache';
|
|
6
|
+
import { SharingPublicCryptoService } from './cryptoService';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Provides high-level actions for managing public link share.
|
|
10
|
+
*
|
|
11
|
+
* The public link share manager provides the same interface as the code share
|
|
12
|
+
* service so it can be used in the same way in various modules that use shares.
|
|
13
|
+
*/
|
|
14
|
+
export class SharingPublicSharesManager {
|
|
15
|
+
private promisePublicLinkRoot?: Promise<{
|
|
16
|
+
rootIds: { volumeId: string; rootNodeId: string; rootNodeUid: string };
|
|
17
|
+
shareKey: PrivateKey;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private apiService: SharingPublicAPIService,
|
|
22
|
+
private cryptoCache: SharingPublicCryptoCache,
|
|
23
|
+
private cryptoService: SharingPublicCryptoService,
|
|
24
|
+
private account: ProtonDriveAccount,
|
|
25
|
+
private token: string,
|
|
26
|
+
) {
|
|
27
|
+
this.apiService = apiService;
|
|
28
|
+
this.cryptoCache = cryptoCache;
|
|
29
|
+
this.cryptoService = cryptoService;
|
|
30
|
+
this.account = account;
|
|
31
|
+
this.token = token;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// TODO: Rename to getRootIDs everywhere.
|
|
35
|
+
async getOwnVolumeIDs(): Promise<{ volumeId: string; rootNodeId: string; rootNodeUid: string }> {
|
|
36
|
+
const { rootIds } = await this.getPublicLinkRoot();
|
|
37
|
+
return rootIds;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getSharePrivateKey(): Promise<PrivateKey> {
|
|
41
|
+
const { shareKey } = await this.getPublicLinkRoot();
|
|
42
|
+
return shareKey;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async getPublicLinkRoot(): Promise<{
|
|
46
|
+
rootIds: { volumeId: string; rootNodeId: string; rootNodeUid: string };
|
|
47
|
+
shareKey: PrivateKey;
|
|
48
|
+
}> {
|
|
49
|
+
if (!this.promisePublicLinkRoot) {
|
|
50
|
+
this.promisePublicLinkRoot = (async () => {
|
|
51
|
+
const { encryptedNode, encryptedShare } = await this.apiService.getPublicLinkRoot(this.token);
|
|
52
|
+
|
|
53
|
+
const { volumeId, nodeId: rootNodeId } = splitNodeUid(encryptedNode.uid);
|
|
54
|
+
|
|
55
|
+
const shareKey = await this.cryptoService.decryptPublicLinkShareKey(encryptedShare);
|
|
56
|
+
await this.cryptoCache.setShareKey(shareKey);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
rootIds: { volumeId, rootNodeId, rootNodeUid: encryptedNode.uid },
|
|
60
|
+
shareKey,
|
|
61
|
+
};
|
|
62
|
+
})();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this.promisePublicLinkRoot;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async getContextShareMemberEmailKey(): Promise<{
|
|
69
|
+
email: string;
|
|
70
|
+
addressId: string;
|
|
71
|
+
addressKey: PrivateKey;
|
|
72
|
+
addressKeyId: string;
|
|
73
|
+
}> {
|
|
74
|
+
const address = await this.account.getOwnPrimaryAddress();
|
|
75
|
+
return {
|
|
76
|
+
email: address.email,
|
|
77
|
+
addressId: address.addressId,
|
|
78
|
+
addressKey: address.keys[address.primaryKeyIndex].key,
|
|
79
|
+
addressKeyId: address.keys[address.primaryKeyIndex].id,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getVolumeMetricContext(): Promise<MetricVolumeType> {
|
|
84
|
+
return MetricVolumeType.SharedPublic;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -53,8 +53,8 @@ type PostDeleteNodesResponse =
|
|
|
53
53
|
|
|
54
54
|
export class UploadAPIService {
|
|
55
55
|
constructor(
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
protected apiService: DriveAPIService,
|
|
57
|
+
protected clientUid: string | undefined,
|
|
58
58
|
) {
|
|
59
59
|
this.apiService = apiService;
|
|
60
60
|
this.clientUid = clientUid;
|
|
@@ -110,6 +110,17 @@ export class UploadAPIService {
|
|
|
110
110
|
nodeUid: string;
|
|
111
111
|
nodeRevisionUid: string;
|
|
112
112
|
}> {
|
|
113
|
+
// The client shouldn't send the clear text size of the file.
|
|
114
|
+
// The intented upload size is needed only for early validation that
|
|
115
|
+
// the file can fit in the remaining quota to avoid data transfer when
|
|
116
|
+
// the upload would be rejected. The backend will still validate
|
|
117
|
+
// the quota during block upload and revision commit.
|
|
118
|
+
const precision = 100_000; // bytes
|
|
119
|
+
const intendedUploadSize =
|
|
120
|
+
node.intendedUploadSize && node.intendedUploadSize > precision
|
|
121
|
+
? Math.floor(node.intendedUploadSize / precision) * precision
|
|
122
|
+
: null;
|
|
123
|
+
|
|
113
124
|
const { volumeId, nodeId: parentNodeId } = splitNodeUid(parentNodeUid);
|
|
114
125
|
const result = await this.apiService.post<PostCreateDraftRequest, PostCreateDraftResponse>(
|
|
115
126
|
`drive/v2/volumes/${volumeId}/files`,
|
|
@@ -119,7 +130,7 @@ export class UploadAPIService {
|
|
|
119
130
|
Hash: node.hash,
|
|
120
131
|
MIMEType: node.mediaType,
|
|
121
132
|
ClientUID: this.clientUid || null,
|
|
122
|
-
IntendedUploadSize:
|
|
133
|
+
IntendedUploadSize: intendedUploadSize,
|
|
123
134
|
NodeKey: node.armoredNodeKey,
|
|
124
135
|
NodePassphrase: node.armoredNodePassphrase,
|
|
125
136
|
NodePassphraseSignature: node.armoredNodePassphraseSignature,
|
|
@@ -252,7 +263,7 @@ export class UploadAPIService {
|
|
|
252
263
|
ManifestSignature: options.armoredManifestSignature,
|
|
253
264
|
SignatureAddress: options.signatureEmail,
|
|
254
265
|
XAttr: options.armoredExtendedAttributes || null,
|
|
255
|
-
Photo: null, //
|
|
266
|
+
Photo: null, // Only used for photos in the Photo volume.
|
|
256
267
|
});
|
|
257
268
|
}
|
|
258
269
|
|