Skip to content

Commit

Permalink
Encrypt device info on auth flows (#2948)
Browse files Browse the repository at this point in the history
<!--
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)
  • Loading branch information
cmonfortep authored Mar 10, 2023
1 parent 43a7e05 commit eb5b05e
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.sync.impl

import android.util.Base64

internal fun String.encodeB64(): String {
return Base64.encodeToString(this.toByteArray(), Base64.DEFAULT)
}

internal fun String.decodeB64(): String {
return String(Base64.decode(this, Base64.DEFAULT))
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ package com.duckduckgo.sync.impl

import android.annotation.SuppressLint
import android.os.Build
import com.duckduckgo.app.global.device.DeviceInfo
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.sync.impl.Type.DESKTOP
import com.duckduckgo.sync.impl.Type.MOBILE
import com.duckduckgo.sync.impl.Type.UNKNOWN
import com.duckduckgo.sync.store.SyncStore
import com.squareup.anvil.annotations.ContributesBinding
import java.util.*
Expand All @@ -28,13 +32,15 @@ interface SyncDeviceIds {
fun userId(): String
fun deviceName(): String
fun deviceId(): String
fun deviceType(): DeviceType
}

@ContributesBinding(AppScope::class)
class AppSyncDeviceIds
@Inject
constructor(
private val syncStore: SyncStore,
private val deviceInfo: DeviceInfo,
) : SyncDeviceIds {
override fun userId(): String {
var userId = syncStore.userId
Expand All @@ -61,4 +67,29 @@ constructor(
deviceId = UUID.randomUUID().toString()
return deviceId
}

override fun deviceType(): DeviceType {
return DeviceType(deviceInfo.formFactor().description)
}
}

private val mobileRegex = Regex("(phone|tablet)", RegexOption.IGNORE_CASE)
private val desktopRegex = Regex("(desktop)", RegexOption.IGNORE_CASE)

data class DeviceType(val deviceFactor: String = "") {
fun type(): Type {
return when {
deviceFactor.contains(mobileRegex) -> MOBILE
deviceFactor.contains(desktopRegex) -> DESKTOP
deviceFactor.isEmpty() -> UNKNOWN
else -> UNKNOWN
}
}
}

enum class Type {
MOBILE,
UNKNOWN,
DESKTOP,
;
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,23 @@ class AppSyncRepository @Inject constructor(

override fun createAccount(): Result<Boolean> {
val userId = syncDeviceIds.userId()
val deviceId = syncDeviceIds.deviceId()
val deviceName = syncDeviceIds.deviceName()

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

val deviceId = syncDeviceIds.deviceId()
val deviceName = syncDeviceIds.deviceName()
val deviceType = syncDeviceIds.deviceType()
val encryptedDeviceName = nativeLib.encryptData(deviceName, account.primaryKey).encryptedData
val encryptedDeviceType = nativeLib.encryptData(deviceType.deviceFactor, account.primaryKey).encryptedData

val result = syncApi.createAccount(
account.userId,
account.passwordHash,
account.protectedSecretKey,
deviceId,
deviceName,
encryptedDeviceName,
encryptedDeviceType,
)

return when (result) {
Expand Down Expand Up @@ -110,7 +115,9 @@ class AppSyncRepository @Inject constructor(
}

override fun login(recoveryCodeRawJson: String): Result<Boolean> {
val recoveryCode = Adapters.recoveryCodeAdapter.fromJson(recoveryCodeRawJson)?.recovery ?: return Result.Error(reason = "Failed reading json")
val recoveryCode = Adapters.recoveryCodeAdapter.fromJson(recoveryCodeRawJson.decodeB64())?.recovery ?: return Result.Error(
reason = "Failed reading json",
)
val primaryKey = recoveryCode.primaryKey
val userId = recoveryCode.userId
val deviceId = syncDeviceIds.deviceId()
Expand Down Expand Up @@ -145,7 +152,7 @@ class AppSyncRepository @Inject constructor(
override fun getRecoveryCode(): String? {
val primaryKey = syncStore.primaryKey ?: return null
val userID = syncStore.userId ?: return null
return Adapters.recoveryCodeAdapter.toJson(LinkCode(RecoveryCode(primaryKey, userID)))
return Adapters.recoveryCodeAdapter.toJson(LinkCode(RecoveryCode(primaryKey, userID))).encodeB64()
}

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

return Result.Success(linkingQRCode)
return Result.Success(linkingQRCode.encodeB64())
}

override fun connectDevice(contents: String): Result<Boolean> {
val connectKeys = Adapters.recoveryCodeAdapter.fromJson(contents)?.connect ?: return Result.Error(reason = "Error reading json")
val connectKeys = Adapters.recoveryCodeAdapter.fromJson(contents.decodeB64())?.connect ?: return Result.Error(reason = "Error reading json")
if (!isSignedIn()) {
val result = createAccount()
if (result is Error) return result
Expand Down Expand Up @@ -191,44 +198,7 @@ class AppSyncRepository @Inject constructor(
val recoveryCode = Adapters.recoveryCodeAdapter.fromJson(sealOpen)?.recovery ?: return Result.Error(reason = "Error reading json")
syncStore.userId = recoveryCode.userId
syncStore.primaryKey = recoveryCode.primaryKey
return login(recoveryCode.userId, recoveryCode.primaryKey)
}
}
}

private fun login(
userId: String,
primaryKey: String,
): Result<Boolean> {
val deviceId = syncDeviceIds.deviceId()
val deviceName = syncDeviceIds.deviceName()

val preLogin: LoginKeys = nativeLib.prepareForLogin(primaryKey)
Timber.i("SYNC prelogin ${preLogin.passwordHash} and ${preLogin.stretchedPrimaryKey}")
if (preLogin.result != 0L) return Result.Error(code = preLogin.result.toInt(), reason = "Account keys failed")

val result =
syncApi.login(
userID = userId,
hashedPassword = preLogin.passwordHash,
deviceId = deviceId,
deviceName = deviceName,
)

return when (result) {
is Result.Error -> {
result
}
is Result.Success -> {
val decryptResult = nativeLib.decrypt(result.data.protected_encryption_key, preLogin.stretchedPrimaryKey)
if (decryptResult.result != 0L) return Result.Error(code = decryptResult.result.toInt(), reason = "Decrypt failed")
syncStore.userId = userId
syncStore.deviceId = deviceId
syncStore.deviceName = deviceName
syncStore.token = result.data.token
syncStore.primaryKey = preLogin.primaryKey
syncStore.secretKey = decryptResult.decryptedData
Result.Success(true)
return performLogin(recoveryCode.userId, deviceId, syncDeviceIds.deviceName(), recoveryCode.primaryKey)
}
}
}
Expand Down Expand Up @@ -288,6 +258,8 @@ class AppSyncRepository @Inject constructor(
override fun getConnectedDevices(): Result<List<ConnectedDevice>> {
val token = syncStore.token.takeUnless { it.isNullOrEmpty() }
?: return Result.Error(reason = "Token Empty")
val primaryKey = syncStore.primaryKey.takeUnless { it.isNullOrEmpty() }
?: return Result.Error(reason = "PrimaryKey not found")

return when (val result = syncApi.getDevices(token)) {
is Result.Error -> {
Expand All @@ -300,8 +272,11 @@ class AppSyncRepository @Inject constructor(
result.data.map {
ConnectedDevice(
thisDevice = syncStore.deviceId == it.deviceId,
deviceName = it.deviceName,
deviceName = nativeLib.decryptData(it.deviceName, primaryKey).decryptedData,
deviceId = it.deviceId,
deviceType = it.deviceType.takeUnless { it.isNullOrEmpty() }?.let { encryptedDeviceType ->
DeviceType(nativeLib.decryptData(encryptedDeviceType, primaryKey).decryptedData)
} ?: DeviceType(),
)
},
)
Expand All @@ -320,11 +295,15 @@ class AppSyncRepository @Inject constructor(
val preLogin: LoginKeys = nativeLib.prepareForLogin(primaryKey)
if (preLogin.result != 0L) return Result.Error(code = preLogin.result.toInt(), reason = "Login account keys failed")

val deviceType = syncDeviceIds.deviceType()
val encryptedDeviceType = nativeLib.encryptData(deviceType.deviceFactor, preLogin.primaryKey).encryptedData

val result = syncApi.login(
userID = userId,
hashedPassword = preLogin.passwordHash,
deviceId = deviceId,
deviceName = deviceName,
deviceName = nativeLib.encryptData(deviceName, preLogin.primaryKey).encryptedData,
deviceType = encryptedDeviceType,
)

return when (result) {
Expand Down Expand Up @@ -377,6 +356,7 @@ data class ConnectedDevice(
val thisDevice: Boolean = false,
val deviceName: String,
val deviceId: String,
val deviceType: DeviceType,
)

data class ConnectCode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ data class Login(
@field:Json(name = "hashed_password") val hashedPassword: String,
@field:Json(name = "device_id") val deviceId: String,
@field:Json(name = "device_name") val deviceName: String,
@field:Json(name = "device_type") val deviceType: String,
)

data class Signup(
Expand All @@ -80,6 +81,7 @@ data class Signup(
@field:Json(name = "protected_encryption_key") val protectedEncryptionKey: String,
@field:Json(name = "device_id") val deviceId: String,
@field:Json(name = "device_name") val deviceName: String,
@field:Json(name = "device_type") val deviceType: String,
)

data class Logout(
Expand Down Expand Up @@ -117,6 +119,7 @@ data class DeviceEntries(
data class Device(
@field:Json(name = "device_id") val deviceId: String,
@field:Json(name = "device_name") val deviceName: String,
@field:Json(name = "device_type") val deviceType: String?,
@field:Json(name = "jw_iat") val jwIat: String,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ interface SyncApi {
protectedEncryptionKey: String,
deviceId: String,
deviceName: String,
deviceType: String,
): Result<AccountCreatedResponse>

fun login(
userID: String,
hashedPassword: String,
deviceId: String,
deviceName: String,
deviceType: String,
): Result<LoginResponse>

fun logout(
Expand Down Expand Up @@ -68,6 +70,7 @@ class SyncServiceRemote @Inject constructor(private val syncService: SyncService
protectedEncryptionKey: String,
deviceId: String,
deviceName: String,
deviceType: String,
): Result<AccountCreatedResponse> {
val response = runCatching {
val call = syncService.signup(
Expand All @@ -77,6 +80,7 @@ class SyncServiceRemote @Inject constructor(private val syncService: SyncService
protectedEncryptionKey = protectedEncryptionKey,
deviceId = deviceId,
deviceName = deviceName,
deviceType = deviceType,
),
)
call.execute()
Expand Down Expand Up @@ -176,6 +180,7 @@ class SyncServiceRemote @Inject constructor(private val syncService: SyncService
hashedPassword: String,
deviceId: String,
deviceName: String,
deviceType: String,
): Result<LoginResponse> {
val response = runCatching {
val call = syncService.login(
Expand All @@ -184,6 +189,7 @@ class SyncServiceRemote @Inject constructor(private val syncService: SyncService
hashedPassword = hashedPassword,
deviceId = deviceId,
deviceName = deviceName,
deviceType = deviceType,
),
)
call.execute()
Expand Down
Loading

0 comments on commit eb5b05e

Please sign in to comment.