@protontech/drive-sdk 0.1.2 → 0.2.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/driveCrypto.d.ts +11 -0
- package/dist/crypto/driveCrypto.js +20 -7
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/interface.d.ts +10 -1
- package/dist/crypto/openPGPCrypto.d.ts +18 -2
- package/dist/crypto/openPGPCrypto.js +25 -6
- package/dist/crypto/openPGPCrypto.js.map +1 -1
- package/dist/diagnostic/telemetry.d.ts +1 -1
- package/dist/diagnostic/telemetry.js +1 -1
- package/dist/diagnostic/telemetry.js.map +1 -1
- package/dist/interface/download.d.ts +46 -0
- package/dist/interface/index.d.ts +2 -2
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +26 -1
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/sharing.d.ts +3 -1
- package/dist/interface/telemetry.d.ts +5 -2
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/internal/apiService/apiService.js +1 -1
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +78 -165
- package/dist/internal/apiService/errorCodes.d.ts +1 -0
- package/dist/internal/apiService/errors.js +1 -0
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/apiService/index.d.ts +1 -1
- package/dist/internal/apiService/index.js +2 -2
- package/dist/internal/apiService/index.js.map +1 -1
- package/dist/internal/apiService/transformers.d.ts +1 -1
- package/dist/internal/apiService/transformers.js +2 -2
- package/dist/internal/apiService/transformers.js.map +1 -1
- package/dist/internal/download/blockIndex.d.ts +11 -0
- package/dist/internal/download/blockIndex.js +35 -0
- package/dist/internal/download/blockIndex.js.map +1 -0
- package/dist/internal/download/blockIndex.test.d.ts +1 -0
- package/dist/internal/download/blockIndex.test.js +147 -0
- package/dist/internal/download/blockIndex.test.js.map +1 -0
- package/dist/internal/download/fileDownloader.d.ts +6 -2
- package/dist/internal/download/fileDownloader.js +83 -6
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +69 -4
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/download/interface.d.ts +4 -4
- package/dist/internal/download/seekableStream.d.ts +80 -0
- package/dist/internal/download/seekableStream.js +163 -0
- package/dist/internal/download/seekableStream.js.map +1 -0
- package/dist/internal/download/seekableStream.test.d.ts +1 -0
- package/dist/internal/download/seekableStream.test.js +149 -0
- package/dist/internal/download/seekableStream.test.js.map +1 -0
- package/dist/internal/download/telemetry.js +1 -1
- package/dist/internal/download/telemetry.js.map +1 -1
- package/dist/internal/download/telemetry.test.js +7 -7
- package/dist/internal/download/telemetry.test.js.map +1 -1
- package/dist/internal/errors.d.ts +1 -1
- package/dist/internal/errors.js +7 -1
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/errors.test.js +44 -10
- package/dist/internal/errors.test.js.map +1 -1
- package/dist/internal/events/index.js +1 -1
- package/dist/internal/events/index.js.map +1 -1
- package/dist/internal/nodes/apiService.js +16 -3
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +43 -7
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.js +12 -3
- package/dist/internal/nodes/cache.js.map +1 -1
- package/dist/internal/nodes/cache.test.js +31 -1
- package/dist/internal/nodes/cache.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +4 -1
- package/dist/internal/nodes/cryptoService.js +66 -16
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +129 -46
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.d.ts +2 -1
- package/dist/internal/nodes/extendedAttributes.js +27 -1
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.test.js +59 -6
- package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
- package/dist/internal/nodes/index.test.js +1 -1
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +18 -2
- package/dist/internal/nodes/nodesAccess.js +11 -1
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.js +1 -1
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesRevisions.d.ts +4 -3
- package/dist/internal/nodes/nodesRevisions.js +2 -2
- package/dist/internal/nodes/nodesRevisions.js.map +1 -1
- package/dist/internal/shares/cryptoService.js +7 -4
- package/dist/internal/shares/cryptoService.js.map +1 -1
- package/dist/internal/shares/cryptoService.test.js +5 -3
- package/dist/internal/shares/cryptoService.test.js.map +1 -1
- package/dist/internal/sharing/apiService.js +5 -5
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/sharing/cryptoService.d.ts +1 -0
- package/dist/internal/sharing/cryptoService.js +22 -10
- package/dist/internal/sharing/cryptoService.js.map +1 -1
- package/dist/internal/sharing/cryptoService.test.js +7 -4
- package/dist/internal/sharing/cryptoService.test.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.js +4 -2
- package/dist/internal/sharing/sharingAccess.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.test.js +6 -0
- package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
- package/dist/internal/upload/telemetry.js +2 -2
- package/dist/internal/upload/telemetry.js.map +1 -1
- package/dist/internal/upload/telemetry.test.js +7 -7
- package/dist/internal/upload/telemetry.test.js.map +1 -1
- package/dist/telemetry.d.ts +2 -2
- package/dist/telemetry.js +2 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/tests/telemetry.js +1 -1
- package/dist/tests/telemetry.js.map +1 -1
- package/dist/transformers.d.ts +1 -1
- package/dist/transformers.js +2 -1
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +70 -25
- package/src/crypto/interface.ts +15 -0
- package/src/crypto/openPGPCrypto.ts +37 -5
- package/src/diagnostic/telemetry.ts +1 -1
- package/src/interface/download.ts +46 -0
- package/src/interface/index.ts +2 -1
- package/src/interface/nodes.ts +28 -1
- package/src/interface/sharing.ts +3 -1
- package/src/interface/telemetry.ts +6 -1
- package/src/internal/apiService/apiService.ts +1 -1
- package/src/internal/apiService/driveTypes.ts +78 -165
- package/src/internal/apiService/errorCodes.ts +1 -0
- package/src/internal/apiService/errors.ts +1 -0
- package/src/internal/apiService/index.ts +1 -1
- package/src/internal/apiService/transformers.ts +1 -1
- package/src/internal/download/blockIndex.test.ts +158 -0
- package/src/internal/download/blockIndex.ts +36 -0
- package/src/internal/download/fileDownloader.test.ts +100 -7
- package/src/internal/download/fileDownloader.ts +109 -9
- package/src/internal/download/interface.ts +4 -4
- package/src/internal/download/seekableStream.test.ts +187 -0
- package/src/internal/download/seekableStream.ts +182 -0
- package/src/internal/download/telemetry.test.ts +7 -7
- package/src/internal/download/telemetry.ts +1 -1
- package/src/internal/errors.test.ts +45 -11
- package/src/internal/errors.ts +8 -0
- package/src/internal/events/index.ts +1 -1
- package/src/internal/nodes/apiService.test.ts +59 -15
- package/src/internal/nodes/apiService.ts +21 -4
- package/src/internal/nodes/cache.test.ts +34 -1
- package/src/internal/nodes/cache.ts +12 -3
- package/src/internal/nodes/cryptoService.test.ts +139 -47
- package/src/internal/nodes/cryptoService.ts +94 -9
- package/src/internal/nodes/extendedAttributes.test.ts +60 -7
- package/src/internal/nodes/extendedAttributes.ts +37 -1
- package/src/internal/nodes/index.test.ts +1 -1
- package/src/internal/nodes/interface.ts +19 -2
- package/src/internal/nodes/nodesAccess.ts +15 -1
- package/src/internal/nodes/nodesManagement.ts +1 -1
- package/src/internal/nodes/nodesRevisions.ts +14 -5
- package/src/internal/shares/cryptoService.test.ts +5 -3
- package/src/internal/shares/cryptoService.ts +7 -4
- package/src/internal/sharing/apiService.ts +6 -6
- package/src/internal/sharing/cryptoService.test.ts +7 -4
- package/src/internal/sharing/cryptoService.ts +27 -10
- package/src/internal/sharing/sharingAccess.test.ts +6 -0
- package/src/internal/sharing/sharingAccess.ts +4 -2
- package/src/internal/upload/telemetry.test.ts +7 -7
- package/src/internal/upload/telemetry.ts +2 -2
- package/src/telemetry.ts +2 -2
- package/src/tests/telemetry.ts +1 -1
- package/src/transformers.ts +4 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DriveCrypto, PrivateKey, SessionKey, VERIFICATION_STATUS } from '../../crypto';
|
|
2
|
-
import { ProtonDriveAccount, ProtonDriveTelemetry, RevisionState } from '../../interface';
|
|
2
|
+
import { MemberRole, ProtonDriveAccount, ProtonDriveTelemetry, RevisionState } from '../../interface';
|
|
3
3
|
import { getMockTelemetry } from '../../tests/telemetry';
|
|
4
4
|
import { DecryptedNode, DecryptedNodeKeys, DecryptedUnparsedNode, EncryptedNode, SharesService } from './interface';
|
|
5
5
|
import { NodesCryptoService } from './cryptoService';
|
|
@@ -48,13 +48,18 @@ describe('nodesCryptoService', () => {
|
|
|
48
48
|
armoredNodeName: 'armoredName',
|
|
49
49
|
}),
|
|
50
50
|
),
|
|
51
|
-
// @ts-expect-error
|
|
51
|
+
// @ts-expect-error Faking sessionKey as string.
|
|
52
52
|
decryptAndVerifySessionKey: jest.fn(async () =>
|
|
53
53
|
Promise.resolve({
|
|
54
54
|
sessionKey: 'contentKeyPacketSessionKey',
|
|
55
55
|
verified: VERIFICATION_STATUS.SIGNED_AND_VALID,
|
|
56
56
|
}),
|
|
57
57
|
),
|
|
58
|
+
verifyInvitation: jest.fn(async () =>
|
|
59
|
+
Promise.resolve({
|
|
60
|
+
verified: VERIFICATION_STATUS.SIGNED_AND_VALID,
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
58
63
|
};
|
|
59
64
|
account = {
|
|
60
65
|
// @ts-expect-error No need to implement all methods for mocking
|
|
@@ -75,42 +80,60 @@ describe('nodesCryptoService', () => {
|
|
|
75
80
|
const parentKey = 'parentKey' as unknown as PrivateKey;
|
|
76
81
|
|
|
77
82
|
function verifyLogEventVerificationError(options = {}) {
|
|
78
|
-
expect(telemetry.
|
|
79
|
-
expect(telemetry.
|
|
83
|
+
expect(telemetry.recordMetric).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(telemetry.recordMetric).toHaveBeenCalledWith({
|
|
80
85
|
eventName: 'verificationError',
|
|
81
86
|
volumeType: 'own_volume',
|
|
82
87
|
fromBefore2024: false,
|
|
83
88
|
addressMatchingDefaultShare: false,
|
|
89
|
+
uid: 'volumeId~nodeId',
|
|
84
90
|
...options,
|
|
85
91
|
});
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
function verifyLogEventDecryptionError(options = {}) {
|
|
89
|
-
expect(telemetry.
|
|
90
|
-
expect(telemetry.
|
|
95
|
+
expect(telemetry.recordMetric).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(telemetry.recordMetric).toHaveBeenCalledWith({
|
|
91
97
|
eventName: 'decryptionError',
|
|
92
98
|
volumeType: 'own_volume',
|
|
93
99
|
fromBefore2024: false,
|
|
100
|
+
uid: 'volumeId~nodeId',
|
|
94
101
|
...options,
|
|
95
102
|
});
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
describe('folder node', () => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
folder: {
|
|
109
|
-
armoredHashKey: 'armoredHashKey',
|
|
110
|
-
armoredExtendedAttributes: 'folderArmoredExtendedAttributes',
|
|
106
|
+
let encryptedNode: EncryptedNode;
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
encryptedNode = {
|
|
110
|
+
uid: 'volumeId~nodeId',
|
|
111
|
+
parentUid: 'volumeId~parentId',
|
|
112
|
+
membership: {
|
|
113
|
+
role: MemberRole.Admin,
|
|
114
|
+
inviteTime: new Date(1234567890000),
|
|
111
115
|
},
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
encryptedCrypto: {
|
|
117
|
+
signatureEmail: 'signatureEmail',
|
|
118
|
+
nameSignatureEmail: 'nameSignatureEmail',
|
|
119
|
+
armoredKey: 'armoredKey',
|
|
120
|
+
armoredNodePassphrase: 'armoredNodePassphrase',
|
|
121
|
+
armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
|
|
122
|
+
folder: {
|
|
123
|
+
armoredHashKey: 'armoredHashKey',
|
|
124
|
+
armoredExtendedAttributes: 'folderArmoredExtendedAttributes',
|
|
125
|
+
},
|
|
126
|
+
membership: {
|
|
127
|
+
inviterEmail: 'inviterEmail',
|
|
128
|
+
base64MemberSharePassphraseKeyPacket: 'base64MemberSharePassphraseKeyPacket',
|
|
129
|
+
armoredInviterSharePassphraseKeyPacketSignature:
|
|
130
|
+
'armoredInviterSharePassphraseKeyPacketSignature',
|
|
131
|
+
armoredInviteeSharePassphraseSessionKeySignature:
|
|
132
|
+
'armoredInviteeSharePassphraseSessionKeySignature',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
} as EncryptedNode;
|
|
136
|
+
});
|
|
114
137
|
|
|
115
138
|
function verifyResult(
|
|
116
139
|
result: { node: DecryptedUnparsedNode; keys?: DecryptedNodeKeys },
|
|
@@ -125,6 +148,11 @@ describe('nodesCryptoService', () => {
|
|
|
125
148
|
folder: {
|
|
126
149
|
extendedAttributes: '{}',
|
|
127
150
|
},
|
|
151
|
+
membership: {
|
|
152
|
+
role: MemberRole.Admin,
|
|
153
|
+
inviteTime: new Date(1234567890000),
|
|
154
|
+
sharedBy: { ok: true, value: 'inviterEmail' },
|
|
155
|
+
},
|
|
128
156
|
activeRevision: undefined,
|
|
129
157
|
errors: undefined,
|
|
130
158
|
...expectedNode,
|
|
@@ -145,19 +173,7 @@ describe('nodesCryptoService', () => {
|
|
|
145
173
|
|
|
146
174
|
describe('should decrypt successfuly', () => {
|
|
147
175
|
it('same author everywhere', async () => {
|
|
148
|
-
|
|
149
|
-
encryptedCrypto: {
|
|
150
|
-
signatureEmail: 'signatureEmail',
|
|
151
|
-
nameSignatureEmail: 'signatureEmail',
|
|
152
|
-
armoredKey: 'armoredKey',
|
|
153
|
-
armoredNodePassphrase: 'armoredNodePassphrase',
|
|
154
|
-
armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
|
|
155
|
-
folder: {
|
|
156
|
-
armoredHashKey: 'armoredHashKey',
|
|
157
|
-
armoredExtendedAttributes: 'folderArmoredExtendedAttributes',
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
} as EncryptedNode;
|
|
176
|
+
encryptedNode.encryptedCrypto.nameSignatureEmail = 'signatureEmail';
|
|
161
177
|
|
|
162
178
|
const result = await cryptoService.decryptNode(encryptedNode, parentKey);
|
|
163
179
|
verifyResult(result, {
|
|
@@ -165,18 +181,20 @@ describe('nodesCryptoService', () => {
|
|
|
165
181
|
nameAuthor: { ok: true, value: 'signatureEmail' },
|
|
166
182
|
});
|
|
167
183
|
|
|
168
|
-
expect(account.getPublicKeys).toHaveBeenCalledTimes(
|
|
184
|
+
expect(account.getPublicKeys).toHaveBeenCalledTimes(2); // signatureEmail (for both key and name) and inviterEmail
|
|
169
185
|
expect(account.getPublicKeys).toHaveBeenCalledWith('signatureEmail');
|
|
170
|
-
expect(
|
|
186
|
+
expect(account.getPublicKeys).toHaveBeenCalledWith('inviterEmail');
|
|
187
|
+
expect(telemetry.recordMetric).not.toHaveBeenCalled();
|
|
171
188
|
});
|
|
172
189
|
|
|
173
190
|
it('different authors on key and name', async () => {
|
|
174
191
|
const result = await cryptoService.decryptNode(encryptedNode, parentKey);
|
|
175
192
|
verifyResult(result);
|
|
176
|
-
expect(account.getPublicKeys).toHaveBeenCalledTimes(
|
|
193
|
+
expect(account.getPublicKeys).toHaveBeenCalledTimes(3); // signatureEmail, nameSignatureEmail, inviterEmail
|
|
177
194
|
expect(account.getPublicKeys).toHaveBeenCalledWith('signatureEmail');
|
|
178
195
|
expect(account.getPublicKeys).toHaveBeenCalledWith('nameSignatureEmail');
|
|
179
|
-
expect(
|
|
196
|
+
expect(account.getPublicKeys).toHaveBeenCalledWith('inviterEmail');
|
|
197
|
+
expect(telemetry.recordMetric).not.toHaveBeenCalled();
|
|
180
198
|
});
|
|
181
199
|
});
|
|
182
200
|
|
|
@@ -188,6 +206,7 @@ describe('nodesCryptoService', () => {
|
|
|
188
206
|
key: 'decryptedKey' as unknown as PrivateKey,
|
|
189
207
|
passphraseSessionKey: 'passphraseSessionKey' as unknown as SessionKey,
|
|
190
208
|
verified: VERIFICATION_STATUS.NOT_SIGNED,
|
|
209
|
+
verificationErrors: [new Error('verification error')],
|
|
191
210
|
}),
|
|
192
211
|
);
|
|
193
212
|
|
|
@@ -200,6 +219,7 @@ describe('nodesCryptoService', () => {
|
|
|
200
219
|
});
|
|
201
220
|
verifyLogEventVerificationError({
|
|
202
221
|
field: 'nodeKey',
|
|
222
|
+
error: 'verification error',
|
|
203
223
|
});
|
|
204
224
|
});
|
|
205
225
|
|
|
@@ -208,6 +228,7 @@ describe('nodesCryptoService', () => {
|
|
|
208
228
|
Promise.resolve({
|
|
209
229
|
name: 'name',
|
|
210
230
|
verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
|
|
231
|
+
verificationErrors: [new Error('verification error')],
|
|
211
232
|
}),
|
|
212
233
|
);
|
|
213
234
|
|
|
@@ -215,11 +236,15 @@ describe('nodesCryptoService', () => {
|
|
|
215
236
|
verifyResult(result, {
|
|
216
237
|
nameAuthor: {
|
|
217
238
|
ok: false,
|
|
218
|
-
error: {
|
|
239
|
+
error: {
|
|
240
|
+
claimedAuthor: 'nameSignatureEmail',
|
|
241
|
+
error: 'Signature verification for name failed: verification error',
|
|
242
|
+
},
|
|
219
243
|
},
|
|
220
244
|
});
|
|
221
245
|
verifyLogEventVerificationError({
|
|
222
246
|
field: 'nodeName',
|
|
247
|
+
error: 'verification error',
|
|
223
248
|
});
|
|
224
249
|
});
|
|
225
250
|
|
|
@@ -228,6 +253,7 @@ describe('nodesCryptoService', () => {
|
|
|
228
253
|
Promise.resolve({
|
|
229
254
|
hashKey: new Uint8Array(),
|
|
230
255
|
verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
|
|
256
|
+
verificationErrors: [new Error('verification error')],
|
|
231
257
|
}),
|
|
232
258
|
);
|
|
233
259
|
|
|
@@ -235,11 +261,15 @@ describe('nodesCryptoService', () => {
|
|
|
235
261
|
verifyResult(result, {
|
|
236
262
|
keyAuthor: {
|
|
237
263
|
ok: false,
|
|
238
|
-
error: {
|
|
264
|
+
error: {
|
|
265
|
+
claimedAuthor: 'signatureEmail',
|
|
266
|
+
error: 'Signature verification for hash key failed: verification error',
|
|
267
|
+
},
|
|
239
268
|
},
|
|
240
269
|
});
|
|
241
270
|
verifyLogEventVerificationError({
|
|
242
271
|
field: 'nodeHashKey',
|
|
272
|
+
error: 'verification error',
|
|
243
273
|
});
|
|
244
274
|
});
|
|
245
275
|
|
|
@@ -250,6 +280,7 @@ describe('nodesCryptoService', () => {
|
|
|
250
280
|
key: 'decryptedKey' as unknown as PrivateKey,
|
|
251
281
|
passphraseSessionKey: 'passphraseSessionKey' as unknown as SessionKey,
|
|
252
282
|
verified: VERIFICATION_STATUS.NOT_SIGNED,
|
|
283
|
+
verificationErrors: [new Error('verification error')],
|
|
253
284
|
}),
|
|
254
285
|
);
|
|
255
286
|
driveCrypto.decryptNodeHashKey = jest.fn(async () =>
|
|
@@ -268,6 +299,7 @@ describe('nodesCryptoService', () => {
|
|
|
268
299
|
});
|
|
269
300
|
verifyLogEventVerificationError({
|
|
270
301
|
field: 'nodeKey',
|
|
302
|
+
error: 'verification error',
|
|
271
303
|
});
|
|
272
304
|
});
|
|
273
305
|
|
|
@@ -276,6 +308,7 @@ describe('nodesCryptoService', () => {
|
|
|
276
308
|
Promise.resolve({
|
|
277
309
|
extendedAttributes: '{}',
|
|
278
310
|
verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
|
|
311
|
+
verificationErrors: [new Error('verification error')],
|
|
279
312
|
}),
|
|
280
313
|
);
|
|
281
314
|
|
|
@@ -285,12 +318,39 @@ describe('nodesCryptoService', () => {
|
|
|
285
318
|
ok: false,
|
|
286
319
|
error: {
|
|
287
320
|
claimedAuthor: 'signatureEmail',
|
|
288
|
-
error: 'Signature verification for attributes failed',
|
|
321
|
+
error: 'Signature verification for attributes failed: verification error',
|
|
289
322
|
},
|
|
290
323
|
},
|
|
291
324
|
});
|
|
292
325
|
verifyLogEventVerificationError({
|
|
293
326
|
field: 'nodeExtendedAttributes',
|
|
327
|
+
error: 'verification error',
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('on membership', async () => {
|
|
332
|
+
driveCrypto.verifyInvitation = jest.fn().mockResolvedValue({
|
|
333
|
+
verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
|
|
334
|
+
verificationErrors: [new Error('verification error')],
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const result = await cryptoService.decryptNode(encryptedNode, parentKey);
|
|
338
|
+
verifyResult(result, {
|
|
339
|
+
membership: {
|
|
340
|
+
role: MemberRole.Admin,
|
|
341
|
+
inviteTime: new Date(1234567890000),
|
|
342
|
+
sharedBy: {
|
|
343
|
+
ok: false,
|
|
344
|
+
error: {
|
|
345
|
+
claimedAuthor: 'inviterEmail',
|
|
346
|
+
error: 'Signature verification for membership failed: verification error',
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
verifyLogEventVerificationError({
|
|
352
|
+
field: 'membershipInviter',
|
|
353
|
+
error: 'verification error',
|
|
294
354
|
});
|
|
295
355
|
});
|
|
296
356
|
});
|
|
@@ -380,6 +440,27 @@ describe('nodesCryptoService', () => {
|
|
|
380
440
|
error,
|
|
381
441
|
});
|
|
382
442
|
});
|
|
443
|
+
|
|
444
|
+
it('on membership', async () => {
|
|
445
|
+
const error = new Error('Decryption error');
|
|
446
|
+
driveCrypto.verifyInvitation = jest.fn(async () => Promise.reject(error));
|
|
447
|
+
|
|
448
|
+
const result = await cryptoService.decryptNode(encryptedNode, parentKey);
|
|
449
|
+
verifyResult(result, {
|
|
450
|
+
membership: {
|
|
451
|
+
role: MemberRole.Admin,
|
|
452
|
+
inviteTime: new Date(1234567890000),
|
|
453
|
+
sharedBy: {
|
|
454
|
+
ok: false,
|
|
455
|
+
error: { claimedAuthor: 'inviterEmail', error: 'Failed to verify invitation' },
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
verifyLogEventVerificationError({
|
|
460
|
+
field: 'membershipInviter',
|
|
461
|
+
addressMatchingDefaultShare: undefined,
|
|
462
|
+
});
|
|
463
|
+
});
|
|
383
464
|
});
|
|
384
465
|
|
|
385
466
|
it('should fail when keys cannot be loaded', async () => {
|
|
@@ -491,7 +572,7 @@ describe('nodesCryptoService', () => {
|
|
|
491
572
|
|
|
492
573
|
expect(account.getPublicKeys).toHaveBeenCalledTimes(2); // node + revision
|
|
493
574
|
expect(account.getPublicKeys).toHaveBeenCalledWith('signatureEmail');
|
|
494
|
-
expect(telemetry.
|
|
575
|
+
expect(telemetry.recordMetric).not.toHaveBeenCalled();
|
|
495
576
|
});
|
|
496
577
|
|
|
497
578
|
it('different authors on key and name', async () => {
|
|
@@ -501,7 +582,7 @@ describe('nodesCryptoService', () => {
|
|
|
501
582
|
expect(account.getPublicKeys).toHaveBeenCalledWith('signatureEmail');
|
|
502
583
|
expect(account.getPublicKeys).toHaveBeenCalledWith('nameSignatureEmail');
|
|
503
584
|
expect(account.getPublicKeys).toHaveBeenCalledWith('revisionSignatureEmail');
|
|
504
|
-
expect(telemetry.
|
|
585
|
+
expect(telemetry.recordMetric).not.toHaveBeenCalled();
|
|
505
586
|
});
|
|
506
587
|
});
|
|
507
588
|
|
|
@@ -513,6 +594,7 @@ describe('nodesCryptoService', () => {
|
|
|
513
594
|
key: 'decryptedKey' as unknown as PrivateKey,
|
|
514
595
|
passphraseSessionKey: 'passphraseSessionKey' as unknown as SessionKey,
|
|
515
596
|
verified: VERIFICATION_STATUS.NOT_SIGNED,
|
|
597
|
+
verificationErrors: [new Error('verification error')],
|
|
516
598
|
}),
|
|
517
599
|
);
|
|
518
600
|
|
|
@@ -525,6 +607,7 @@ describe('nodesCryptoService', () => {
|
|
|
525
607
|
});
|
|
526
608
|
verifyLogEventVerificationError({
|
|
527
609
|
field: 'nodeKey',
|
|
610
|
+
error: 'verification error',
|
|
528
611
|
});
|
|
529
612
|
});
|
|
530
613
|
|
|
@@ -533,6 +616,7 @@ describe('nodesCryptoService', () => {
|
|
|
533
616
|
Promise.resolve({
|
|
534
617
|
name: 'name',
|
|
535
618
|
verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
|
|
619
|
+
verificationErrors: [new Error('verification error')],
|
|
536
620
|
}),
|
|
537
621
|
);
|
|
538
622
|
|
|
@@ -540,11 +624,15 @@ describe('nodesCryptoService', () => {
|
|
|
540
624
|
verifyResult(result, {
|
|
541
625
|
nameAuthor: {
|
|
542
626
|
ok: false,
|
|
543
|
-
error: {
|
|
627
|
+
error: {
|
|
628
|
+
claimedAuthor: 'nameSignatureEmail',
|
|
629
|
+
error: 'Signature verification for name failed: verification error',
|
|
630
|
+
},
|
|
544
631
|
},
|
|
545
632
|
});
|
|
546
633
|
verifyLogEventVerificationError({
|
|
547
634
|
field: 'nodeName',
|
|
635
|
+
error: 'verification error',
|
|
548
636
|
});
|
|
549
637
|
});
|
|
550
638
|
|
|
@@ -553,6 +641,7 @@ describe('nodesCryptoService', () => {
|
|
|
553
641
|
Promise.resolve({
|
|
554
642
|
extendedAttributes: '{}',
|
|
555
643
|
verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
|
|
644
|
+
verificationErrors: [new Error('verification error')],
|
|
556
645
|
}),
|
|
557
646
|
);
|
|
558
647
|
|
|
@@ -570,7 +659,7 @@ describe('nodesCryptoService', () => {
|
|
|
570
659
|
ok: false,
|
|
571
660
|
error: {
|
|
572
661
|
claimedAuthor: 'revisionSignatureEmail',
|
|
573
|
-
error: 'Signature verification for attributes failed',
|
|
662
|
+
error: 'Signature verification for attributes failed: verification error',
|
|
574
663
|
},
|
|
575
664
|
},
|
|
576
665
|
},
|
|
@@ -578,6 +667,7 @@ describe('nodesCryptoService', () => {
|
|
|
578
667
|
});
|
|
579
668
|
verifyLogEventVerificationError({
|
|
580
669
|
field: 'nodeExtendedAttributes',
|
|
670
|
+
error: 'verification error',
|
|
581
671
|
});
|
|
582
672
|
});
|
|
583
673
|
|
|
@@ -587,6 +677,7 @@ describe('nodesCryptoService', () => {
|
|
|
587
677
|
Promise.resolve({
|
|
588
678
|
sessionKey: 'contentKeyPacketSessionKey',
|
|
589
679
|
verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
|
|
680
|
+
verificationErrors: [new Error('verification error')],
|
|
590
681
|
}) as any,
|
|
591
682
|
);
|
|
592
683
|
|
|
@@ -596,12 +687,13 @@ describe('nodesCryptoService', () => {
|
|
|
596
687
|
ok: false,
|
|
597
688
|
error: {
|
|
598
689
|
claimedAuthor: 'signatureEmail',
|
|
599
|
-
error: 'Signature verification for content key failed',
|
|
690
|
+
error: 'Signature verification for content key failed: verification error',
|
|
600
691
|
},
|
|
601
692
|
},
|
|
602
693
|
});
|
|
603
694
|
verifyLogEventVerificationError({
|
|
604
695
|
field: 'nodeContentKey',
|
|
696
|
+
error: 'verification error',
|
|
605
697
|
});
|
|
606
698
|
});
|
|
607
699
|
});
|
|
@@ -743,7 +835,7 @@ describe('nodesCryptoService', () => {
|
|
|
743
835
|
});
|
|
744
836
|
|
|
745
837
|
expect(account.getPublicKeys).toHaveBeenCalledTimes(2);
|
|
746
|
-
expect(telemetry.
|
|
838
|
+
expect(telemetry.recordMetric).not.toHaveBeenCalled();
|
|
747
839
|
});
|
|
748
840
|
});
|
|
749
841
|
|
|
@@ -878,7 +970,7 @@ describe('nodesCryptoService', () => {
|
|
|
878
970
|
keyAuthor: { ok: true, value: null },
|
|
879
971
|
nameAuthor: { ok: true, value: null },
|
|
880
972
|
});
|
|
881
|
-
expect(telemetry.
|
|
973
|
+
expect(telemetry.recordMetric).not.toHaveBeenCalled();
|
|
882
974
|
expect(driveCrypto.decryptKey).toHaveBeenCalledWith(
|
|
883
975
|
encryptedNode.encryptedCrypto.armoredKey,
|
|
884
976
|
encryptedNode.encryptedCrypto.armoredNodePassphrase,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Logger,
|
|
13
13
|
MetricsDecryptionErrorField,
|
|
14
14
|
MetricVerificationErrorField,
|
|
15
|
+
Membership,
|
|
15
16
|
} from '../../interface';
|
|
16
17
|
import { ValidationError } from '../../errors';
|
|
17
18
|
import { getErrorMessage, getVerificationMessage } from '../errors';
|
|
@@ -90,6 +91,11 @@ export class NodesCryptoService {
|
|
|
90
91
|
|
|
91
92
|
const { name, author: nameAuthor } = await this.decryptName(node, parentKey, nameVerificationKeys);
|
|
92
93
|
|
|
94
|
+
let membership;
|
|
95
|
+
if (node.membership) {
|
|
96
|
+
membership = await this.decryptMembership(node);
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
let passphrase, key, passphraseSessionKey, keyAuthor;
|
|
94
100
|
try {
|
|
95
101
|
const keyResult = await this.decryptKey(node, parentKey, keyVerificationKeys);
|
|
@@ -110,6 +116,7 @@ export class NodesCryptoService {
|
|
|
110
116
|
error: errorMessage,
|
|
111
117
|
}),
|
|
112
118
|
nameAuthor,
|
|
119
|
+
membership,
|
|
113
120
|
activeRevision: 'file' in node.encryptedCrypto ? resultError(new Error(errorMessage)) : undefined,
|
|
114
121
|
folder: undefined,
|
|
115
122
|
errors: [error],
|
|
@@ -187,6 +194,7 @@ export class NodesCryptoService {
|
|
|
187
194
|
'nodeContentKey',
|
|
188
195
|
c('Property').t`content key`,
|
|
189
196
|
keySessionKeyResult.verified,
|
|
197
|
+
keySessionKeyResult.verificationErrors,
|
|
190
198
|
node.encryptedCrypto.signatureEmail,
|
|
191
199
|
));
|
|
192
200
|
} catch (error: unknown) {
|
|
@@ -228,6 +236,7 @@ export class NodesCryptoService {
|
|
|
228
236
|
name,
|
|
229
237
|
keyAuthor: finalKeyAuthor,
|
|
230
238
|
nameAuthor,
|
|
239
|
+
membership,
|
|
231
240
|
activeRevision,
|
|
232
241
|
folder,
|
|
233
242
|
errors: errors.length ? errors : undefined,
|
|
@@ -268,6 +277,7 @@ export class NodesCryptoService {
|
|
|
268
277
|
'nodeKey',
|
|
269
278
|
c('Property').t`key`,
|
|
270
279
|
key.verified,
|
|
280
|
+
key.verificationErrors,
|
|
271
281
|
node.encryptedCrypto.signatureEmail,
|
|
272
282
|
verificationKeys.length === 0,
|
|
273
283
|
),
|
|
@@ -285,7 +295,7 @@ export class NodesCryptoService {
|
|
|
285
295
|
const nameSignatureEmail = node.encryptedCrypto.nameSignatureEmail;
|
|
286
296
|
|
|
287
297
|
try {
|
|
288
|
-
const { name, verified } = await this.driveCrypto.decryptNodeName(
|
|
298
|
+
const { name, verified, verificationErrors } = await this.driveCrypto.decryptNodeName(
|
|
289
299
|
node.encryptedName,
|
|
290
300
|
parentKey,
|
|
291
301
|
verificationKeys,
|
|
@@ -298,6 +308,7 @@ export class NodesCryptoService {
|
|
|
298
308
|
'nodeName',
|
|
299
309
|
c('Property').t`name`,
|
|
300
310
|
verified,
|
|
311
|
+
verificationErrors,
|
|
301
312
|
nameSignatureEmail,
|
|
302
313
|
verificationKeys.length === 0,
|
|
303
314
|
),
|
|
@@ -319,6 +330,60 @@ export class NodesCryptoService {
|
|
|
319
330
|
return this.driveCrypto.decryptSessionKey(node.encryptedName, parentKey);
|
|
320
331
|
}
|
|
321
332
|
|
|
333
|
+
private async decryptMembership(node: EncryptedNode): Promise<Membership | undefined> {
|
|
334
|
+
if (!node.membership) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let sharedBy: Author;
|
|
339
|
+
if (node.encryptedCrypto.membership) {
|
|
340
|
+
let inviterEmailKeys: PublicKey[] | undefined;
|
|
341
|
+
try {
|
|
342
|
+
inviterEmailKeys = await this.account.getPublicKeys(node.encryptedCrypto.membership.inviterEmail);
|
|
343
|
+
} catch (error: unknown) {
|
|
344
|
+
this.logger.error('Failed to get inviter email keys', error);
|
|
345
|
+
sharedBy = resultError({
|
|
346
|
+
claimedAuthor: node.encryptedCrypto.membership.inviterEmail,
|
|
347
|
+
error: c('Error').t`Failed to get inviter keys`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const { verified, verificationErrors } = await this.driveCrypto.verifyInvitation(
|
|
353
|
+
node.encryptedCrypto.membership.base64MemberSharePassphraseKeyPacket,
|
|
354
|
+
node.encryptedCrypto.membership.armoredInviterSharePassphraseKeyPacketSignature,
|
|
355
|
+
inviterEmailKeys || [],
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
sharedBy = await this.handleClaimedAuthor(
|
|
359
|
+
node,
|
|
360
|
+
'membershipInviter',
|
|
361
|
+
c('Property').t`membership`,
|
|
362
|
+
verified,
|
|
363
|
+
verificationErrors,
|
|
364
|
+
node.encryptedCrypto.membership.inviterEmail,
|
|
365
|
+
);
|
|
366
|
+
} catch (error: unknown) {
|
|
367
|
+
void this.reportVerificationError(node, 'membershipInviter');
|
|
368
|
+
this.logger.error('Failed to verify invitation', error);
|
|
369
|
+
sharedBy = resultError({
|
|
370
|
+
claimedAuthor: node.encryptedCrypto.membership.inviterEmail,
|
|
371
|
+
error: c('Error').t`Failed to verify invitation`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
sharedBy = resultError({
|
|
376
|
+
error: c('Error').t`Missing inviter email`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
role: node.membership.role,
|
|
382
|
+
inviteTime: node.membership.inviteTime,
|
|
383
|
+
sharedBy,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
322
387
|
private async decryptHashKey(
|
|
323
388
|
node: EncryptedNode,
|
|
324
389
|
nodeKey: PrivateKey,
|
|
@@ -332,7 +397,7 @@ export class NodesCryptoService {
|
|
|
332
397
|
throw new Error('Node is not a folder');
|
|
333
398
|
}
|
|
334
399
|
|
|
335
|
-
const { hashKey, verified } = await this.driveCrypto.decryptNodeHashKey(
|
|
400
|
+
const { hashKey, verified, verificationErrors } = await this.driveCrypto.decryptNodeHashKey(
|
|
336
401
|
node.encryptedCrypto.folder.armoredHashKey,
|
|
337
402
|
nodeKey,
|
|
338
403
|
addressKeys,
|
|
@@ -345,6 +410,7 @@ export class NodesCryptoService {
|
|
|
345
410
|
'nodeHashKey',
|
|
346
411
|
c('Property').t`hash key`,
|
|
347
412
|
verified,
|
|
413
|
+
verificationErrors,
|
|
348
414
|
node.encryptedCrypto.signatureEmail,
|
|
349
415
|
),
|
|
350
416
|
};
|
|
@@ -394,7 +460,7 @@ export class NodesCryptoService {
|
|
|
394
460
|
};
|
|
395
461
|
}
|
|
396
462
|
|
|
397
|
-
const { extendedAttributes, verified } = await this.driveCrypto.decryptExtendedAttributes(
|
|
463
|
+
const { extendedAttributes, verified, verificationErrors } = await this.driveCrypto.decryptExtendedAttributes(
|
|
398
464
|
encryptedExtendedAttributes,
|
|
399
465
|
nodeKey,
|
|
400
466
|
addressKeys,
|
|
@@ -407,6 +473,7 @@ export class NodesCryptoService {
|
|
|
407
473
|
'nodeExtendedAttributes',
|
|
408
474
|
c('Property').t`attributes`,
|
|
409
475
|
verified,
|
|
476
|
+
verificationErrors,
|
|
410
477
|
signatureEmail,
|
|
411
478
|
),
|
|
412
479
|
};
|
|
@@ -418,7 +485,13 @@ export class NodesCryptoService {
|
|
|
418
485
|
name: string,
|
|
419
486
|
extendedAttributes?: string,
|
|
420
487
|
): Promise<{
|
|
421
|
-
encryptedCrypto:
|
|
488
|
+
encryptedCrypto: EncryptedNodeFolderCrypto & {
|
|
489
|
+
// signatureEmail and nameSignatureEmail are not optional.
|
|
490
|
+
signatureEmail: string;
|
|
491
|
+
nameSignatureEmail: string;
|
|
492
|
+
encryptedName: string;
|
|
493
|
+
hash: string;
|
|
494
|
+
};
|
|
422
495
|
keys: DecryptedNodeKeys;
|
|
423
496
|
}> {
|
|
424
497
|
const { email, addressKey } = address;
|
|
@@ -536,12 +609,19 @@ export class NodesCryptoService {
|
|
|
536
609
|
field: MetricVerificationErrorField,
|
|
537
610
|
signatureType: string,
|
|
538
611
|
verified: VERIFICATION_STATUS,
|
|
612
|
+
verificationErrors?: Error[],
|
|
539
613
|
claimedAuthor?: string,
|
|
540
614
|
notAvailableVerificationKeys = false,
|
|
541
615
|
): Promise<Author> {
|
|
542
|
-
const author = handleClaimedAuthor(
|
|
616
|
+
const author = handleClaimedAuthor(
|
|
617
|
+
signatureType,
|
|
618
|
+
verified,
|
|
619
|
+
verificationErrors,
|
|
620
|
+
claimedAuthor,
|
|
621
|
+
notAvailableVerificationKeys,
|
|
622
|
+
);
|
|
543
623
|
if (!author.ok) {
|
|
544
|
-
void this.reportVerificationError(node, field, claimedAuthor);
|
|
624
|
+
void this.reportVerificationError(node, field, verificationErrors, claimedAuthor);
|
|
545
625
|
}
|
|
546
626
|
return author;
|
|
547
627
|
}
|
|
@@ -549,6 +629,7 @@ export class NodesCryptoService {
|
|
|
549
629
|
private async reportVerificationError(
|
|
550
630
|
node: { uid: string; creationTime: Date },
|
|
551
631
|
field: MetricVerificationErrorField,
|
|
632
|
+
verificationErrors?: Error[],
|
|
552
633
|
claimedAuthor?: string,
|
|
553
634
|
) {
|
|
554
635
|
if (this.reportedVerificationErrors.has(node.uid)) {
|
|
@@ -571,12 +652,14 @@ export class NodesCryptoService {
|
|
|
571
652
|
`Failed to verify ${field} for node ${node.uid} (from before 2024: ${fromBefore2024}, matching address: ${addressMatchingDefaultShare})`,
|
|
572
653
|
);
|
|
573
654
|
|
|
574
|
-
this.telemetry.
|
|
655
|
+
this.telemetry.recordMetric({
|
|
575
656
|
eventName: 'verificationError',
|
|
576
657
|
volumeType,
|
|
577
658
|
field,
|
|
578
659
|
addressMatchingDefaultShare,
|
|
579
660
|
fromBefore2024,
|
|
661
|
+
error: verificationErrors?.map((e) => e.message).join(', '),
|
|
662
|
+
uid: node.uid,
|
|
580
663
|
});
|
|
581
664
|
this.reportedVerificationErrors.add(node.uid);
|
|
582
665
|
}
|
|
@@ -598,12 +681,13 @@ export class NodesCryptoService {
|
|
|
598
681
|
|
|
599
682
|
this.logger.error(`Failed to decrypt node ${node.uid} (from before 2024: ${fromBefore2024})`, error);
|
|
600
683
|
|
|
601
|
-
this.telemetry.
|
|
684
|
+
this.telemetry.recordMetric({
|
|
602
685
|
eventName: 'decryptionError',
|
|
603
686
|
volumeType,
|
|
604
687
|
field,
|
|
605
688
|
fromBefore2024,
|
|
606
689
|
error,
|
|
690
|
+
uid: node.uid,
|
|
607
691
|
});
|
|
608
692
|
this.reportedDecryptionErrors.add(node.uid);
|
|
609
693
|
}
|
|
@@ -615,6 +699,7 @@ export class NodesCryptoService {
|
|
|
615
699
|
function handleClaimedAuthor(
|
|
616
700
|
signatureType: string,
|
|
617
701
|
verified: VERIFICATION_STATUS,
|
|
702
|
+
verificationErrors?: Error[],
|
|
618
703
|
claimedAuthor?: string,
|
|
619
704
|
notAvailableVerificationKeys = false,
|
|
620
705
|
): Author {
|
|
@@ -628,6 +713,6 @@ function handleClaimedAuthor(
|
|
|
628
713
|
|
|
629
714
|
return resultError({
|
|
630
715
|
claimedAuthor: claimedAuthor,
|
|
631
|
-
error: getVerificationMessage(verified, signatureType, notAvailableVerificationKeys),
|
|
716
|
+
error: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
|
|
632
717
|
});
|
|
633
718
|
}
|