@protontech/drive-sdk 0.1.2 → 0.2.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.
Files changed (153) hide show
  1. package/dist/crypto/driveCrypto.d.ts +11 -0
  2. package/dist/crypto/driveCrypto.js +20 -7
  3. package/dist/crypto/driveCrypto.js.map +1 -1
  4. package/dist/crypto/interface.d.ts +10 -1
  5. package/dist/crypto/openPGPCrypto.d.ts +18 -2
  6. package/dist/crypto/openPGPCrypto.js +25 -6
  7. package/dist/crypto/openPGPCrypto.js.map +1 -1
  8. package/dist/diagnostic/telemetry.d.ts +1 -1
  9. package/dist/diagnostic/telemetry.js +1 -1
  10. package/dist/diagnostic/telemetry.js.map +1 -1
  11. package/dist/interface/download.d.ts +46 -0
  12. package/dist/interface/index.d.ts +2 -2
  13. package/dist/interface/index.js.map +1 -1
  14. package/dist/interface/nodes.d.ts +26 -1
  15. package/dist/interface/nodes.js.map +1 -1
  16. package/dist/interface/telemetry.d.ts +5 -2
  17. package/dist/interface/telemetry.js.map +1 -1
  18. package/dist/internal/apiService/apiService.js +1 -1
  19. package/dist/internal/apiService/apiService.js.map +1 -1
  20. package/dist/internal/apiService/driveTypes.d.ts +78 -165
  21. package/dist/internal/apiService/index.d.ts +1 -1
  22. package/dist/internal/apiService/index.js +2 -2
  23. package/dist/internal/apiService/index.js.map +1 -1
  24. package/dist/internal/apiService/transformers.d.ts +1 -1
  25. package/dist/internal/apiService/transformers.js +2 -2
  26. package/dist/internal/apiService/transformers.js.map +1 -1
  27. package/dist/internal/download/blockIndex.d.ts +11 -0
  28. package/dist/internal/download/blockIndex.js +35 -0
  29. package/dist/internal/download/blockIndex.js.map +1 -0
  30. package/dist/internal/download/blockIndex.test.d.ts +1 -0
  31. package/dist/internal/download/blockIndex.test.js +147 -0
  32. package/dist/internal/download/blockIndex.test.js.map +1 -0
  33. package/dist/internal/download/fileDownloader.d.ts +6 -2
  34. package/dist/internal/download/fileDownloader.js +83 -6
  35. package/dist/internal/download/fileDownloader.js.map +1 -1
  36. package/dist/internal/download/fileDownloader.test.js +69 -4
  37. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  38. package/dist/internal/download/interface.d.ts +4 -4
  39. package/dist/internal/download/seekableStream.d.ts +80 -0
  40. package/dist/internal/download/seekableStream.js +163 -0
  41. package/dist/internal/download/seekableStream.js.map +1 -0
  42. package/dist/internal/download/seekableStream.test.d.ts +1 -0
  43. package/dist/internal/download/seekableStream.test.js +149 -0
  44. package/dist/internal/download/seekableStream.test.js.map +1 -0
  45. package/dist/internal/download/telemetry.js +1 -1
  46. package/dist/internal/download/telemetry.js.map +1 -1
  47. package/dist/internal/download/telemetry.test.js +7 -7
  48. package/dist/internal/download/telemetry.test.js.map +1 -1
  49. package/dist/internal/errors.d.ts +1 -1
  50. package/dist/internal/errors.js +7 -1
  51. package/dist/internal/errors.js.map +1 -1
  52. package/dist/internal/errors.test.js +44 -10
  53. package/dist/internal/errors.test.js.map +1 -1
  54. package/dist/internal/events/index.js +1 -1
  55. package/dist/internal/events/index.js.map +1 -1
  56. package/dist/internal/nodes/apiService.js +16 -3
  57. package/dist/internal/nodes/apiService.js.map +1 -1
  58. package/dist/internal/nodes/apiService.test.js +43 -7
  59. package/dist/internal/nodes/apiService.test.js.map +1 -1
  60. package/dist/internal/nodes/cache.js +9 -2
  61. package/dist/internal/nodes/cache.js.map +1 -1
  62. package/dist/internal/nodes/cache.test.js +6 -1
  63. package/dist/internal/nodes/cache.test.js.map +1 -1
  64. package/dist/internal/nodes/cryptoService.d.ts +4 -1
  65. package/dist/internal/nodes/cryptoService.js +66 -16
  66. package/dist/internal/nodes/cryptoService.js.map +1 -1
  67. package/dist/internal/nodes/cryptoService.test.js +129 -46
  68. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  69. package/dist/internal/nodes/extendedAttributes.d.ts +2 -1
  70. package/dist/internal/nodes/extendedAttributes.js +27 -1
  71. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  72. package/dist/internal/nodes/extendedAttributes.test.js +59 -6
  73. package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
  74. package/dist/internal/nodes/index.test.js +1 -1
  75. package/dist/internal/nodes/index.test.js.map +1 -1
  76. package/dist/internal/nodes/interface.d.ts +18 -2
  77. package/dist/internal/nodes/nodesAccess.js +11 -1
  78. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  79. package/dist/internal/nodes/nodesManagement.js +1 -1
  80. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  81. package/dist/internal/nodes/nodesRevisions.d.ts +4 -3
  82. package/dist/internal/nodes/nodesRevisions.js +2 -2
  83. package/dist/internal/nodes/nodesRevisions.js.map +1 -1
  84. package/dist/internal/shares/cryptoService.js +7 -4
  85. package/dist/internal/shares/cryptoService.js.map +1 -1
  86. package/dist/internal/shares/cryptoService.test.js +5 -3
  87. package/dist/internal/shares/cryptoService.test.js.map +1 -1
  88. package/dist/internal/sharing/apiService.js +5 -5
  89. package/dist/internal/sharing/apiService.js.map +1 -1
  90. package/dist/internal/sharing/cryptoService.js +8 -5
  91. package/dist/internal/sharing/cryptoService.js.map +1 -1
  92. package/dist/internal/sharing/cryptoService.test.js +7 -4
  93. package/dist/internal/sharing/cryptoService.test.js.map +1 -1
  94. package/dist/internal/upload/telemetry.js +2 -2
  95. package/dist/internal/upload/telemetry.js.map +1 -1
  96. package/dist/internal/upload/telemetry.test.js +7 -7
  97. package/dist/internal/upload/telemetry.test.js.map +1 -1
  98. package/dist/telemetry.d.ts +2 -2
  99. package/dist/telemetry.js +2 -2
  100. package/dist/telemetry.js.map +1 -1
  101. package/dist/tests/telemetry.js +1 -1
  102. package/dist/tests/telemetry.js.map +1 -1
  103. package/dist/transformers.d.ts +1 -1
  104. package/dist/transformers.js +2 -1
  105. package/dist/transformers.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/crypto/driveCrypto.ts +70 -25
  108. package/src/crypto/interface.ts +15 -0
  109. package/src/crypto/openPGPCrypto.ts +37 -5
  110. package/src/diagnostic/telemetry.ts +1 -1
  111. package/src/interface/download.ts +46 -0
  112. package/src/interface/index.ts +2 -1
  113. package/src/interface/nodes.ts +28 -1
  114. package/src/interface/telemetry.ts +6 -1
  115. package/src/internal/apiService/apiService.ts +1 -1
  116. package/src/internal/apiService/driveTypes.ts +78 -165
  117. package/src/internal/apiService/index.ts +1 -1
  118. package/src/internal/apiService/transformers.ts +1 -1
  119. package/src/internal/download/blockIndex.test.ts +158 -0
  120. package/src/internal/download/blockIndex.ts +36 -0
  121. package/src/internal/download/fileDownloader.test.ts +100 -7
  122. package/src/internal/download/fileDownloader.ts +109 -9
  123. package/src/internal/download/interface.ts +4 -4
  124. package/src/internal/download/seekableStream.test.ts +187 -0
  125. package/src/internal/download/seekableStream.ts +182 -0
  126. package/src/internal/download/telemetry.test.ts +7 -7
  127. package/src/internal/download/telemetry.ts +1 -1
  128. package/src/internal/errors.test.ts +45 -11
  129. package/src/internal/errors.ts +8 -0
  130. package/src/internal/events/index.ts +1 -1
  131. package/src/internal/nodes/apiService.test.ts +59 -15
  132. package/src/internal/nodes/apiService.ts +21 -4
  133. package/src/internal/nodes/cache.test.ts +6 -1
  134. package/src/internal/nodes/cache.ts +9 -2
  135. package/src/internal/nodes/cryptoService.test.ts +139 -47
  136. package/src/internal/nodes/cryptoService.ts +94 -9
  137. package/src/internal/nodes/extendedAttributes.test.ts +60 -7
  138. package/src/internal/nodes/extendedAttributes.ts +37 -1
  139. package/src/internal/nodes/index.test.ts +1 -1
  140. package/src/internal/nodes/interface.ts +19 -2
  141. package/src/internal/nodes/nodesAccess.ts +15 -1
  142. package/src/internal/nodes/nodesManagement.ts +1 -1
  143. package/src/internal/nodes/nodesRevisions.ts +14 -5
  144. package/src/internal/shares/cryptoService.test.ts +5 -3
  145. package/src/internal/shares/cryptoService.ts +7 -4
  146. package/src/internal/sharing/apiService.ts +6 -6
  147. package/src/internal/sharing/cryptoService.test.ts +7 -4
  148. package/src/internal/sharing/cryptoService.ts +8 -5
  149. package/src/internal/upload/telemetry.test.ts +7 -7
  150. package/src/internal/upload/telemetry.ts +2 -2
  151. package/src/telemetry.ts +2 -2
  152. package/src/tests/telemetry.ts +1 -1
  153. 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 No need to implement all methods for mocking
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.logEvent).toHaveBeenCalledTimes(1);
79
- expect(telemetry.logEvent).toHaveBeenCalledWith({
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.logEvent).toHaveBeenCalledTimes(1);
90
- expect(telemetry.logEvent).toHaveBeenCalledWith({
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
- const encryptedNode = {
100
- uid: 'volumeId~nodeId',
101
- parentUid: 'volumeId~parentId',
102
- encryptedCrypto: {
103
- signatureEmail: 'signatureEmail',
104
- nameSignatureEmail: 'nameSignatureEmail',
105
- armoredKey: 'armoredKey',
106
- armoredNodePassphrase: 'armoredNodePassphrase',
107
- armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
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
- } as EncryptedNode;
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
- const encryptedNode = {
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(1);
184
+ expect(account.getPublicKeys).toHaveBeenCalledTimes(2); // signatureEmail (for both key and name) and inviterEmail
169
185
  expect(account.getPublicKeys).toHaveBeenCalledWith('signatureEmail');
170
- expect(telemetry.logEvent).not.toHaveBeenCalled();
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(2);
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(telemetry.logEvent).not.toHaveBeenCalled();
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: { claimedAuthor: 'nameSignatureEmail', error: 'Signature verification for name failed' },
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: { claimedAuthor: 'signatureEmail', error: 'Signature verification for hash key failed' },
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.logEvent).not.toHaveBeenCalled();
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.logEvent).not.toHaveBeenCalled();
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: { claimedAuthor: 'nameSignatureEmail', error: 'Signature verification for name failed' },
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.logEvent).not.toHaveBeenCalled();
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.logEvent).not.toHaveBeenCalled();
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: Required<EncryptedNodeFolderCrypto> & { encryptedName: string; hash: string };
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(signatureType, verified, claimedAuthor, notAvailableVerificationKeys);
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.logEvent({
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.logEvent({
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
  }