Skip to content

Commit eb5b05e

Browse files
authored
Encrypt device info on auth flows (#2948)
<!-- Note: This checklist is a reminder of our shared engineering expectations. The items in Bold are required If your PR involves UI changes: 1. Upload screenshots or screencasts that illustrate the changes before / after 2. Add them under the UI changes section (feel free to add more columns if needed) 3. Make sure these changes are tested in API 23 and API 26 If your PR does not involve UI changes, you can remove the **UI changes** section --> Task/Issue URL: https://app.asana.com/0/1201493110486074/1203835825731590/f ### Description Encrypts device info (name and type) on all auth flows. Also changes QR format to follow b64 encoding ### Steps to test this PR (you will need 2 devices to test some of the auth flows) _create account and login_ - [x] install app on device A and device B - [x] device A(unauthenticated): go to settings -> sync -> create account - [x] device A(authenticated): show QR code - [x] device B(unauthenticated): go to settings -> sync - [x] device B: read QR code - [x] device B: scan QR code from device A - [x] device B(authenticated): ensure login success _connect_ (device B should have a camera) - [x] install app on device A and device B (both need to be unauthenticated, go to settings -> sync -> reset button) - [x] device A(unauthenticated): go to settings -> sync - [x] device A: click on "connect (show QR)" (it will start polling data) - [x] device B(unauthenticated, with camera!): go to settings -> sync - [x] device B: click on "connect (read QR)" - [x] device B: scan QR code from device A - [x] device B(authenticated): ensure an account is created (account data will appear at the top) - [x] device A: ensure it joins the same account (can take 7 seconds) (account data will appear at the top)
1 parent 43a7e05 commit eb5b05e

File tree

10 files changed

+228
-79
lines changed

10 files changed

+228
-79
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2023 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.sync.impl
18+
19+
import android.util.Base64
20+
21+
internal fun String.encodeB64(): String {
22+
return Base64.encodeToString(this.toByteArray(), Base64.DEFAULT)
23+
}
24+
25+
internal fun String.decodeB64(): String {
26+
return String(Base64.decode(this, Base64.DEFAULT))
27+
}

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncDeviceId.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ package com.duckduckgo.sync.impl
1818

1919
import android.annotation.SuppressLint
2020
import android.os.Build
21+
import com.duckduckgo.app.global.device.DeviceInfo
2122
import com.duckduckgo.di.scopes.AppScope
23+
import com.duckduckgo.sync.impl.Type.DESKTOP
24+
import com.duckduckgo.sync.impl.Type.MOBILE
25+
import com.duckduckgo.sync.impl.Type.UNKNOWN
2226
import com.duckduckgo.sync.store.SyncStore
2327
import com.squareup.anvil.annotations.ContributesBinding
2428
import java.util.*
@@ -28,13 +32,15 @@ interface SyncDeviceIds {
2832
fun userId(): String
2933
fun deviceName(): String
3034
fun deviceId(): String
35+
fun deviceType(): DeviceType
3136
}
3237

3338
@ContributesBinding(AppScope::class)
3439
class AppSyncDeviceIds
3540
@Inject
3641
constructor(
3742
private val syncStore: SyncStore,
43+
private val deviceInfo: DeviceInfo,
3844
) : SyncDeviceIds {
3945
override fun userId(): String {
4046
var userId = syncStore.userId
@@ -61,4 +67,29 @@ constructor(
6167
deviceId = UUID.randomUUID().toString()
6268
return deviceId
6369
}
70+
71+
override fun deviceType(): DeviceType {
72+
return DeviceType(deviceInfo.formFactor().description)
73+
}
74+
}
75+
76+
private val mobileRegex = Regex("(phone|tablet)", RegexOption.IGNORE_CASE)
77+
private val desktopRegex = Regex("(desktop)", RegexOption.IGNORE_CASE)
78+
79+
data class DeviceType(val deviceFactor: String = "") {
80+
fun type(): Type {
81+
return when {
82+
deviceFactor.contains(mobileRegex) -> MOBILE
83+
deviceFactor.contains(desktopRegex) -> DESKTOP
84+
deviceFactor.isEmpty() -> UNKNOWN
85+
else -> UNKNOWN
86+
}
87+
}
88+
}
89+
90+
enum class Type {
91+
MOBILE,
92+
UNKNOWN,
93+
DESKTOP,
94+
;
6495
}

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncRepository.kt

Lines changed: 27 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,23 @@ class AppSyncRepository @Inject constructor(
6363

6464
override fun createAccount(): Result<Boolean> {
6565
val userId = syncDeviceIds.userId()
66-
val deviceId = syncDeviceIds.deviceId()
67-
val deviceName = syncDeviceIds.deviceName()
6866

6967
val account: AccountKeys = nativeLib.generateAccountKeys(userId = userId)
7068
if (account.result != 0L) return Result.Error(code = account.result.toInt(), reason = "Account keys failed")
7169

70+
val deviceId = syncDeviceIds.deviceId()
71+
val deviceName = syncDeviceIds.deviceName()
72+
val deviceType = syncDeviceIds.deviceType()
73+
val encryptedDeviceName = nativeLib.encryptData(deviceName, account.primaryKey).encryptedData
74+
val encryptedDeviceType = nativeLib.encryptData(deviceType.deviceFactor, account.primaryKey).encryptedData
75+
7276
val result = syncApi.createAccount(
7377
account.userId,
7478
account.passwordHash,
7579
account.protectedSecretKey,
7680
deviceId,
77-
deviceName,
81+
encryptedDeviceName,
82+
encryptedDeviceType,
7883
)
7984

8085
return when (result) {
@@ -110,7 +115,9 @@ class AppSyncRepository @Inject constructor(
110115
}
111116

112117
override fun login(recoveryCodeRawJson: String): Result<Boolean> {
113-
val recoveryCode = Adapters.recoveryCodeAdapter.fromJson(recoveryCodeRawJson)?.recovery ?: return Result.Error(reason = "Failed reading json")
118+
val recoveryCode = Adapters.recoveryCodeAdapter.fromJson(recoveryCodeRawJson.decodeB64())?.recovery ?: return Result.Error(
119+
reason = "Failed reading json",
120+
)
114121
val primaryKey = recoveryCode.primaryKey
115122
val userId = recoveryCode.userId
116123
val deviceId = syncDeviceIds.deviceId()
@@ -145,7 +152,7 @@ class AppSyncRepository @Inject constructor(
145152
override fun getRecoveryCode(): String? {
146153
val primaryKey = syncStore.primaryKey ?: return null
147154
val userID = syncStore.userId ?: return null
148-
return Adapters.recoveryCodeAdapter.toJson(LinkCode(RecoveryCode(primaryKey, userID)))
155+
return Adapters.recoveryCodeAdapter.toJson(LinkCode(RecoveryCode(primaryKey, userID))).encodeB64()
149156
}
150157

151158
override fun getConnectQR(): Result<String> {
@@ -159,11 +166,11 @@ class AppSyncRepository @Inject constructor(
159166
LinkCode(connect = ConnectCode(deviceId = deviceId, secretKey = prepareForConnect.publicKey)),
160167
) ?: return Error(reason = "Error generating Linking Code")
161168

162-
return Result.Success(linkingQRCode)
169+
return Result.Success(linkingQRCode.encodeB64())
163170
}
164171

165172
override fun connectDevice(contents: String): Result<Boolean> {
166-
val connectKeys = Adapters.recoveryCodeAdapter.fromJson(contents)?.connect ?: return Result.Error(reason = "Error reading json")
173+
val connectKeys = Adapters.recoveryCodeAdapter.fromJson(contents.decodeB64())?.connect ?: return Result.Error(reason = "Error reading json")
167174
if (!isSignedIn()) {
168175
val result = createAccount()
169176
if (result is Error) return result
@@ -191,44 +198,7 @@ class AppSyncRepository @Inject constructor(
191198
val recoveryCode = Adapters.recoveryCodeAdapter.fromJson(sealOpen)?.recovery ?: return Result.Error(reason = "Error reading json")
192199
syncStore.userId = recoveryCode.userId
193200
syncStore.primaryKey = recoveryCode.primaryKey
194-
return login(recoveryCode.userId, recoveryCode.primaryKey)
195-
}
196-
}
197-
}
198-
199-
private fun login(
200-
userId: String,
201-
primaryKey: String,
202-
): Result<Boolean> {
203-
val deviceId = syncDeviceIds.deviceId()
204-
val deviceName = syncDeviceIds.deviceName()
205-
206-
val preLogin: LoginKeys = nativeLib.prepareForLogin(primaryKey)
207-
Timber.i("SYNC prelogin ${preLogin.passwordHash} and ${preLogin.stretchedPrimaryKey}")
208-
if (preLogin.result != 0L) return Result.Error(code = preLogin.result.toInt(), reason = "Account keys failed")
209-
210-
val result =
211-
syncApi.login(
212-
userID = userId,
213-
hashedPassword = preLogin.passwordHash,
214-
deviceId = deviceId,
215-
deviceName = deviceName,
216-
)
217-
218-
return when (result) {
219-
is Result.Error -> {
220-
result
221-
}
222-
is Result.Success -> {
223-
val decryptResult = nativeLib.decrypt(result.data.protected_encryption_key, preLogin.stretchedPrimaryKey)
224-
if (decryptResult.result != 0L) return Result.Error(code = decryptResult.result.toInt(), reason = "Decrypt failed")
225-
syncStore.userId = userId
226-
syncStore.deviceId = deviceId
227-
syncStore.deviceName = deviceName
228-
syncStore.token = result.data.token
229-
syncStore.primaryKey = preLogin.primaryKey
230-
syncStore.secretKey = decryptResult.decryptedData
231-
Result.Success(true)
201+
return performLogin(recoveryCode.userId, deviceId, syncDeviceIds.deviceName(), recoveryCode.primaryKey)
232202
}
233203
}
234204
}
@@ -288,6 +258,8 @@ class AppSyncRepository @Inject constructor(
288258
override fun getConnectedDevices(): Result<List<ConnectedDevice>> {
289259
val token = syncStore.token.takeUnless { it.isNullOrEmpty() }
290260
?: return Result.Error(reason = "Token Empty")
261+
val primaryKey = syncStore.primaryKey.takeUnless { it.isNullOrEmpty() }
262+
?: return Result.Error(reason = "PrimaryKey not found")
291263

292264
return when (val result = syncApi.getDevices(token)) {
293265
is Result.Error -> {
@@ -300,8 +272,11 @@ class AppSyncRepository @Inject constructor(
300272
result.data.map {
301273
ConnectedDevice(
302274
thisDevice = syncStore.deviceId == it.deviceId,
303-
deviceName = it.deviceName,
275+
deviceName = nativeLib.decryptData(it.deviceName, primaryKey).decryptedData,
304276
deviceId = it.deviceId,
277+
deviceType = it.deviceType.takeUnless { it.isNullOrEmpty() }?.let { encryptedDeviceType ->
278+
DeviceType(nativeLib.decryptData(encryptedDeviceType, primaryKey).decryptedData)
279+
} ?: DeviceType(),
305280
)
306281
},
307282
)
@@ -320,11 +295,15 @@ class AppSyncRepository @Inject constructor(
320295
val preLogin: LoginKeys = nativeLib.prepareForLogin(primaryKey)
321296
if (preLogin.result != 0L) return Result.Error(code = preLogin.result.toInt(), reason = "Login account keys failed")
322297

298+
val deviceType = syncDeviceIds.deviceType()
299+
val encryptedDeviceType = nativeLib.encryptData(deviceType.deviceFactor, preLogin.primaryKey).encryptedData
300+
323301
val result = syncApi.login(
324302
userID = userId,
325303
hashedPassword = preLogin.passwordHash,
326304
deviceId = deviceId,
327-
deviceName = deviceName,
305+
deviceName = nativeLib.encryptData(deviceName, preLogin.primaryKey).encryptedData,
306+
deviceType = encryptedDeviceType,
328307
)
329308

330309
return when (result) {
@@ -377,6 +356,7 @@ data class ConnectedDevice(
377356
val thisDevice: Boolean = false,
378357
val deviceName: String,
379358
val deviceId: String,
359+
val deviceType: DeviceType,
380360
)
381361

382362
data class ConnectCode(

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncService.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ data class Login(
7272
@field:Json(name = "hashed_password") val hashedPassword: String,
7373
@field:Json(name = "device_id") val deviceId: String,
7474
@field:Json(name = "device_name") val deviceName: String,
75+
@field:Json(name = "device_type") val deviceType: String,
7576
)
7677

7778
data class Signup(
@@ -80,6 +81,7 @@ data class Signup(
8081
@field:Json(name = "protected_encryption_key") val protectedEncryptionKey: String,
8182
@field:Json(name = "device_id") val deviceId: String,
8283
@field:Json(name = "device_name") val deviceName: String,
84+
@field:Json(name = "device_type") val deviceType: String,
8385
)
8486

8587
data class Logout(
@@ -117,6 +119,7 @@ data class DeviceEntries(
117119
data class Device(
118120
@field:Json(name = "device_id") val deviceId: String,
119121
@field:Json(name = "device_name") val deviceName: String,
122+
@field:Json(name = "device_type") val deviceType: String?,
120123
@field:Json(name = "jw_iat") val jwIat: String,
121124
)
122125

sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncServiceRemote.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ interface SyncApi {
3131
protectedEncryptionKey: String,
3232
deviceId: String,
3333
deviceName: String,
34+
deviceType: String,
3435
): Result<AccountCreatedResponse>
3536

3637
fun login(
3738
userID: String,
3839
hashedPassword: String,
3940
deviceId: String,
4041
deviceName: String,
42+
deviceType: String,
4143
): Result<LoginResponse>
4244

4345
fun logout(
@@ -68,6 +70,7 @@ class SyncServiceRemote @Inject constructor(private val syncService: SyncService
6870
protectedEncryptionKey: String,
6971
deviceId: String,
7072
deviceName: String,
73+
deviceType: String,
7174
): Result<AccountCreatedResponse> {
7275
val response = runCatching {
7376
val call = syncService.signup(
@@ -77,6 +80,7 @@ class SyncServiceRemote @Inject constructor(private val syncService: SyncService
7780
protectedEncryptionKey = protectedEncryptionKey,
7881
deviceId = deviceId,
7982
deviceName = deviceName,
83+
deviceType = deviceType,
8084
),
8185
)
8286
call.execute()
@@ -176,6 +180,7 @@ class SyncServiceRemote @Inject constructor(private val syncService: SyncService
176180
hashedPassword: String,
177181
deviceId: String,
178182
deviceName: String,
183+
deviceType: String,
179184
): Result<LoginResponse> {
180185
val response = runCatching {
181186
val call = syncService.login(
@@ -184,6 +189,7 @@ class SyncServiceRemote @Inject constructor(private val syncService: SyncService
184189
hashedPassword = hashedPassword,
185190
deviceId = deviceId,
186191
deviceName = deviceName,
192+
deviceType = deviceType,
187193
),
188194
)
189195
call.execute()

0 commit comments

Comments
 (0)