Skip to content

Commit 840228a

Browse files
Merge pull request #16345 from nextcloud/e2ee-v2.1
fix(e2ee): handle E2EE v2.1
2 parents d74f162 + f48f2be commit 840228a

File tree

11 files changed

+410
-125
lines changed

11 files changed

+410
-125
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.nextcloud.utils.e2ee
9+
10+
import com.google.gson.reflect.TypeToken
11+
import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1
12+
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile
13+
import com.owncloud.android.lib.resources.status.E2EVersion
14+
import com.owncloud.android.lib.resources.status.OCCapability
15+
import com.owncloud.android.utils.EncryptionUtils
16+
17+
object E2EVersionHelper {
18+
19+
/**
20+
* Returns true if the given E2EE version is v2 or newer.
21+
*/
22+
fun isV2Plus(capability: OCCapability): Boolean = isV2Plus(capability.endToEndEncryptionApiVersion)
23+
24+
/**
25+
* Returns true if the given E2EE version is v2 or newer.
26+
*/
27+
fun isV2Plus(version: E2EVersion): Boolean = version == E2EVersion.V2_0 || version == E2EVersion.V2_1
28+
29+
/**
30+
* Returns true if the given E2EE version is v1.x.
31+
*/
32+
fun isV1(capability: OCCapability): Boolean = isV1(capability.endToEndEncryptionApiVersion)
33+
34+
/**
35+
* Returns true if the given E2EE version is v1.x.
36+
*/
37+
fun isV1(version: E2EVersion): Boolean =
38+
version == E2EVersion.V1_0 || version == E2EVersion.V1_1 || version == E2EVersion.V1_2
39+
40+
/**
41+
* Returns the latest supported E2EE version.
42+
*
43+
* @param isV2 indicates whether the E2EE v2 series should be used
44+
*/
45+
fun latestVersion(isV2: Boolean): E2EVersion = if (isV2) {
46+
E2EVersion.V2_1
47+
} else {
48+
E2EVersion.V1_2
49+
}
50+
51+
/**
52+
* Maps a raw version string to an [E2EVersion].
53+
*
54+
* @param version version string
55+
* @return resolved [E2EVersion] or [E2EVersion.UNKNOWN] if unsupported
56+
*/
57+
fun fromVersionString(version: String?): E2EVersion = when (version?.trim()) {
58+
"1.0" -> E2EVersion.V1_0
59+
"1.1" -> E2EVersion.V1_1
60+
"1.2" -> E2EVersion.V1_2
61+
"2", "2.0" -> E2EVersion.V2_0
62+
"2.1" -> E2EVersion.V2_1
63+
else -> E2EVersion.UNKNOWN
64+
}
65+
66+
/**
67+
* Determines the E2EE version by inspecting encrypted folder metadata.
68+
*
69+
* Supports both V1 and V2 metadata formats and falls back safely
70+
* to [E2EVersion.UNKNOWN] if parsing fails.
71+
*/
72+
fun fromMetadata(metadata: String): E2EVersion = runCatching {
73+
val v1 = EncryptionUtils.deserializeJSON<EncryptedFolderMetadataFileV1>(
74+
metadata,
75+
object : TypeToken<EncryptedFolderMetadataFileV1>() {}
76+
)
77+
78+
fromVersionString(v1?.metadata?.version.toString()).also {
79+
if (it == E2EVersion.UNKNOWN) {
80+
throw IllegalStateException("Unknown V1 version")
81+
}
82+
}
83+
}.recoverCatching {
84+
val v2 = EncryptionUtils.deserializeJSON<EncryptedFolderMetadataFile>(
85+
metadata,
86+
object : TypeToken<EncryptedFolderMetadataFile>() {}
87+
)
88+
89+
fromVersionString(v2.version)
90+
}.getOrDefault(E2EVersion.UNKNOWN)
91+
}

app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*/
88
package com.owncloud.android.datamodel.e2e.v2.decrypted
99

