Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Setup Dialog #13456

Merged
merged 6 commits into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/src/androidTest/assets/credentials.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3wSNveXIhRsKl86pUnL7\n/AIAH+IJya5vqP0lv+yCBkd728szrLRYRWxPNC4VDbzyRHBr0RWj0ibsLJvU2OeF\n5p4er1tMIGgB0AEwiuDXBBz/RrxjPdhlilq7mvvqeUS2M3t5iroIxM6VEGQrhVrb\nb3U+7c6Lt7dIHAHEVOXnZiHYhhhduEmIzbsrAZFuMjlnWXTiMhuuWBf6t1nPyCHa\noA96loWibbvIsMegC73J3Ej5sgLkz/TjlrYmv6p3RGAEs74KHfggy4Fzw9TxBAAY\nyIX0NY8Rhb10XKrOSXrvRYuL/wkJ3P5XVK/NfsuLKbrhuUjDSgKplY9xCtOSaEPJ\nVQIDAQAB\n-----END PUBLIC KEY-----",
"certificate": "-----BEGIN CERTIFICATE-----\nMIIC9DCCAdygAwIBAgIBADANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhuYXJy\nYXRvcjAeFw0yNDA1MjcxMzEyNDVaFw00NDA1MjIxMzEyNDVaMBMxETAPBgNVBAMM\nCG5hcnJhdG9yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjA2EEYeN\nc3BdVDPkJK/AWPB1kd9sWAonZt/V4sbAE6fGy4qU21xfInZQaMHyhqdMXga10juE\nJLPKuyyRz+qijASryW+WzCJ3A9QeHHO+CiLc09yuB80JRpH0oHsol6WrdO1n5zuH\nlPtAdCwi4OeRmvazfBysbP2gaUl7DxackqbMei8a0MoyDxUB11hp0tpyYAU1/sXZ\nLGh4R4q4/F2KlSeYY9D62OJ8wNTgv9AYF/HRxXxWmVftB1En/DdvVr1zJGraHiRm\nQbaEnmsSGK8QHHm4h37cfD5f7rW1WO5A8KyJKwluOIXjMfL1YijAPpNW6EHhSlfT\n5RVLCHxvrzMHewIDAQABo1MwUTAdBgNVHQ4EFgQUzT6RHEHtpdjr8N3ABJK0wpFt\n1PMwHwYDVR0jBBgwFoAUzT6RHEHtpdjr8N3ABJK0wpFt1PMwDwYDVR0TAQH/BAUw\nAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAJ1q3CSBHrLauOZAD56BeElgh/ahbegsE\nZ4w7q4FdhkixLIwe6yrMmSvpNTuxRDHUrVLXxQmN0X3Yb7BLNXnnIUfH9EozaV7p\nYjOLWD2XCfLJmpGIBVvqZhyZrTl69jkBaVHF78aj1vt+qKihHUAVnG+qGH0PFms+\nG0KyY8bNYg+2HQiSTva1kgGPUA/8nQNj3lwi+r03tgqbw88fQKRPeMUJWdh/yV9U\noBdPHt+TBsUFZQZP3lBBS9lYhDT9fNoGX12WPAEUjYNhHVX+Qdup8Mg3aUMITXXJ\nvlGsN1SknlLoN0RwBFbyH9BCzqAdEIj5qQM3YDzIIyyy6AAnswNEUg==\n-----END CERTIFICATE-----"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils

import androidx.test.platform.app.InstrumentationRegistry
import com.google.gson.Gson
import com.owncloud.android.datamodel.Credentials
import com.owncloud.android.ui.dialog.setupEncryption.CertificateValidator
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.InputStreamReader

