diff --git a/app/src/androidTest/java/com/owncloud/android/operations/DeleteE2ERemoteOperationIT.kt b/app/src/androidTest/java/com/owncloud/android/operations/DeleteE2ERemoteOperationIT.kt new file mode 100644 index 000000000000..88d072a60efa --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/operations/DeleteE2ERemoteOperationIT.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations + +import com.owncloud.android.AbstractOnServerIT +import com.owncloud.android.lib.resources.e2ee.DeleteEncryptedFilesRemoteOperation +import com.owncloud.android.lib.resources.users.DeletePrivateKeyRemoteOperation +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.StorePrivateKeyRemoteOperation +import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.crypto.CryptoHelper +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class DeleteE2ERemoteOperationIT : AbstractOnServerIT() { + + @Test + fun testDeleteEncryptedFiles() { + val sut = DeleteEncryptedFilesRemoteOperation() + val result = sut.execute(nextcloudClient) + assertTrue(result.isSuccess) + } + + @Test + fun testDeletePrivateKey() { + val keyPair = EncryptionUtils.generateKeyPair() + val privateKey = keyPair.private + val keyPhrase = "moreovertelevisionfactorytendencyindependenceinternationalintellectualimpress" + + "interestvolunteer" + val privatePemKeyString = EncryptionUtils.privateKeyToPEM(privateKey) + val encryptedPrivateKey = CryptoHelper.encryptPrivateKey( + privatePemKeyString, + keyPhrase + ) + + StorePrivateKeyRemoteOperation(encryptedPrivateKey).execute(nextcloudClient) + + val sut = DeletePrivateKeyRemoteOperation() + val result = sut.execute(nextcloudClient) + assertTrue(result.isSuccess) + + val getResult = GetPrivateKeyRemoteOperation().execute(nextcloudClient) + assertFalse(getResult.isSuccess) + } + + @Test + fun testDeletePublicKey() { + val sut = DeletePublicKeyRemoteOperation() + val result = sut.execute(nextcloudClient) + assertTrue(result.isSuccess) + + val getResult = GetPublicKeyRemoteOperation().execute(nextcloudClient) + assertFalse(getResult.isSuccess) + } +} diff --git a/app/src/main/java/com/owncloud/android/operations/e2e/E2EDeletionService.kt b/app/src/main/java/com/owncloud/android/operations/e2e/E2EDeletionService.kt new file mode 100644 index 000000000000..d3b9038bf3e2 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EDeletionService.kt @@ -0,0 +1,79 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.e2e + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.account.User +import com.nextcloud.client.network.ClientFactory +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.e2ee.DeleteEncryptedFilesRemoteOperation +import com.owncloud.android.lib.resources.users.DeletePrivateKeyRemoteOperation +import com.owncloud.android.lib.resources.users.DeletePublicKeyRemoteOperation + +@Suppress("MagicNumber") +class E2EDeletionService(private val clientFactory: ClientFactory) { + private val mainHandler = Handler(Looper.getMainLooper()) + + fun showRemoveE2EKeysAndFilesAlertDialog(context: Context, user: User, onResult: (Boolean) -> Unit) { + MaterialAlertDialogBuilder(context, R.style.FallbackTheming_Dialog) + .setTitle(R.string.prefs_remove_e2e_keys_and_files) + .setMessage(R.string.remove_e2e_keys_and_files_dialog_warning) + .setCancelable(true) + .setNegativeButton(R.string.common_cancel) { dialog, _ -> dialog.dismiss() } + .setPositiveButton(R.string.common_ok) { dialog, _ -> + deleteKeysAndFiles(user) { + dialog.dismiss() + onResult(it) + } + } + .show() + } + + private fun deleteKeysAndFiles(user: User, onResult: (Boolean) -> Unit) { + Thread { + val result = runCatching { + val client = clientFactory.createNextcloudClient(user) + var successfulOperationResultCount = 3 + + if (!DeletePrivateKeyRemoteOperation().execute(client).isSuccess) { + successfulOperationResultCount -= 1 + } + + Log_OC.i(TAG, "🔑" + "private key is deleted") + + if (!DeletePublicKeyRemoteOperation().execute(client).isSuccess) { + successfulOperationResultCount -= 1 + } + + Log_OC.i(TAG, "🗝" + "public key is deleted") + + if (!DeleteEncryptedFilesRemoteOperation().execute(client).isSuccess) { + successfulOperationResultCount -= 1 + } + + Log_OC.i(TAG, "🗂️" + "encrypted files are deleted") + + successfulOperationResultCount == 3 + }.getOrElse { e -> + Log.e(TAG, "Cannot delete E2E keys and files", e) + false + } + + mainHandler.post { onResult(result) } + }.start() + } + + companion object { + private val TAG = E2EDeletionService::class.java.simpleName + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java index 5018c9f45e89..d1093bcb5d93 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java @@ -54,6 +54,7 @@ import com.nextcloud.client.preferences.DarkMode; import com.nextcloud.utils.extensions.ContextExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; +import com.owncloud.android.BuildConfig; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.authentication.AuthenticatorActivity; @@ -63,6 +64,7 @@ import com.owncloud.android.lib.common.ExternalLink; import com.owncloud.android.lib.common.ExternalLinkType; import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.operations.e2e.E2EDeletionService; import com.owncloud.android.providers.DocumentsStorageProvider; import com.owncloud.android.ui.ThemeableSwitchPreference; import com.owncloud.android.ui.asynctasks.LoadingVersionNumberTask; @@ -78,7 +80,6 @@ import com.owncloud.android.utils.theme.CapabilityUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; -import java.util.List; import java.util.Objects; import javax.inject.Inject; @@ -91,7 +92,6 @@ import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import kotlin.Unit; -import kotlin.jvm.functions.Function1; import static com.owncloud.android.ui.activity.DrawerActivity.REQ_ALL_FILES_ACCESS; @@ -138,6 +138,8 @@ public class SettingsActivity extends PreferenceActivity private String storagePath; private String pendingLock; + private E2EDeletionService e2EDeletionService; + private User user; @Inject ArbitraryDataProvider arbitraryDataProvider; @Inject AppPreferences preferences; @@ -164,6 +166,7 @@ public void onCreate(Bundle savedInstanceState) { PreferenceScreen preferenceScreen = (PreferenceScreen) findPreference("preference_screen"); user = accountManager.getUser(); + e2EDeletionService = new E2EDeletionService(clientFactory); // retrieve user's base uri setupBaseUri(); @@ -368,6 +371,8 @@ private void setupMoreCategory() { removeE2E(preferenceCategoryMore); + removeE2EFilesAndKeys(preferenceCategoryMore); + setupHelpPreference(preferenceCategoryMore); setupRecommendPreference(preferenceCategoryMore); @@ -537,6 +542,46 @@ private void removeE2E(PreferenceCategory preferenceCategoryMore) { } } + private void removeE2EFilesAndKeys(PreferenceCategory preferenceCategoryMore) { + if (BuildConfig.DEBUG) { + Preference removeKeysAndFilesPreference = findPreference("remove_e2e_files_and_keys"); + if (removeKeysAndFilesPreference != null) { + if (!FileOperationsHelper.isEndToEndEncryptionSetup(this, user)) { + preferenceCategoryMore.removePreference(removeKeysAndFilesPreference); + } else { + removeKeysAndFilesPreference.setOnPreferenceClickListener(p -> { + showRemoveE2EKeysAndFilesAlertDialog(preferenceCategoryMore, removeKeysAndFilesPreference); + return true; + }); + } + } + } + } + + private void showRemoveE2EKeysAndFilesAlertDialog(PreferenceCategory preferenceCategoryMore, Preference preference) { + if (e2EDeletionService == null) { + return; + } + + e2EDeletionService.showRemoveE2EKeysAndFilesAlertDialog(this, user, success -> { + if (success) { + EncryptionUtils.removeE2E(arbitraryDataProvider, user); + preferenceCategoryMore.removePreference(preference); + + Preference pMnemonic = findPreference("mnemonic"); + if (pMnemonic != null) { + preferenceCategoryMore.removePreference(pMnemonic); + } + + Preference pRemoveE2E = findPreference("remove_e2e"); + if (pRemoveE2E != null) { + preferenceCategoryMore.removePreference(pRemoveE2E); + } + } + return Unit.INSTANCE; + }); + } + private void showRemoveE2EAlertDialog(PreferenceCategory preferenceCategoryMore, Preference preference) { new MaterialAlertDialogBuilder(this, R.style.FallbackTheming_Dialog) .setTitle(R.string.prefs_e2e_mnemonic) diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java index 078bc598b357..9e7b1bd2de90 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java @@ -17,6 +17,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.nextcloud.client.account.User; +import com.nextcloud.common.SessionTimeOutKt; import com.nextcloud.utils.e2ee.E2EVersionHelper; import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; @@ -42,7 +43,6 @@ import com.owncloud.android.lib.resources.e2ee.StoreMetadataRemoteOperation; import com.owncloud.android.lib.resources.e2ee.StoreMetadataV2RemoteOperation; import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation; -import com.owncloud.android.lib.resources.e2ee.UnlockFileV1RemoteOperation; import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation; import com.owncloud.android.lib.resources.e2ee.UpdateMetadataV2RemoteOperation; import com.owncloud.android.lib.resources.files.model.ServerFileInterface; @@ -1166,7 +1166,8 @@ public static String lockFolder(ServerFileInterface parentFile, OwnCloudClient c public static String lockFolder(ServerFileInterface parentFile, OwnCloudClient client, long counter) throws UploadException { // Lock folder LockFileRemoteOperation lockFileOperation = new LockFileRemoteOperation(parentFile.getLocalId(), - counter); + counter, + SessionTimeOutKt.getDefaultSessionTimeOut()); RemoteOperationResult lockFileOperationResult = lockFileOperation.execute(client); if (lockFileOperationResult.isSuccess() && @@ -1366,7 +1367,7 @@ public static RemoteOperationResult unlockFolder(ServerFileInterface paren public static RemoteOperationResult unlockFolderV1(ServerFileInterface parentFolder, OwnCloudClient client, String token) { if (token != null) { - return new UnlockFileV1RemoteOperation(parentFolder.getLocalId(), token).execute(client); + return new UnlockFileRemoteOperation(parentFolder.getLocalId(), token, SessionTimeOutKt.getDefaultSessionTimeOut(), false).execute(client); } else { return new RemoteOperationResult<>(new Exception("No token available")); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f763f4f7973d..ffc70f139ff0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1383,7 +1383,9 @@ An internet connection is required to set up the encrypted folder Set up end-to-end encryption End-to-end encryption is set up! + Remove encrypted files and keys Remove encryption locally + This operation will remove all encrypted files, private and public keys. Are you sure? You can remove end-to-end encryption locally on this client Remove local encryption You can remove end-to-end encryption locally on this client. The encrypted files will remain on server, but will not be synced to this computer any longer. diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 74655f678fa1..fb7f89111f0f 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -113,6 +113,9 @@ android:title="@string/prefs_remove_e2e" android:key="remove_e2e" android:summary="@string/remove_e2e" /> + + + + + + + + + @@ -21550,6 +21558,14 @@ + + + + + + + +