@@ -33,6 +33,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.GENERIC_ERROR
33
33
import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE
34
34
import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED
35
35
import com.duckduckgo.sync.impl.Result.Error
36
+ import com.duckduckgo.sync.impl.Result.Success
36
37
import com.duckduckgo.sync.impl.pixels.*
37
38
import com.duckduckgo.sync.store.*
38
39
import com.squareup.anvil.annotations.*
@@ -54,10 +55,12 @@ interface SyncAccountRepository {
54
55
fun deleteAccount (): Result <Boolean >
55
56
fun latestToken (): String
56
57
fun getRecoveryCode (): Result <String >
58
+ fun getInvitationCode (): Result <String >
57
59
fun getThisConnectedDevice (): ConnectedDevice ?
58
60
fun getConnectedDevices (): Result <List <ConnectedDevice >>
59
61
fun getConnectQR (): Result <String >
60
62
fun pollConnectionKeys (): Result <Boolean >
63
+ fun pollSecondDeviceAck (): Result <Boolean >
61
64
fun renameDevice (device : ConnectedDevice ): Result <Boolean >
62
65
fun logoutAndJoinNewAccount (stringCode : String ): Result <Boolean >
63
66
}
@@ -77,6 +80,9 @@ class AppSyncAccountRepository @Inject constructor(
77
80
private val syncFeature : SyncFeature ,
78
81
) : SyncAccountRepository {
79
82
83
+ private var tempPrivateKey: String? = null
84
+ private var tempPublickKey: String? = null
85
+
80
86
private val connectedDevicesCached: MutableList <ConnectedDevice > = mutableListOf ()
81
87
82
88
override fun isSyncSupported (): Boolean {
@@ -169,6 +175,17 @@ class AppSyncAccountRepository @Inject constructor(
169
175
return Result .Success (Adapters .recoveryCodeAdapter.toJson(LinkCode (RecoveryCode (primaryKey, userID))).encodeB64())
170
176
}
171
177
178
+ override fun getInvitationCode (): Result <String > {
179
+ Timber .d(" InvitationFlow: Generating invitation code" )
180
+
181
+ val deviceID = syncStore.deviceId ? : return Error (reason = " Get Invitation Code: Not existing device ID" ).alsoFireAccountErrorPixel()
182
+ val prepareForConnect = nativeLib.prepareForConnect() // TODO: check if values returned are different or always the same
183
+ tempPrivateKey = prepareForConnect.secretKey
184
+ tempPublickKey = prepareForConnect.publicKey
185
+ val invitationCode = InvitationCode (deviceId = deviceID, publicKey = prepareForConnect.publicKey)
186
+ return Result .Success (Adapters .invitationCodeAdapter.toJson(invitationCode).encodeB64())
187
+ }
188
+
172
189
override fun getConnectQR (): Result <String > {
173
190
val prepareForConnect = kotlin.runCatching {
174
191
nativeLib.prepareForConnect().also {
@@ -234,6 +251,53 @@ class AppSyncAccountRepository @Inject constructor(
234
251
}
235
252
}
236
253
254
+ // TODO: review error codes
255
+ override fun pollSecondDeviceAck (): Result <Boolean > {
256
+ val deviceId = syncDeviceIds.deviceId()
257
+
258
+ val result = syncApi.invitationACK(deviceId)
259
+ return when (result) {
260
+ is Error -> {
261
+ if (result.code == NOT_FOUND .code) { // TODO: check with JP which errors we can receive here.
262
+ return Result .Success (false )
263
+ } else if (result.code == GONE .code) {
264
+ return Error (code = CONNECT_FAILED .code, reason = " Connect: keys expired" ).alsoFireAccountErrorPixel() // TODO: change according to JP errors
265
+ }
266
+ result.alsoFireAccountErrorPixel()
267
+ }
268
+
269
+ is Result .Success -> {
270
+ val sealOpen = kotlin.runCatching {
271
+ nativeLib.sealOpen(result.data, tempPrivateKey!! , tempPublickKey!! ) // TODO: handle those double bangs
272
+ }.getOrElse { throwable ->
273
+ throwable.asErrorResult().alsoFireAccountErrorPixel()
274
+ return Error (code = CONNECT_FAILED .code, reason = " Connect: Error opening seal" )
275
+ }
276
+ val publicKeyNewDevice = Adapters .ackCodeAdapter.fromJson(sealOpen)?.publicKey
277
+ ? : return Error (code = CONNECT_FAILED .code, reason = " Connect: Error reading received recovery code" ).alsoFireAccountErrorPixel()
278
+ val deviceId2 = Adapters .ackCodeAdapter.fromJson(sealOpen)?.deviceId
279
+ ? : return Error (code = CONNECT_FAILED .code, reason = " Connect: Error reading received recovery code" ).alsoFireAccountErrorPixel()
280
+
281
+ // we encrypt our secrets with publickeynewdevice
282
+ // we send them to the BE endpoint
283
+ return sendSecrets(deviceId2, publicKeyNewDevice).onFailure {
284
+ return it.copy(code = LOGIN_FAILED .code)
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ private fun sendSecrets (deviceId : String , publicKey : String ): Result <Boolean > {
291
+ val recoveryCode = getRecoveryCode()
292
+ when (recoveryCode) {
293
+ is Error -> TODO ()
294
+ is Success -> {
295
+ val encryptedSecrets = nativeLib.seal(recoveryCode.data, publicKey)
296
+ return syncApi.sendSecrets(deviceId, encryptedSecrets)
297
+ }
298
+ }
299
+ }
300
+
237
301
override fun logout (deviceId : String ): Result <Boolean > {
238
302
val token = syncStore.token.takeUnless { it.isNullOrEmpty() }
239
303
? : return Error (reason = " Logout: Token Empty" ).alsoFireLogoutErrorPixel()
@@ -527,6 +591,9 @@ class AppSyncAccountRepository @Inject constructor(
527
591
companion object {
528
592
private val moshi = Moshi .Builder ().build()
529
593
val recoveryCodeAdapter: JsonAdapter <LinkCode > = moshi.adapter(LinkCode ::class .java)
594
+
595
+ val invitationCodeAdapter: JsonAdapter <InvitationCode > = moshi.adapter(InvitationCode ::class .java)
596
+ val ackCodeAdapter: JsonAdapter <ACKCode > = moshi.adapter(ACKCode ::class .java)
530
597
}
531
598
}
532
599
}
@@ -563,6 +630,16 @@ data class LinkCode(
563
630
val connect : ConnectCode ? = null ,
564
631
)
565
632
633
+ data class InvitationCode (
634
+ val deviceId : String ,
635
+ val publicKey : String ,
636
+ )
637
+
638
+ data class ACKCode ( // TODO: if this is the same as Invitation, we can remove
639
+ val deviceId : String ,
640
+ val publicKey : String ,
641
+ )
642
+
566
643
data class RecoveryCode (
567
644
@field:Json(name = "primary_key") val primaryKey : String ,
568
645
@field:Json(name = "user_id") val userId : String ,
0 commit comments