class CertificateValidatorTests {

private var sut: CertificateValidator? = null

@Before
fun setup() {
sut = CertificateValidator()
}

@After
fun destroy() {
sut = null
}

@Test
fun testValidateWhenGivenValidServerKeyAndCertificateShouldReturnTrue() {
val inputStream =
InstrumentationRegistry.getInstrumentation().context.assets.open("credentials.json")

val credentials = InputStreamReader(inputStream).use { reader ->
Gson().fromJson(reader, Credentials::class.java)
}

val isCertificateValid = sut?.validate(credentials.publicKey, credentials.certificate) ?: false
assert(isCertificateValid)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.owncloud.android.datamodel

data class Credentials(
val publicKey: String,
val certificate: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import com.nextcloud.test.TestActivity
import com.owncloud.android.AbstractIT
import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment
import com.owncloud.android.utils.ScreenshotTest
import org.junit.Rule
import org.junit.Test
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/com/nextcloud/client/di/AppModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import com.owncloud.android.ui.activities.data.files.FilesRepository;
import com.owncloud.android.ui.activities.data.files.FilesServiceApiImpl;
import com.owncloud.android.ui.activities.data.files.RemoteFilesRepository;
import com.owncloud.android.ui.dialog.setupEncryption.CertificateValidator;
import com.owncloud.android.utils.theme.ViewThemeUtils;

import org.greenrobot.eventbus.EventBus;
Expand Down Expand Up @@ -255,4 +256,10 @@ UsersAndGroupsSearchConfig userAndGroupSearchConfig() {
return new UsersAndGroupsSearchConfig();
}


@Provides
@Singleton
CertificateValidator certificateValidator() {
return new CertificateValidator();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
import com.owncloud.android.ui.dialog.RenamePublicShareDialogFragment;
import com.owncloud.android.ui.dialog.SendFilesDialog;
import com.owncloud.android.ui.dialog.SendShareDialog;
import com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment;
import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment;
import com.owncloud.android.ui.dialog.SharePasswordDialogFragment;
import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
import com.owncloud.android.ui.dialog.SslUntrustedCertDialog;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ object StringConstants {
const val DOT = "."
const val SPACE = " "
}

fun String.getContentOfPublicKey(): String {
return replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("\\s+".toRegex(), "")
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
import com.owncloud.android.ui.asynctasks.FetchRemoteFileTask;
import com.owncloud.android.ui.asynctasks.GetRemoteFileTask;
import com.owncloud.android.ui.dialog.SendShareDialog;
import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment;
import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment;
import com.owncloud.android.ui.events.SearchEvent;
Expand Down Expand Up @@ -157,6 +158,7 @@
import kotlin.Unit;

import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
import static com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG;
import static com.owncloud.android.utils.PermissionUtil.PERMISSION_CHOICE_DIALOG_TAG;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
import com.owncloud.android.providers.DocumentsStorageProvider;
import com.owncloud.android.ui.ThemeableSwitchPreference;
import com.owncloud.android.ui.asynctasks.LoadingVersionNumberTask;
import com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment;
import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment;
import com.owncloud.android.ui.helpers.FileOperationsHelper;
import com.owncloud.android.utils.ClipboardUtil;
import com.owncloud.android.utils.DeviceCredentialUtils;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
import com.nextcloud.client.account.User
import com.nextcloud.utils.extensions.getParcelableArgument
import com.owncloud.android.R
import com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment
import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment

class SetupEncryptionActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.owncloud.android.ui.dialog.setupEncryption

import android.util.Base64
import com.nextcloud.utils.extensions.getContentOfPublicKey
import com.owncloud.android.lib.common.utils.Log_OC
import java.io.ByteArrayInputStream
import java.security.KeyFactory
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.X509EncodedKeySpec
import javax.inject.Inject

@Suppress("EmptyClassBlock")
class CertificateValidator @Inject constructor() {
private val tag = "CertificateValidator"

/**
* Validates certificate with given public key
*
* @param serverPublicKeyString Public key with header
* @param certificate Certificate in PEM format
*/
@Suppress("TooGenericExceptionCaught")
fun validate(serverPublicKeyString: String, certificate: String): Boolean {
val contentOfServerKey = serverPublicKeyString.getContentOfPublicKey()

return try {
val decodedPublicKey = Base64.decode(contentOfServerKey, Base64.NO_WRAP)

val keySpec = X509EncodedKeySpec(decodedPublicKey)
val keyFactory = KeyFactory.getInstance("RSA")
val serverPublicKey = keyFactory.generatePublic(keySpec)

val certificateFactory = CertificateFactory.getInstance("X.509")
val certificateInputStream = ByteArrayInputStream(certificate.toByteArray())
val x509Certificate = certificateFactory.generateCertificate(certificateInputStream) as X509Certificate

// Check date of the certificate
x509Certificate.checkValidity()

// Verify certificate with serverPublicKey
x509Certificate.verify(serverPublicKey)
Log_OC.d(tag, "Client certificate is valid against server public key")
true
} catch (e: Exception) {
Log_OC.d(tag, "Client certificate is not valid against the server public key")
false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2024 Alper Ozturk <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.ui.dialog
package com.owncloud.android.ui.dialog.setupEncryption

import android.accounts.AccountManager
import android.annotation.SuppressLint
Expand All @@ -22,6 +22,7 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.client.account.User
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.network.ClientFactory
import com.nextcloud.utils.extensions.getParcelableArgument
import com.owncloud.android.R
import com.owncloud.android.databinding.SetupEncryptionDialogBinding
Expand All @@ -33,6 +34,7 @@ import com.owncloud.android.lib.resources.e2ee.CsrHelper
import com.owncloud.android.lib.resources.users.DeletePublicKeyRemoteOperation
import com.owncloud.android.lib.resources.users.GetPrivateKeyRemoteOperation
import com.owncloud.android.lib.resources.users.GetPublicKeyRemoteOperation
import com.owncloud.android.lib.resources.users.GetServerPublicKeyRemoteOperation
import com.owncloud.android.lib.resources.users.SendCSRRemoteOperation
import com.owncloud.android.lib.resources.users.StorePrivateKeyRemoteOperation
import com.owncloud.android.utils.EncryptionUtils
Expand All @@ -50,6 +52,14 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils

@JvmField
@Inject
var clientFactory: ClientFactory? = null

@JvmField
@Inject
var certificateValidator: CertificateValidator? = null

private var user: User? = null
private var arbitraryDataProvider: ArbitraryDataProvider? = null
private var positiveButton: MaterialButton? = null
Expand Down Expand Up @@ -270,34 +280,50 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
// if available
// - store public key
// - decrypt private key, store unencrypted private key in database

val context = mWeakContext.get() ?: return null
val publicKeyOperation = GetPublicKeyRemoteOperation()
val certificateOperation = GetPublicKeyRemoteOperation()
val serverPublicKeyOperation = GetServerPublicKeyRemoteOperation()
val user = user ?: return null

val publicKeyResult = publicKeyOperation.executeNextcloudClient(user, context)
val privateKeyOperation = GetPrivateKeyRemoteOperation()
val privateKeyResult = privateKeyOperation.executeNextcloudClient(user, context)
val certificateResult = certificateOperation.executeNextcloudClient(user, context)
val serverPublicKeyResult = serverPublicKeyOperation.executeNextcloudClient(user, context)

if (!publicKeyResult.isSuccess) {
var encryptedPrivateKey: com.owncloud.android.lib.ocs.responses.PrivateKey? = null
if (privateKeyResult.isSuccess) {
encryptedPrivateKey = privateKeyResult.resultData
}

if (!certificateResult.isSuccess || !serverPublicKeyResult.isSuccess) {
Log_OC.d(TAG, "certificate or server public key not fetched")
return null
}

Log_OC.d(TAG, "public key successful downloaded for " + user.accountName)
val serverKey = serverPublicKeyResult.resultData
val certificateAsString = certificateResult.resultData
val isCertificateValid = certificateValidator?.validate(serverKey, certificateAsString)

if (isCertificateValid == false) {
Log_OC.d(TAG, "Could not save certificate, certificate is not valid")
return null
}

if (arbitraryDataProvider == null) {
return null
}

val publicKeyFromServer = publicKeyResult.resultData
arbitraryDataProvider?.storeOrUpdateKeyValue(
user.accountName,
EncryptionUtils.PUBLIC_KEY,
publicKeyFromServer
certificateAsString
)

val privateKeyResult = GetPrivateKeyRemoteOperation().executeNextcloudClient(user, context)
if (privateKeyResult.isSuccess) {
Log_OC.d(TAG, "private key successful downloaded for " + user.accountName)
keyResult = KEY_EXISTING_USED
return privateKeyResult.resultData.getKey()
return encryptedPrivateKey?.getKey()
}

return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
import com.owncloud.android.ui.dialog.RenameFileDialogFragment;
import com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment;
import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment;
import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment;
import com.owncloud.android.ui.events.ChangeMenuEvent;
import com.owncloud.android.ui.events.CommentsEvent;
Expand Down Expand Up @@ -147,8 +147,8 @@
import androidx.recyclerview.widget.RecyclerView;

import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
import static com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG;
import static com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment.SETUP_ENCRYPTION_REQUEST_CODE;
import static com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG;
import static com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment.SETUP_ENCRYPTION_REQUEST_CODE;
import static com.owncloud.android.ui.fragment.SearchType.FAVORITE_SEARCH;
import static com.owncloud.android.ui.fragment.SearchType.FILE_SEARCH;
import static com.owncloud.android.ui.fragment.SearchType.NO_SEARCH;
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/
buildscript {
ext {
androidLibraryVersion ="86b0279d70"
androidLibraryVersion ="a4d86ef9d1"
androidPluginVersion = '8.5.2'
androidxMediaVersion = '1.4.0'
androidxTestVersion = "1.6.1"
Expand Down
8 changes: 8 additions & 0 deletions gradle/verification-metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6085,6 +6085,14 @@
<sha256 value="6907f3626be02ec7508a98ea912529780445a0336dc4fc665c96af007c5d6c49" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.nextcloud" name="android-library" version="a4d86ef9d1">
<artifact name="android-library-a4d86ef9d1.aar">
<sha256 value="c6c70775d49d935e7691f43fee0ff51c2c0df03c041fd75da92cf1f0df1385a7" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="android-library-a4d86ef9d1.module">
<sha256 value="47c4737be2cd41173f4e44a12f02e527f0014152103629364d86318c6198d484" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.nextcloud" name="android-library" version="acc7df66e4a43ed7f450136c13753f2743fb245e">
<artifact name="android-library-acc7df66e4a43ed7f450136c13753f2743fb245e.aar">
<sha256 value="f30c2d22c923c6ff8c605eb4cf713cfaf49eb967611f70dc3d139725b0891483" origin="Generated by Gradle" reason="Artifact is not signed"/>
Expand Down
Loading