Skip to content

Commit ca3983e

Browse files
Merge pull request #16926 from nextcloud/feat/delete-e2ee-files-and-credentials
feat(e2ee): add debug deletion
2 parents e018b80 + 541fa5e commit ca3983e

8 files changed

Lines changed: 223 additions & 6 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.owncloud.android.operations
9+
10+
import com.owncloud.android.AbstractOnServerIT
11+
import com.owncloud.android.lib.resources.e2ee.DeleteEncryptedFilesRemoteOperation
12+
import com.owncloud.android.lib.resources.users.DeletePrivateKeyRemoteOperation
13+
import com.owncloud.android.lib.resources.users.DeletePublicKeyRemoteOperation
14+
import com.owncloud.android.lib.resources.users.GetPrivateKeyRemoteOperation
15+
import com.owncloud.android.lib.resources.users.GetPublicKeyRemoteOperation
16+
import com.owncloud.android.lib.resources.users.StorePrivateKeyRemoteOperation
17+
import com.owncloud.android.utils.EncryptionUtils
18+
import com.owncloud.android.utils.crypto.CryptoHelper
19+
import org.junit.Assert.assertFalse
20+
import org.junit.Assert.assertTrue
21+
import org.junit.Test
22+
23+
class DeleteE2ERemoteOperationIT : AbstractOnServerIT() {
24+
25+
@Test
26+
fun testDeleteEncryptedFiles() {
27+
val sut = DeleteEncryptedFilesRemoteOperation()
28+
val result = sut.execute(nextcloudClient)
29+
assertTrue(result.isSuccess)
30+
}
31+
32+
@Test
33+
fun testDeletePrivateKey() {
34+
val keyPair = EncryptionUtils.generateKeyPair()
35+
val privateKey = keyPair.private
36+
val keyPhrase = "moreovertelevisionfactorytendencyindependenceinternationalintellectualimpress" +
37+
"interestvolunteer"
38+
val privatePemKeyString = EncryptionUtils.privateKeyToPEM(privateKey)
39+
val encryptedPrivateKey = CryptoHelper.encryptPrivateKey(
40+
privatePemKeyString,
41+
keyPhrase
42+
)
43+
44+
StorePrivateKeyRemoteOperation(encryptedPrivateKey).execute(nextcloudClient)
45+
46+
val sut = DeletePrivateKeyRemoteOperation()
47+
val result = sut.execute(nextcloudClient)
48+
assertTrue(result.isSuccess)
49+
50+
val getResult = GetPrivateKeyRemoteOperation().execute(nextcloudClient)
51+
assertFalse(getResult.isSuccess)
52+
}
53+
54+
@Test
55+
fun testDeletePublicKey() {
56+
val sut = DeletePublicKeyRemoteOperation()
57+
val result = sut.execute(nextcloudClient)
58+
assertTrue(result.isSuccess)
59+
60+
val getResult = GetPublicKeyRemoteOperation().execute(nextcloudClient)
61+
assertFalse(getResult.isSuccess)
62+
}
63+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.owncloud.android.operations.e2e
9+
10+
import android.content.Context
11+
import android.os.Handler
12+
import android.os.Looper
13+
import android.util.Log
14+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
15+
import com.nextcloud.client.account.User
16+
import com.nextcloud.client.network.ClientFactory
17+
import com.owncloud.android.R
18+
import com.owncloud.android.lib.common.utils.Log_OC
19+
import com.owncloud.android.lib.resources.e2ee.DeleteEncryptedFilesRemoteOperation
20+
import com.owncloud.android.lib.resources.users.DeletePrivateKeyRemoteOperation
21+
import com.owncloud.android.lib.resources.users.DeletePublicKeyRemoteOperation
22+
23+
@Suppress("MagicNumber")
24+
class E2EDeletionService(private val clientFactory: ClientFactory) {
25+
private val mainHandler = Handler(Looper.getMainLooper())
26+
27+
fun showRemoveE2EKeysAndFilesAlertDialog(context: Context, user: User, onResult: (Boolean) -> Unit) {
28+
MaterialAlertDialogBuilder(context, R.style.FallbackTheming_Dialog)
29+
.setTitle(R.string.prefs_remove_e2e_keys_and_files)
30+
.setMessage(R.string.remove_e2e_keys_and_files_dialog_warning)
31+
.setCancelable(true)
32+
.setNegativeButton(R.string.common_cancel) { dialog, _ -> dialog.dismiss() }
33+
.setPositiveButton(R.string.common_ok) { dialog, _ ->
34+
deleteKeysAndFiles(user) {
35+
dialog.dismiss()
36+
onResult(it)
37+
}
38+
}
39+
.show()
40+
}
41+
42+
private fun deleteKeysAndFiles(user: User, onResult: (Boolean) -> Unit) {
43+
Thread {
44+
val result = runCatching {
45+
val client = clientFactory.createNextcloudClient(user)
46+
var successfulOperationResultCount = 3
47+
48+
if (!DeletePrivateKeyRemoteOperation().execute(client).isSuccess) {
49+
successfulOperationResultCount -= 1
50+
}
51+
52+
Log_OC.i(TAG, "🔑" + "private key is deleted")
53+
54+
if (!DeletePublicKeyRemoteOperation().execute(client).isSuccess) {
55+
successfulOperationResultCount -= 1
56+
}
57+
58+
Log_OC.i(TAG, "🗝" + "public key is deleted")
59+
60+
if (!DeleteEncryptedFilesRemoteOperation().execute(client).isSuccess) {
61+
successfulOperationResultCount -= 1
62+
}
63+
64+
Log_OC.i(TAG, "🗂️" + "encrypted files are deleted")
65+
66+
successfulOperationResultCount == 3
67+
}.getOrElse { e ->
68+
Log.e(TAG, "Cannot delete E2E keys and files", e)
69+
false
70+
}
71+
72+
mainHandler.post { onResult(result) }
73+
}.start()
74+
}
75+
76+
companion object {
77+
private val TAG = E2EDeletionService::class.java.simpleName
78+
}
79+
}

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

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import com.nextcloud.client.preferences.DarkMode;
5555
import com.nextcloud.utils.extensions.ContextExtensionsKt;
5656
import com.nextcloud.utils.mdm.MDMConfig;
57+
import com.owncloud.android.BuildConfig;
5758
import com.owncloud.android.MainApp;
5859
import com.owncloud.android.R;
5960
import com.owncloud.android.authentication.AuthenticatorActivity;
@@ -63,6 +64,7 @@
6364
import com.owncloud.android.lib.common.ExternalLink;
6465
import com.owncloud.android.lib.common.ExternalLinkType;
6566
import com.owncloud.android.lib.common.utils.Log_OC;
67+
import com.owncloud.android.operations.e2e.E2EDeletionService;
6668
import com.owncloud.android.providers.DocumentsStorageProvider;
6769
import com.owncloud.android.ui.ThemeableSwitchPreference;
6870
import com.owncloud.android.ui.asynctasks.LoadingVersionNumberTask;
@@ -78,7 +80,6 @@
7880
import com.owncloud.android.utils.theme.CapabilityUtils;
7981
import com.owncloud.android.utils.theme.ViewThemeUtils;
8082

81-
import java.util.List;
8283
import java.util.Objects;
8384

8485
import javax.inject.Inject;
@@ -91,7 +92,6 @@
9192
import androidx.core.content.ContextCompat;
9293
import androidx.core.content.res.ResourcesCompat;
9394
import kotlin.Unit;
94-
import kotlin.jvm.functions.Function1;
9595

9696
import static com.owncloud.android.ui.activity.DrawerActivity.REQ_ALL_FILES_ACCESS;
9797

@@ -138,6 +138,8 @@ public class SettingsActivity extends PreferenceActivity
138138
private String storagePath;
139139
private String pendingLock;
140140

141+
private E2EDeletionService e2EDeletionService;
142+
141143
private User user;
142144
@Inject ArbitraryDataProvider arbitraryDataProvider;
143145
@Inject AppPreferences preferences;
@@ -164,6 +166,7 @@ public void onCreate(Bundle savedInstanceState) {
164166
PreferenceScreen preferenceScreen = (PreferenceScreen) findPreference("preference_screen");
165167

166168
user = accountManager.getUser();
169+
e2EDeletionService = new E2EDeletionService(clientFactory);
167170

168171
// retrieve user's base uri
169172
setupBaseUri();
@@ -368,6 +371,8 @@ private void setupMoreCategory() {
368371

369372
removeE2E(preferenceCategoryMore);
370373

374+
removeE2EFilesAndKeys(preferenceCategoryMore);
375+
371376
setupHelpPreference(preferenceCategoryMore);
372377

373378
setupRecommendPreference(preferenceCategoryMore);
@@ -537,6 +542,46 @@ private void removeE2E(PreferenceCategory preferenceCategoryMore) {
537542
}
538543
}
539544

545+
private void removeE2EFilesAndKeys(PreferenceCategory preferenceCategoryMore) {
546+
if (BuildConfig.DEBUG) {
547+
Preference removeKeysAndFilesPreference = findPreference("remove_e2e_files_and_keys");
548+
if (removeKeysAndFilesPreference != null) {
549+
if (!FileOperationsHelper.isEndToEndEncryptionSetup(this, user)) {
550+
preferenceCategoryMore.removePreference(removeKeysAndFilesPreference);
551+
} else {
552+
removeKeysAndFilesPreference.setOnPreferenceClickListener(p -> {
553+
showRemoveE2EKeysAndFilesAlertDialog(preferenceCategoryMore, removeKeysAndFilesPreference);
554+
return true;
555+
});
556+
}
557+
}
558+
}
559+
}
560+
561+
private void showRemoveE2EKeysAndFilesAlertDialog(PreferenceCategory preferenceCategoryMore, Preference preference) {
562+
if (e2EDeletionService == null) {
563+
return;
564+
}
565+
566+
e2EDeletionService.showRemoveE2EKeysAndFilesAlertDialog(this, user, success -> {
567+
if (success) {
568+
EncryptionUtils.removeE2E(arbitraryDataProvider, user);
569+
preferenceCategoryMore.removePreference(preference);
570+
571+
Preference pMnemonic = findPreference("mnemonic");
572+
if (pMnemonic != null) {
573+
preferenceCategoryMore.removePreference(pMnemonic);
574+
}
575+
576+
Preference pRemoveE2E = findPreference("remove_e2e");
577+
if (pRemoveE2E != null) {
578+
preferenceCategoryMore.removePreference(pRemoveE2E);
579+
}
580+
}
581+
return Unit.INSTANCE;
582+
});
583+
}
584+
540585
private void showRemoveE2EAlertDialog(PreferenceCategory preferenceCategoryMore, Preference preference) {
541586
new MaterialAlertDialogBuilder(this, R.style.FallbackTheming_Dialog)
542587
.setTitle(R.string.prefs_e2e_mnemonic)

app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.google.gson.GsonBuilder;
1818
import com.google.gson.reflect.TypeToken;
1919
import com.nextcloud.client.account.User;
20+
import com.nextcloud.common.SessionTimeOutKt;
2021
import com.nextcloud.utils.e2ee.E2EVersionHelper;
2122
import com.owncloud.android.R;
2223
import com.owncloud.android.datamodel.ArbitraryDataProvider;
@@ -42,7 +43,6 @@
4243
import com.owncloud.android.lib.resources.e2ee.StoreMetadataRemoteOperation;
4344
import com.owncloud.android.lib.resources.e2ee.StoreMetadataV2RemoteOperation;
4445
import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation;
45-
import com.owncloud.android.lib.resources.e2ee.UnlockFileV1RemoteOperation;
4646
import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation;
4747
import com.owncloud.android.lib.resources.e2ee.UpdateMetadataV2RemoteOperation;
4848
import com.owncloud.android.lib.resources.files.model.ServerFileInterface;
@@ -1166,7 +1166,8 @@ public static String lockFolder(ServerFileInterface parentFile, OwnCloudClient c
11661166
public static String lockFolder(ServerFileInterface parentFile, OwnCloudClient client, long counter) throws UploadException {
11671167
// Lock folder
11681168
LockFileRemoteOperation lockFileOperation = new LockFileRemoteOperation(parentFile.getLocalId(),
1169-
counter);
1169+
counter,
1170+
SessionTimeOutKt.getDefaultSessionTimeOut());
11701171
RemoteOperationResult<String> lockFileOperationResult = lockFileOperation.execute(client);
11711172

11721173
if (lockFileOperationResult.isSuccess() &&
@@ -1366,7 +1367,7 @@ public static RemoteOperationResult<Void> unlockFolder(ServerFileInterface paren
13661367

13671368
public static RemoteOperationResult<Void> unlockFolderV1(ServerFileInterface parentFolder, OwnCloudClient client, String token) {
13681369
if (token != null) {
1369-
return new UnlockFileV1RemoteOperation(parentFolder.getLocalId(), token).execute(client);
1370+
return new UnlockFileRemoteOperation(parentFolder.getLocalId(), token, SessionTimeOutKt.getDefaultSessionTimeOut(), false).execute(client);
13701371
} else {
13711372
return new RemoteOperationResult<>(new Exception("No token available"));
13721373
}

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,7 +1384,9 @@
13841384
<string name="internet_connection_required_for_encrypted_folder_setup">An internet connection is required to set up the encrypted folder</string>
13851385
<string name="prefs_setup_e2e">Set up end-to-end encryption</string>
13861386
<string name="prefs_e2e_active">End-to-end encryption is set up!</string>
1387+
<string name="prefs_remove_e2e_keys_and_files">Remove encrypted files and keys</string>
13871388
<string name="prefs_remove_e2e">Remove encryption locally</string>
1389+
<string name="remove_e2e_keys_and_files_dialog_warning">This operation will remove all encrypted files, private and public keys. Are you sure?</string>
13881390
<string name="remove_e2e">You can remove end-to-end encryption locally on this client</string>
13891391
<string name="confirm_removal">Remove local encryption</string>
13901392
<string name="remove_e2e_message">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.</string>

app/src/main/res/xml/preferences.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@
113113
android:title="@string/prefs_remove_e2e"
114114
android:key="remove_e2e"
115115
android:summary="@string/remove_e2e" />
116+
<Preference
117+
android:title="@string/prefs_remove_e2e_keys_and_files"
118+
android:key="remove_e2e_files_and_keys" />
116119

117120
<Preference
118121
android:title="@string/prefs_help"

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
androidCommonLibraryVersion = "0.33.2"
66
androidGifDrawableVersion = "1.2.31"
77
androidImageCropperVersion = "4.7.0"
8-
androidLibraryVersion ="313455ebde7ed7b8ad7488d126c7024ceb00c99d"
8+
androidLibraryVersion ="3e61a5e3ef7a2bb7b3182944922bdf95c6201d11"
99
androidPluginVersion = "9.2.0"
1010
androidsvgVersion = "1.4"
1111
androidxMediaVersion = "1.5.1"

gradle/verification-metadata.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21102,6 +21102,14 @@
2110221102
<sha256 value="69722898d31a3a5ad32cc372f2a14a057902c75bd14019570cbfc221a67fc52c" origin="Generated by Gradle" reason="Artifact is not signed"/>
2110321103
</artifact>
2110421104
</component>
21105+
<component group="com.github.nextcloud" name="android-library" version="3c4d9d6">
21106+
<artifact name="android-library-3c4d9d6.aar">
21107+
<sha256 value="24093678fe62d18725875dba92603e92e47013844b7d67cb5c83116f78af65c4" origin="Generated by Gradle" reason="Artifact is not signed"/>
21108+
</artifact>
21109+
<artifact name="android-library-3c4d9d6.module">
21110+
<sha256 value="9e53f9f1a8a4c84165d8f408febe67c80a0591791730ef70ea97ba60d1eb586d" origin="Generated by Gradle" reason="Artifact is not signed"/>
21111+
</artifact>
21112+
</component>
2110521113
<component group="com.github.nextcloud" name="android-library" version="3d422b28376339c0fbd772e480dbbdc56b7ae1a1">
2110621114
<artifact name="android-library-3d422b28376339c0fbd772e480dbbdc56b7ae1a1.aar">
2110721115
<sha256 value="58debdb6fecf8f254f38bed011ea4849405017679c7978755e2a004031b3f69e" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -21118,6 +21126,14 @@
2111821126
<sha256 value="532080a02f0443bef7dc09d3ff484e1b9fa6ba2ca5070a0e7b44b04c4ce7475c" origin="Generated by Gradle"/>
2111921127
</artifact>
2112021128
</component>
21129+
<component group="com.github.nextcloud" name="android-library" version="3e61a5e3ef7a2bb7b3182944922bdf95c6201d11">
21130+
<artifact name="android-library-3e61a5e3ef7a2bb7b3182944922bdf95c6201d11.aar">
21131+
<sha256 value="2fbe1710b99ab094a4a0c7d1fb6cb32241dfc78188af994fd1ea6e214a754d84" origin="Generated by Gradle" reason="Artifact is not signed"/>
21132+
</artifact>
21133+
<artifact name="android-library-3e61a5e3ef7a2bb7b3182944922bdf95c6201d11.module">
21134+
<sha256 value="14f4defbcd583fbb339f5296f96e5e54455d1855761c48385a6438c828a86488" origin="Generated by Gradle" reason="Artifact is not signed"/>
21135+
</artifact>
21136+
</component>
2112121137
<component group="com.github.nextcloud" name="android-library" version="3ff8fea794d165afc1b3be698ad04bdee59e37c1">
2112221138
<artifact name="android-library-3ff8fea794d165afc1b3be698ad04bdee59e37c1.aar">
2112321139
<sha256 value="43888bc29328b621703ca813bee857853e41af6979aeaa44578beef2477ce314" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -21558,6 +21574,14 @@
2155821574
<sha256 value="c310576fd498cdc27dbcbf15532d78cd3c9cca1e30708b7e23001b7d53633d15" origin="Generated by Gradle" reason="Artifact is not signed"/>
2155921575
</artifact>
2156021576
</component>
21577+
<component group="com.github.nextcloud" name="android-library" version="a1fb1a12a2">
21578+
<artifact name="android-library-a1fb1a12a2.aar">
21579+
<sha256 value="b4a99e1d80a646f6d8c7d9fa525be60661dcb3f766aae69ba29d5c6aeb42d9f3" origin="Generated by Gradle" reason="Artifact is not signed"/>
21580+
</artifact>
21581+
<artifact name="android-library-a1fb1a12a2.module">
21582+
<sha256 value="196c709cd6ad27ee6b27efb70ef3301a630552f76b5591d7e31784488214c340" origin="Generated by Gradle" reason="Artifact is not signed"/>
21583+
</artifact>
21584+
</component>
2156121585
<component group="com.github.nextcloud" name="android-library" version="a4d86ef9d1">
2156221586
<artifact name="android-library-a4d86ef9d1.aar">
2156321587
<sha256 value="c6c70775d49d935e7691f43fee0ff51c2c0df03c041fd75da92cf1f0df1385a7" origin="Generated by Gradle" reason="Artifact is not signed"/>

0 commit comments

Comments
 (0)