10+
import com.nextcloud.utils.e2ee.E2EVersionHelper
11+
1012
/**
1113
* Decrypted class representation of metadata json of folder metadata.
1214
*/
@@ -15,5 +17,5 @@ data class DecryptedFolderMetadataFile(
1517
var users: MutableList<DecryptedUser> = mutableListOf(),
1618
@Transient
1719
val filedrop: MutableMap<String, DecryptedFile> = HashMap(),
18-
val version: String = "2.0"
20+
val version: String = E2EVersionHelper.latestVersion(true).value
1921
)

app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
*/
88
package com.owncloud.android.datamodel.e2e.v2.encrypted
99

10+
import com.nextcloud.utils.e2ee.E2EVersionHelper
11+
1012
/**
1113
* Decrypted class representation of metadata json of folder metadata.
1214
*/
1315
data class EncryptedFolderMetadataFile(
1416
val metadata: EncryptedMetadata,
1517
val users: List<EncryptedUser>,
1618
@Transient val filedrop: MutableMap<String, EncryptedFiledrop>?,
17-
val version: String = "2.0"
19+
val version: String = E2EVersionHelper.latestVersion(true).value
1820
)

app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import android.util.Pair;
1616

1717
import com.nextcloud.client.account.User;
18+
import com.nextcloud.utils.e2ee.E2EVersionHelper;
1819
import com.owncloud.android.datamodel.ArbitraryDataProvider;
1920
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
2021
import com.owncloud.android.datamodel.FileDataStorageManager;
@@ -33,7 +34,6 @@
3334
import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation;
3435
import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation;
3536
import com.owncloud.android.lib.resources.files.model.RemoteFile;
36-
import com.owncloud.android.lib.resources.status.E2EVersion;
3737
import com.owncloud.android.operations.common.SyncOperation;
3838
import com.owncloud.android.utils.EncryptionUtils;
3939
import com.owncloud.android.utils.EncryptionUtilsV2;
@@ -96,15 +96,15 @@ protected RemoteOperationResult run(OwnCloudClient client) {
9696
boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(parent, getStorageManager());
9797

9898
if (encryptedAncestor) {
99-
E2EVersion e2EVersion = getStorageManager().getCapability(user).getEndToEndEncryptionApiVersion();
100-
if (e2EVersion == E2EVersion.V1_0 ||
101-
e2EVersion == E2EVersion.V1_1 ||
102-
e2EVersion == E2EVersion.V1_2) {
103-
return encryptedCreateV1(parent, client);
104-
} else if (e2EVersion == E2EVersion.V2_0) {
99+
final var capability = getStorageManager().getCapability(user);
100+
101+
if (E2EVersionHelper.INSTANCE.isV2Plus(capability)) {
105102
return encryptedCreateV2(parent, client);
103+
} else if (E2EVersionHelper.INSTANCE.isV1(capability)) {
104+
return encryptedCreateV1(parent, client);
106105
}
107-
return new RemoteOperationResult(new IllegalStateException("E2E not supported"));
106+
107+
return new RemoteOperationResult<>(new IllegalStateException("E2E not supported"));
108108
} else {
109109
return normalCreate(client);
110110
}
@@ -174,7 +174,7 @@ private RemoteOperationResult encryptedCreateV1(OCFile parent, OwnCloudClient cl
174174
token,
175175
client,
176176
metadataExists,
177-
E2EVersion.V1_2,
177+
E2EVersionHelper.INSTANCE.latestVersion(false),
178178
"",
179179
arbitraryDataProvider,
180180
user);

app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.nextcloud.android.lib.resources.directediting.DirectEditingObtainRemoteOperation;
1717
import com.nextcloud.client.account.User;
1818
import com.nextcloud.common.NextcloudClient;
19+
import com.nextcloud.utils.e2ee.E2EVersionHelper;
1920
import com.nextcloud.utils.extensions.StringExtensionsKt;
2021
import com.owncloud.android.datamodel.ArbitraryDataProvider;
2122
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
@@ -538,7 +539,9 @@ private void synchronizeData(List<Object> folderAndFiles) {
538539
mContext);
539540
}
540541

541-
if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) {
542+
final var capability = CapabilityUtils.getCapability(mContext);
543+
544+
if (E2EVersionHelper.INSTANCE.isV2Plus(capability)) {
542545
if (encryptedAncestor && object == null) {
543546
throw new IllegalStateException("metadata is null!");
544547
}
@@ -548,10 +551,10 @@ private void synchronizeData(List<Object> folderAndFiles) {
548551
Map<String, OCFile> localFilesMap;
549552
E2EVersion e2EVersion;
550553
if (object instanceof DecryptedFolderMetadataFileV1 metadataFileV1) {
551-
e2EVersion = E2EVersion.V1_2;
554+
e2EVersion = E2EVersionHelper.INSTANCE.latestVersion(false);
552555
localFilesMap = prefillLocalFilesMap(metadataFileV1, fileDataStorageManager.getFolderContent(mLocalFolder, false));
553556
} else {
554-
e2EVersion = E2EVersion.V2_0;
557+
e2EVersion = E2EVersionHelper.INSTANCE.latestVersion(true);
555558
localFilesMap = prefillLocalFilesMap(object, fileDataStorageManager.getFolderContent(mLocalFolder, false));
556559

557560
// update counter
@@ -598,7 +601,7 @@ private void synchronizeData(List<Object> folderAndFiles) {
598601
FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName());
599602

600603
// update file name for encrypted files
601-
if (e2EVersion == E2EVersion.V1_2) {
604+
if (e2EVersion == E2EVersionHelper.INSTANCE.latestVersion(false)) {
602605
updateFileNameForEncryptedFileV1(fileDataStorageManager,
603606
(DecryptedFolderMetadataFileV1) object,
604607
updatedFile);
@@ -621,7 +624,7 @@ private void synchronizeData(List<Object> folderAndFiles) {
621624

622625
// save updated contents in local database
623626
// update file name for encrypted files
624-
if (e2EVersion == E2EVersion.V1_2) {
627+
if (e2EVersion == E2EVersionHelper.INSTANCE.latestVersion(false)) {
625628
updateFileNameForEncryptedFileV1(fileDataStorageManager,
626629
(DecryptedFolderMetadataFileV1) object,
627630
mLocalFolder);

app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.content.Context
1111
import androidx.core.util.component1
1212
import androidx.core.util.component2
1313
import com.nextcloud.client.account.User
14+
import com.nextcloud.utils.e2ee.E2EVersionHelper
1415
import com.owncloud.android.datamodel.ArbitraryDataProvider
1516
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
1617
import com.owncloud.android.datamodel.FileDataStorageManager
@@ -19,7 +20,6 @@ import com.owncloud.android.lib.common.OwnCloudClient
1920
import com.owncloud.android.lib.common.operations.RemoteOperation
2021
import com.owncloud.android.lib.common.operations.RemoteOperationResult
2122
import com.owncloud.android.lib.common.utils.Log_OC
22-
import com.owncloud.android.lib.resources.status.E2EVersion
2323
import com.owncloud.android.utils.EncryptionUtils
2424
import com.owncloud.android.utils.EncryptionUtilsV2
2525
import com.owncloud.android.utils.theme.CapabilityUtils
@@ -55,8 +55,8 @@ class RemoveRemoteEncryptedFileOperation internal constructor(
5555
var result: RemoteOperationResult<Void>
5656
var delete: DeleteMethod? = null
5757
var token: String? = null
58-
val e2eVersion = CapabilityUtils.getCapability(context).endToEndEncryptionApiVersion
59-
val isE2EVersionAtLeast2 = e2eVersion >= E2EVersion.V2_0
58+
val capability = CapabilityUtils.getCapability(context)
59+
val isE2EVersionAtLeast2 = (E2EVersionHelper.isV2Plus(capability))
6060

6161
try {
6262
token = EncryptionUtils.lockFolder(parentFolder, client)
@@ -149,7 +149,7 @@ class RemoveRemoteEncryptedFileOperation internal constructor(
149149
token,
150150
client,
151151
metadataExists,
152-
E2EVersion.V1_2,
152+
E2EVersionHelper.latestVersion(false),
153153
"",
154154
arbitraryDataProvider,
155155
user

app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.nextcloud.client.network.Connectivity;
2424
import com.nextcloud.client.network.ConnectivityService;
2525
import com.nextcloud.utils.autoRename.AutoRename;
26+
import com.nextcloud.utils.e2ee.E2EVersionHelper;
2627
import com.owncloud.android.datamodel.ArbitraryDataProvider;
2728
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
2829
import com.owncloud.android.datamodel.FileDataStorageManager;
@@ -51,7 +52,6 @@
5152
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
5253
import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
5354
import com.owncloud.android.lib.resources.files.model.RemoteFile;
54-
import com.owncloud.android.lib.resources.status.E2EVersion;
5555
import com.owncloud.android.lib.resources.status.OCCapability;
5656
import com.owncloud.android.operations.common.SyncOperation;
5757
import com.owncloud.android.operations.e2e.E2EClientData;
@@ -585,11 +585,8 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare
585585
}
586586

587587
private boolean isEndToEndVersionAtLeastV2() {
588-
return getE2EVersion().compareTo(E2EVersion.V2_0) >= 0;
589-
}
590-
591-
private E2EVersion getE2EVersion() {
592-
return CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion();
588+
final var capability = CapabilityUtils.getCapability(mContext);
589+
return E2EVersionHelper.INSTANCE.isV2Plus(capability);
593590
}
594591

595592
private long getE2ECounter(OCFile parentFile) {
@@ -854,7 +851,7 @@ private void updateMetadataForV1(DecryptedFolderMetadataFileV1 metadata, E2EData
854851
clientData.getToken(),
855852
clientData.getClient(),
856853
metadataExists,
857-
E2EVersion.V1_2,
854+
E2EVersionHelper.INSTANCE.latestVersion(false),
858855
"",
859856
arbitraryDataProvider,
860857
user);

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
5555
import com.nextcloud.utils.EditorUtils;
5656
import com.nextcloud.utils.ShortcutUtil;
57+
import com.nextcloud.utils.e2ee.E2EVersionHelper;
5758
import com.nextcloud.utils.extensions.BundleExtensionsKt;
5859
import com.nextcloud.utils.extensions.FileExtensionsKt;
5960
import com.nextcloud.utils.extensions.FragmentExtensionsKt;
@@ -1971,8 +1972,7 @@ private void encryptFolder(OCFile folder,
19711972
String token = EncryptionUtils.lockFolder(folder, client);
19721973

19731974
OCCapability ocCapability = mContainerActivity.getStorageManager().getCapability(user.getAccountName());
1974-
1975-
if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V2_0) {
1975+
if (E2EVersionHelper.INSTANCE.isV2Plus(ocCapability)) {
19761976
// Update metadata
19771977
Pair<Boolean, DecryptedFolderMetadataFile> metadataPair = EncryptionUtils.retrieveMetadata(folder,
19781978
client,
@@ -1998,10 +1998,8 @@ private void encryptFolder(OCFile folder,
19981998
// unlock folder
19991999
EncryptionUtils.unlockFolder(folder, client, token);
20002000

2001-
} else if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_0 ||
2002-
ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_1 ||
2003-
ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_2
2004-
) {
2001+
2002+
} else if (E2EVersionHelper.INSTANCE.isV1(ocCapability)) {
20052003
// unlock folder
20062004
EncryptionUtils.unlockFolderV1(folder, client, token);
20072005
} else if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.UNKNOWN) {

0 commit comments

Comments
 (0)