Browse Source

Merge pull request #13456 from nextcloud/refactor/setup-dialog

Refactor Setup Dialog
Andy Scherzinger 9 months ago
parent
commit
af880a4c3e

+ 4 - 0
app/src/androidTest/assets/credentials.json

@@ -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-----"
+}

+ 45 - 0
app/src/androidTest/java/com/nextcloud/utils/CertificateValidatorTests.kt

@@ -0,0 +1,45 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * 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)
+    }
+}

+ 13 - 0
app/src/androidTest/java/com/owncloud/android/datamodel/Credentials.kt

@@ -0,0 +1,13 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.datamodel
+
+data class Credentials(
+    val publicKey: String,
+    val certificate: String
+)

+ 1 - 0
app/src/androidTest/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragmentIT.kt

@@ -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

+ 7 - 0
app/src/main/java/com/nextcloud/client/di/AppModule.java

@@ -55,6 +55,7 @@ import com.owncloud.android.ui.activities.data.activities.RemoteActivitiesReposi
 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;
@@ -255,4 +256,10 @@ class AppModule {
         return new UsersAndGroupsSearchConfig();
     }
 
+
+    @Provides
+    @Singleton
+    CertificateValidator certificateValidator() {
+        return new CertificateValidator();
+    }
 }

+ 1 - 1
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -86,7 +86,7 @@ import com.owncloud.android.ui.dialog.RenameFileDialogFragment;
 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;

+ 6 - 0
app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt

@@ -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(), "")
+}

+ 2 - 0
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -98,6 +98,7 @@ import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask;
 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;
@@ -157,6 +158,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 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;
 
 /**

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java

@@ -64,7 +64,7 @@ import com.owncloud.android.lib.common.utils.Log_OC;
 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;

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/activity/SetupEncryptionActivity.kt

@@ -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?) {

+ 57 - 0
app/src/main/java/com/owncloud/android/ui/dialog/setupEncryption/CertificateValidator.kt

@@ -0,0 +1,57 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * 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
+        }
+    }
+}

+ 35 - 9
app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt → app/src/main/java/com/owncloud/android/ui/dialog/setupEncryption/SetupEncryptionDialogFragment.kt

@@ -4,7 +4,7 @@
  * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
  * 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
@@ -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
@@ -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
@@ -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
@@ -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

+ 3 - 3
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -93,7 +93,7 @@ import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
 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;
@@ -147,8 +147,8 @@ import androidx.recyclerview.widget.LinearLayoutManager;
 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;

+ 1 - 1
build.gradle

@@ -10,7 +10,7 @@
  */
 buildscript {
     ext {
-        androidLibraryVersion ="86b0279d70"
+        androidLibraryVersion ="a4d86ef9d1"
         androidPluginVersion = '8.5.2'
         androidxMediaVersion = '1.4.0'
         androidxTestVersion = "1.6.1"

+ 8 - 0
gradle/verification-metadata.xml

@@ -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"/>