@@ -29,6 +29,7 @@ import com.duckduckgo.sync.impl.API_CODE.NOT_FOUND
29
29
import com.duckduckgo.sync.impl.AccountErrorCodes.ALREADY_SIGNED_IN
30
30
import com.duckduckgo.sync.impl.AccountErrorCodes.CONNECT_FAILED
31
31
import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED
32
+ import com.duckduckgo.sync.impl.AccountErrorCodes.EXCHANGE_FAILED
32
33
import com.duckduckgo.sync.impl.AccountErrorCodes.GENERIC_ERROR
33
34
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
34
35
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
@@ -87,14 +88,14 @@ class AppSyncAccountRepository @Inject constructor(
87
88
) : SyncAccountRepository {
88
89
89
90
/* *
90
- * If there is a key-exchange invitation in progress, we need to keep a reference to them
91
- * There are separate invitation details for the inviter and the receiver
91
+ * If there is a key-exchange flow in progress, we need to keep a reference to them
92
+ * There are separate device details for the inviter and the receiver
92
93
*
93
94
* Inviter is reset every time a new exchange invitation code is created.
94
95
* Receiver is reset every time an exchange invitation is received.
95
96
*/
96
- private var pendingInvitationAsInviter : PendingInvitation ? = null
97
- private var pendingInvitationAsReceiver : PendingInvitation ? = null
97
+ private var exchangeDeviceDetailsAsInviter : DeviceDetailsForKeyExchange ? = null
98
+ private var exchangeDeviceDetailsAsReceiver : DeviceDetailsForKeyExchange ? = null
98
99
99
100
private val connectedDevicesCached: MutableList <ConnectedDevice > = mutableListOf ()
100
101
@@ -144,7 +145,7 @@ class AppSyncAccountRepository @Inject constructor(
144
145
return @let null
145
146
}
146
147
147
- return completeExchange (it)
148
+ return onInvitationCodeReceived (it)
148
149
}
149
150
150
151
Timber .e(" Sync: code is not supported" )
@@ -165,50 +166,39 @@ class AppSyncAccountRepository @Inject constructor(
165
166
}.getOrDefault(UNKNOWN )
166
167
}
167
168
168
- private fun completeExchange (invitationCode : InvitationCode ): Result <Boolean > {
169
+ private fun onInvitationCodeReceived (invitationCode : InvitationCode ): Result <Boolean > {
169
170
// Sync: InviteFlow - B (https://app.asana.com/0/72649045549333/1209571867429615)
170
171
Timber .d(" Sync-exchange: InviteFlow - B. code is an exchange code $invitationCode " )
171
172
172
- // generate new ID and and public/private key-pair for receiving device
173
- val thisDeviceKeyId = deviceKeyGenerator.generate()
174
- val thisDeviceKeyPair = nativeLib.prepareForConnect()
175
-
176
- pendingInvitationAsReceiver = PendingInvitation (
177
- keyId = thisDeviceKeyId,
178
- privateKey = thisDeviceKeyPair.secretKey,
179
- publicKey = thisDeviceKeyPair.publicKey,
180
- )
181
- val deviceName = syncDeviceIds.deviceName()
182
-
183
- Timber .i(
184
- " Sync: details for this (receiver) device:" +
185
- " \n\t key ID is $thisDeviceKeyId " +
186
- " \n\t public key is ${thisDeviceKeyPair.publicKey} " +
187
- " \n\t device name is $deviceName " ,
188
- )
173
+ // generate new ID and public/private key-pair for receiving device
174
+ val deviceDetailsAsReceiver = kotlin.runCatching {
175
+ generateReceiverDeviceDetails()
176
+ }.getOrElse {
177
+ return Error (code = EXCHANGE_FAILED .code, reason = " Error generating receiver key-pair" ).alsoFireAccountErrorPixel()
178
+ }
189
179
190
180
val invitedDeviceDetails = InvitedDeviceDetails (
191
- keyId = thisDeviceKeyId ,
192
- publicKey = thisDeviceKeyPair .publicKey,
193
- deviceName = deviceName,
181
+ keyId = deviceDetailsAsReceiver.keyId ,
182
+ publicKey = deviceDetailsAsReceiver .publicKey,
183
+ deviceName = syncDeviceIds. deviceName() ,
194
184
)
195
185
196
- kotlin.runCatching {
186
+ val encryptedPayload = kotlin.runCatching {
197
187
val payload = Adapters .invitedDeviceAdapter.toJson(invitedDeviceDetails)
198
- val encrypted = nativeLib.seal(payload, invitationCode.publicKey)
199
- return syncApi.sendEncryptedMessage(invitationCode.keyId, encrypted)
188
+ nativeLib.seal(payload, invitationCode.publicKey)
200
189
}.getOrElse { throwable ->
201
190
throwable.asErrorResult().alsoFireAccountErrorPixel()
202
- return Error (code = GENERIC_ERROR .code, reason = " Exchange: Error encrypting payload" )
191
+ return Error (code = EXCHANGE_FAILED .code, reason = " Exchange: Error encrypting payload" )
203
192
}
193
+ return syncApi.sendEncryptedMessage(invitationCode.keyId, encryptedPayload)
204
194
}
205
195
206
196
override fun pollForRecoveryCodeAndLogin (): Result <ExchangeResult > {
207
197
// Sync: InviteFlow - E (https://app.asana.com/0/72649045549333/1209571867429615)
208
198
Timber .d(" Sync-exchange: InviteFlow - E" )
209
199
210
- val pendingInvite = pendingInvitationAsReceiver
211
- ? : return Error (code = CONNECT_FAILED .code, reason = " Connect : No pending invite initialized" ).also {
200
+ val pendingInvite = exchangeDeviceDetailsAsReceiver
201
+ ? : return Error (code = EXCHANGE_FAILED .code, reason = " Exchange : No pending invite initialized" ).also {
212
202
Timber .w(" Sync-exchange: no pending invite initialized" )
213
203
}
214
204
@@ -221,13 +211,13 @@ class AppSyncAccountRepository @Inject constructor(
221
211
}
222
212
GONE .code -> {
223
213
Timber .w(" Sync-exchange: keys expired: ${result.reason} " )
224
- return Error (code = CONNECT_FAILED .code, reason = " Connect : keys expired" ).alsoFireAccountErrorPixel()
214
+ return Error (code = EXCHANGE_FAILED .code, reason = " Exchange : keys expired" ).alsoFireAccountErrorPixel()
225
215
}
226
216
else -> {
227
217
Timber .e(" Sync-exchange: error getting encrypted recovery code: ${result.reason} " )
218
+ result.alsoFireAccountErrorPixel()
228
219
}
229
220
}
230
- result.alsoFireAccountErrorPixel()
231
221
}
232
222
233
223
is Success -> {
@@ -236,12 +226,12 @@ class AppSyncAccountRepository @Inject constructor(
236
226
val decryptedJson = kotlin.runCatching {
237
227
nativeLib.sealOpen(result.data, pendingInvite.publicKey, pendingInvite.privateKey)
238
228
}.getOrNull()
239
- ? : return Error (code = CONNECT_FAILED .code, reason = " Connect: Error opening seal" ).alsoFireAccountErrorPixel()
229
+ ? : return Error (code = EXCHANGE_FAILED .code, reason = " Connect: Error opening seal" ).alsoFireAccountErrorPixel()
240
230
241
231
val recoveryData = kotlin.runCatching {
242
232
Adapters .recoveryCodeAdapter.fromJson(decryptedJson)?.recovery
243
233
}.getOrNull()
244
- ? : return Error (code = CONNECT_FAILED .code, reason = " Connect: Error reading recovery code" ).alsoFireAccountErrorPixel()
234
+ ? : return Error (code = EXCHANGE_FAILED .code, reason = " Connect: Error reading recovery code" ).alsoFireAccountErrorPixel()
245
235
246
236
return when (val loginResult = login(recoveryData)) {
247
237
is Success -> Success (LoggedIn )
@@ -320,20 +310,21 @@ class AppSyncAccountRepository @Inject constructor(
320
310
// Sync: InviteFlow - A (https://app.asana.com/0/72649045549333/1209571867429615)
321
311
Timber .d(" Sync-exchange: InviteFlow - A. Generating invitation code" )
322
312
323
- // generate new ID and and public/private key-pair
324
- generateInviterDeviceDetails()
325
-
326
- val pendingInvitation = pendingInvitationAsInviter
327
- ? : return Error (code = GENERIC_ERROR .code, reason = " Exchange: No pending invitation initialized" ).alsoFireAccountErrorPixel()
313
+ // generate new ID and and public/private key-pair for inviter device
314
+ val deviceDetailsAsInviter = kotlin.runCatching {
315
+ generateInviterDeviceDetails()
316
+ }.getOrElse {
317
+ return Error (code = EXCHANGE_FAILED .code, reason = " Error generating inviter key-pair" ).alsoFireAccountErrorPixel()
318
+ }
328
319
329
- val invitationCode = InvitationCode (keyId = pendingInvitation .keyId, publicKey = pendingInvitation .publicKey)
320
+ val invitationCode = InvitationCode (keyId = deviceDetailsAsInviter .keyId, publicKey = deviceDetailsAsInviter .publicKey)
330
321
val invitationWrapper = InvitationCodeWrapper (invitationCode)
331
322
332
323
return kotlin.runCatching {
333
324
val code = Adapters .invitationCodeAdapter.toJson(invitationWrapper).encodeB64()
334
325
Success (code)
335
326
}.getOrElse {
336
- Error (code = GENERIC_ERROR .code, reason = " Error generating invitation code" ).alsoFireAccountErrorPixel()
327
+ Error (code = EXCHANGE_FAILED .code, reason = " Error generating invitation code" ).alsoFireAccountErrorPixel()
337
328
}
338
329
}
339
330
@@ -405,15 +396,15 @@ class AppSyncAccountRepository @Inject constructor(
405
396
// Sync: InviteFlow - C (https://app.asana.com/0/72649045549333/1209571867429615)
406
397
Timber .d(" Sync-exchange: InviteFlow - C" )
407
398
408
- val keyId = pendingInvitationAsInviter ?.keyId ? : return Error (reason = " No pending invitation initialized" )
399
+ val keyId = exchangeDeviceDetailsAsInviter ?.keyId ? : return Error (reason = " No pending invitation initialized" )
409
400
410
401
return when (val result = syncApi.getEncryptedMessage(keyId)) {
411
402
is Error -> {
412
403
if (result.code == NOT_FOUND .code) {
413
404
return Success (false )
414
405
} else if (result.code == GONE .code) {
415
406
return Error (
416
- code = CONNECT_FAILED .code,
407
+ code = EXCHANGE_FAILED .code,
417
408
reason = " Connect: keys expired" ,
418
409
).alsoFireAccountErrorPixel()
419
410
}
@@ -424,20 +415,20 @@ class AppSyncAccountRepository @Inject constructor(
424
415
Timber .v(" Sync-exchange: Found invitation acceptance for keyId: $keyId } ${result.data} " )
425
416
426
417
val decrypted = kotlin.runCatching {
427
- val pending = pendingInvitationAsInviter
428
- ? : return Error (code = CONNECT_FAILED .code, reason = " Exchange: No pending invitation initialized" )
418
+ val pending = exchangeDeviceDetailsAsInviter
419
+ ? : return Error (code = EXCHANGE_FAILED .code, reason = " Exchange: No pending invitation initialized" )
429
420
.alsoFireAccountErrorPixel()
430
421
431
422
nativeLib.sealOpen(result.data, pending.publicKey, pending.privateKey)
432
423
}.getOrElse { throwable ->
433
424
throwable.asErrorResult().alsoFireAccountErrorPixel()
434
- return Error (code = CONNECT_FAILED .code, reason = " Connect: Error opening seal" )
425
+ return Error (code = EXCHANGE_FAILED .code, reason = " Connect: Error opening seal" )
435
426
}
436
427
437
428
Timber .v(" Sync-exchange: invitation acceptance received: $decrypted " )
438
429
439
430
val response = Adapters .invitedDeviceAdapter.fromJson(decrypted)
440
- ? : return Error (code = GENERIC_ERROR .code, reason = " Connect: Error reading invitation response" ).alsoFireAccountErrorPixel()
431
+ ? : return Error (code = EXCHANGE_FAILED .code, reason = " Connect: Error reading invitation response" ).alsoFireAccountErrorPixel()
441
432
442
433
val otherDevicePublicKey = response.publicKey
443
434
val otherDeviceKeyId = response.keyId
@@ -450,7 +441,7 @@ class AppSyncAccountRepository @Inject constructor(
450
441
// we encrypt our secrets with otherDevicePublicKey, and send them to the backend endpoint
451
442
return sendSecrets(otherDeviceKeyId, otherDevicePublicKey).onFailure {
452
443
Timber .w(" Sync-exchange: failed to send secrets. error code: ${it.code} ${it.reason} " )
453
- return it.copy(code = LOGIN_FAILED .code)
444
+ return it.copy(code = EXCHANGE_FAILED .code)
454
445
}
455
446
}
456
447
}
@@ -610,23 +601,38 @@ class AppSyncAccountRepository @Inject constructor(
610
601
}
611
602
}
612
603
613
- private fun generateInviterDeviceDetails () {
604
+ private fun generateInviterDeviceDetails (): DeviceDetailsForKeyExchange {
614
605
Timber .i(" Sync-exchange: Generating inviter device details" )
615
- // generate new ID and and public/private key-pair
616
606
val keyId = deviceKeyGenerator.generate()
617
607
val prepareForConnect = nativeLib.prepareForConnect()
618
608
619
- PendingInvitation (
609
+ return DeviceDetailsForKeyExchange (
620
610
keyId = keyId,
621
611
privateKey = prepareForConnect.secretKey,
622
612
publicKey = prepareForConnect.publicKey,
623
613
).also {
624
- pendingInvitationAsInviter = it
614
+ exchangeDeviceDetailsAsInviter = it
625
615
Timber .w(" Sync-exchange: this (inviter) device's key ID is $keyId " )
626
616
Timber .w(" Sync-exchange: this (inviter) device's public key is ${it.publicKey} " )
627
617
}
628
618
}
629
619
620
+ private fun generateReceiverDeviceDetails (): DeviceDetailsForKeyExchange {
621
+ Timber .i(" Sync-exchange: Generating receiver device details" )
622
+ val thisDeviceKeyId = deviceKeyGenerator.generate()
623
+ val thisDeviceKeyPair = nativeLib.prepareForConnect()
624
+
625
+ return DeviceDetailsForKeyExchange (
626
+ keyId = thisDeviceKeyId,
627
+ privateKey = thisDeviceKeyPair.secretKey,
628
+ publicKey = thisDeviceKeyPair.publicKey,
629
+ ).also {
630
+ exchangeDeviceDetailsAsReceiver = it
631
+ Timber .w(" Sync-exchange: this (receiver) device's key ID is ${it.keyId} " )
632
+ Timber .w(" Sync-exchange: this (receiver) device's public key is ${it.publicKey} " )
633
+ }
634
+ }
635
+
630
636
private fun performCreateAccount (): Result <Boolean > {
631
637
val userId = syncDeviceIds.userId()
632
638
val account: AccountKeys = kotlin.runCatching {
@@ -869,6 +875,7 @@ enum class AccountErrorCodes(val code: Int) {
869
875
CREATE_ACCOUNT_FAILED (53 ),
870
876
CONNECT_FAILED (54 ),
871
877
INVALID_CODE (55 ),
878
+ EXCHANGE_FAILED (56 ),
872
879
}
873
880
874
881
enum class CodeType {
@@ -917,7 +924,7 @@ inline fun <T> Result<T>.onFailure(action: (error: Error) -> Unit): Result<T> {
917
924
return this
918
925
}
919
926
920
- private data class PendingInvitation (
927
+ private data class DeviceDetailsForKeyExchange (
921
928
val keyId : String ,
922
929
val privateKey : String ,
923
930
var publicKey : String ,
0 commit comments