diff --git a/app/src/main/java/com/nextcloud/utils/e2ee/E2EVersionHelper.kt b/app/src/main/java/com/nextcloud/utils/e2ee/E2EVersionHelper.kt new file mode 100644 index 000000000000..90cd2a791c07 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/e2ee/E2EVersionHelper.kt @@ -0,0 +1,91 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.e2ee + +import com.google.gson.reflect.TypeToken +import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1 +import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile +import com.owncloud.android.lib.resources.status.E2EVersion +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.utils.EncryptionUtils + +object E2EVersionHelper { + + /** + * Returns true if the given E2EE version is v2 or newer. + */ + fun isV2Plus(capability: OCCapability): Boolean = isV2Plus(capability.endToEndEncryptionApiVersion) + + /** + * Returns true if the given E2EE version is v2 or newer. + */ + fun isV2Plus(version: E2EVersion): Boolean = version == E2EVersion.V2_0 || version == E2EVersion.V2_1 + + /** + * Returns true if the given E2EE version is v1.x. + */ + fun isV1(capability: OCCapability): Boolean = isV1(capability.endToEndEncryptionApiVersion) + + /** + * Returns true if the given E2EE version is v1.x. + */ + fun isV1(version: E2EVersion): Boolean = + version == E2EVersion.V1_0 || version == E2EVersion.V1_1 || version == E2EVersion.V1_2 + + /** + * Returns the latest supported E2EE version. + * + * @param isV2 indicates whether the E2EE v2 series should be used + */ + fun latestVersion(isV2: Boolean): E2EVersion = if (isV2) { + E2EVersion.V2_1 + } else { + E2EVersion.V1_2 + } + + /** + * Maps a raw version string to an [E2EVersion]. + * + * @param version version string + * @return resolved [E2EVersion] or [E2EVersion.UNKNOWN] if unsupported + */ + fun fromVersionString(version: String?): E2EVersion = when (version?.trim()) { + "1.0" -> E2EVersion.V1_0 + "1.1" -> E2EVersion.V1_1 + "1.2" -> E2EVersion.V1_2 + "2", "2.0" -> E2EVersion.V2_0 + "2.1" -> E2EVersion.V2_1 + else -> E2EVersion.UNKNOWN + } + + /** + * Determines the E2EE version by inspecting encrypted folder metadata. + * + * Supports both V1 and V2 metadata formats and falls back safely + * to [E2EVersion.UNKNOWN] if parsing fails. + */ + fun fromMetadata(metadata: String): E2EVersion = runCatching { + val v1 = EncryptionUtils.deserializeJSON( + metadata, + object : TypeToken() {} + ) + + fromVersionString(v1?.metadata?.version.toString()).also { + if (it == E2EVersion.UNKNOWN) { + throw IllegalStateException("Unknown V1 version") + } + } + }.recoverCatching { + val v2 = EncryptionUtils.deserializeJSON( + metadata, + object : TypeToken() {} + ) + + fromVersionString(v2.version) + }.getOrDefault(E2EVersion.UNKNOWN) +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt index c092b8dc657b..da5caae88827 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt @@ -7,6 +7,8 @@ */ package com.owncloud.android.datamodel.e2e.v2.decrypted +import com.nextcloud.utils.e2ee.E2EVersionHelper + /** * Decrypted class representation of metadata json of folder metadata. */ @@ -15,5 +17,5 @@ data class DecryptedFolderMetadataFile( var users: MutableList = mutableListOf(), @Transient val filedrop: MutableMap = HashMap(), - val version: String = "2.0" + val version: String = E2EVersionHelper.latestVersion(true).value ) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt index 5c7b279cb4d7..b7d13dfc13c9 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt @@ -7,6 +7,8 @@ */ package com.owncloud.android.datamodel.e2e.v2.encrypted +import com.nextcloud.utils.e2ee.E2EVersionHelper + /** * Decrypted class representation of metadata json of folder metadata. */ @@ -14,5 +16,5 @@ data class EncryptedFolderMetadataFile( val metadata: EncryptedMetadata, val users: List, @Transient val filedrop: MutableMap?, - val version: String = "2.0" + val version: String = E2EVersionHelper.latestVersion(true).value ) diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index c2d6e3930ff0..a426ee2359e0 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -15,6 +15,7 @@ import android.util.Pair; import com.nextcloud.client.account.User; +import com.nextcloud.utils.e2ee.E2EVersionHelper; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -33,7 +34,6 @@ import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation; import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.utils.EncryptionUtils; import com.owncloud.android.utils.EncryptionUtilsV2; @@ -96,15 +96,15 @@ protected RemoteOperationResult run(OwnCloudClient client) { boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(parent, getStorageManager()); if (encryptedAncestor) { - E2EVersion e2EVersion = getStorageManager().getCapability(user).getEndToEndEncryptionApiVersion(); - if (e2EVersion == E2EVersion.V1_0 || - e2EVersion == E2EVersion.V1_1 || - e2EVersion == E2EVersion.V1_2) { - return encryptedCreateV1(parent, client); - } else if (e2EVersion == E2EVersion.V2_0) { + final var capability = getStorageManager().getCapability(user); + + if (E2EVersionHelper.INSTANCE.isV2Plus(capability)) { return encryptedCreateV2(parent, client); + } else if (E2EVersionHelper.INSTANCE.isV1(capability)) { + return encryptedCreateV1(parent, client); } - return new RemoteOperationResult(new IllegalStateException("E2E not supported")); + + return new RemoteOperationResult<>(new IllegalStateException("E2E not supported")); } else { return normalCreate(client); } @@ -174,7 +174,7 @@ private RemoteOperationResult encryptedCreateV1(OCFile parent, OwnCloudClient cl token, client, metadataExists, - E2EVersion.V1_2, + E2EVersionHelper.INSTANCE.latestVersion(false), "", arbitraryDataProvider, user); diff --git a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java index f760af5e0538..9dbfddc41289 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -16,6 +16,7 @@ import com.nextcloud.android.lib.resources.directediting.DirectEditingObtainRemoteOperation; import com.nextcloud.client.account.User; import com.nextcloud.common.NextcloudClient; +import com.nextcloud.utils.e2ee.E2EVersionHelper; import com.nextcloud.utils.extensions.StringExtensionsKt; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; @@ -538,7 +539,9 @@ private void synchronizeData(List folderAndFiles) { mContext); } - if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) { + final var capability = CapabilityUtils.getCapability(mContext); + + if (E2EVersionHelper.INSTANCE.isV2Plus(capability)) { if (encryptedAncestor && object == null) { throw new IllegalStateException("metadata is null!"); } @@ -548,10 +551,10 @@ private void synchronizeData(List folderAndFiles) { Map localFilesMap; E2EVersion e2EVersion; if (object instanceof DecryptedFolderMetadataFileV1 metadataFileV1) { - e2EVersion = E2EVersion.V1_2; + e2EVersion = E2EVersionHelper.INSTANCE.latestVersion(false); localFilesMap = prefillLocalFilesMap(metadataFileV1, fileDataStorageManager.getFolderContent(mLocalFolder, false)); } else { - e2EVersion = E2EVersion.V2_0; + e2EVersion = E2EVersionHelper.INSTANCE.latestVersion(true); localFilesMap = prefillLocalFilesMap(object, fileDataStorageManager.getFolderContent(mLocalFolder, false)); // update counter @@ -598,7 +601,7 @@ private void synchronizeData(List folderAndFiles) { FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName()); // update file name for encrypted files - if (e2EVersion == E2EVersion.V1_2) { + if (e2EVersion == E2EVersionHelper.INSTANCE.latestVersion(false)) { updateFileNameForEncryptedFileV1(fileDataStorageManager, (DecryptedFolderMetadataFileV1) object, updatedFile); @@ -621,7 +624,7 @@ private void synchronizeData(List folderAndFiles) { // save updated contents in local database // update file name for encrypted files - if (e2EVersion == E2EVersion.V1_2) { + if (e2EVersion == E2EVersionHelper.INSTANCE.latestVersion(false)) { updateFileNameForEncryptedFileV1(fileDataStorageManager, (DecryptedFolderMetadataFileV1) object, mLocalFolder); diff --git a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt index 5c85d0a5b20a..e1b47c58e381 100644 --- a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt @@ -11,6 +11,7 @@ import android.content.Context import androidx.core.util.component1 import androidx.core.util.component2 import com.nextcloud.client.account.User +import com.nextcloud.utils.e2ee.E2EVersionHelper import com.owncloud.android.datamodel.ArbitraryDataProvider import com.owncloud.android.datamodel.ArbitraryDataProviderImpl import com.owncloud.android.datamodel.FileDataStorageManager @@ -19,7 +20,6 @@ import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.lib.resources.status.E2EVersion import com.owncloud.android.utils.EncryptionUtils import com.owncloud.android.utils.EncryptionUtilsV2 import com.owncloud.android.utils.theme.CapabilityUtils @@ -55,8 +55,8 @@ class RemoveRemoteEncryptedFileOperation internal constructor( var result: RemoteOperationResult var delete: DeleteMethod? = null var token: String? = null - val e2eVersion = CapabilityUtils.getCapability(context).endToEndEncryptionApiVersion - val isE2EVersionAtLeast2 = e2eVersion >= E2EVersion.V2_0 + val capability = CapabilityUtils.getCapability(context) + val isE2EVersionAtLeast2 = (E2EVersionHelper.isV2Plus(capability)) try { token = EncryptionUtils.lockFolder(parentFolder, client) @@ -149,7 +149,7 @@ class RemoveRemoteEncryptedFileOperation internal constructor( token, client, metadataExists, - E2EVersion.V1_2, + E2EVersionHelper.latestVersion(false), "", arbitraryDataProvider, user diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index f29faf6d308a..fa8b2a13c72a 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -23,6 +23,7 @@ import com.nextcloud.client.network.Connectivity; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.utils.autoRename.AutoRename; +import com.nextcloud.utils.e2ee.E2EVersionHelper; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -51,7 +52,6 @@ import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.operations.e2e.E2EClientData; @@ -585,11 +585,8 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare } private boolean isEndToEndVersionAtLeastV2() { - return getE2EVersion().compareTo(E2EVersion.V2_0) >= 0; - } - - private E2EVersion getE2EVersion() { - return CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion(); + final var capability = CapabilityUtils.getCapability(mContext); + return E2EVersionHelper.INSTANCE.isV2Plus(capability); } private long getE2ECounter(OCFile parentFile) { @@ -854,7 +851,7 @@ private void updateMetadataForV1(DecryptedFolderMetadataFileV1 metadata, E2EData clientData.getToken(), clientData.getClient(), metadataExists, - E2EVersion.V1_2, + E2EVersionHelper.INSTANCE.latestVersion(false), "", arbitraryDataProvider, user); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index cfe5f17f3f57..f0b294a3007d 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -54,6 +54,7 @@ import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nextcloud.utils.EditorUtils; import com.nextcloud.utils.ShortcutUtil; +import com.nextcloud.utils.e2ee.E2EVersionHelper; import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.extensions.FragmentExtensionsKt; @@ -1953,8 +1954,7 @@ private void encryptFolder(OCFile folder, String token = EncryptionUtils.lockFolder(folder, client); OCCapability ocCapability = mContainerActivity.getStorageManager().getCapability(user.getAccountName()); - - if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V2_0) { + if (E2EVersionHelper.INSTANCE.isV2Plus(ocCapability)) { // Update metadata Pair metadataPair = EncryptionUtils.retrieveMetadata(folder, client, @@ -1980,10 +1980,8 @@ private void encryptFolder(OCFile folder, // unlock folder EncryptionUtils.unlockFolder(folder, client, token); - } else if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_0 || - ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_1 || - ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_2 - ) { + + } else if (E2EVersionHelper.INSTANCE.isV1(ocCapability)) { // unlock folder EncryptionUtils.unlockFolderV1(folder, client, token); } else if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.UNKNOWN) { 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 de6f21b5e1a6..078bc598b357 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.utils.e2ee.E2EVersionHelper; import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; @@ -404,105 +405,64 @@ public static DecryptedFolderMetadataFileV1 decryptFolderMetaData(EncryptedFolde Context context, User user ) { - RemoteOperationResult getMetadataOperationResult = new GetMetadataRemoteOperation(folder.getLocalId()) - .execute(client); + RemoteOperationResult getMetadataOperationResult = + new GetMetadataRemoteOperation(folder.getLocalId()) + .execute(client); if (!getMetadataOperationResult.isSuccess()) { return null; } - OCCapability capability = CapabilityUtils.getCapability(context); - - // decrypt metadata - EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2(); String serializedEncryptedMetadata = getMetadataOperationResult.getResultData().getMetadata(); - - E2EVersion version = determinateVersion(serializedEncryptedMetadata); - - switch (version) { - case UNKNOWN: - Log_OC.e(TAG, "Unknown e2e state"); - return null; - - case V1_0, V1_1, V1_2: - ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context); - String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY); - String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY); - EncryptedFolderMetadataFileV1 encryptedFolderMetadata = EncryptionUtils.deserializeJSON( - serializedEncryptedMetadata, new TypeToken<>() { - }); - - try { - DecryptedFolderMetadataFileV1 v1 = decryptFolderMetaData(encryptedFolderMetadata, - privateKey, - arbitraryDataProvider, - user, - folder.getLocalId()); - - if (capability.getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) { - new EncryptionUtilsV2().migrateV1ToV2andUpload( - v1, - client.getUserId(), - publicKey, - folder, - new FileDataStorageManager(user, context.getContentResolver()), - client, - user, - context - ); - } else { - return v1; - } - } catch (Exception e) { - // TODO do not crash, but show meaningful error - Log_OC.e(TAG, "Could not decrypt metadata for " + folder.getDecryptedFileName(), e); - return null; - } - - case V2_0: - return encryptionUtilsV2.parseAnyMetadata(getMetadataOperationResult.getResultData(), - user, - client, - context, - folder); - } - return null; - } - - public static E2EVersion determinateVersion(String metadata) { - try { - EncryptedFolderMetadataFileV1 v1 = EncryptionUtils.deserializeJSON( - metadata, - new TypeToken<>() { - }); - - double version = v1.getMetadata().getVersion(); - - if (version == 1.0) { - return E2EVersion.V1_0; - } else if (version == 1.1) { - return E2EVersion.V1_1; - } else if (version == 1.2) { - return E2EVersion.V1_2; - } else { - throw new IllegalStateException("Unknown version"); - } - } catch (Exception e) { - EncryptedFolderMetadataFile v2 = EncryptionUtils.deserializeJSON( - metadata, - new TypeToken<>() { + E2EVersion version = E2EVersionHelper.INSTANCE.fromMetadata(serializedEncryptedMetadata); + + if (E2EVersionHelper.INSTANCE.isV2Plus(version)) { + EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2(); + return encryptionUtilsV2.parseAnyMetadata(getMetadataOperationResult.getResultData(), + user, + client, + context, + folder); + } else if (E2EVersionHelper.INSTANCE.isV1(version)) { + ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context); + String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY); + String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY); + EncryptedFolderMetadataFileV1 encryptedFolderMetadata = EncryptionUtils.deserializeJSON( + serializedEncryptedMetadata, new TypeToken<>() { }); - if (v2 != null) { - if ("2.0".equals(v2.getVersion()) || "2".equals(v2.getVersion())) { - return E2EVersion.V2_0; + try { + DecryptedFolderMetadataFileV1 v1 = decryptFolderMetaData(encryptedFolderMetadata, + privateKey, + arbitraryDataProvider, + user, + folder.getLocalId()); + + OCCapability capability = CapabilityUtils.getCapability(context); + if (E2EVersionHelper.INSTANCE.isV2Plus(capability)) { + new EncryptionUtilsV2().migrateV1ToV2andUpload( + v1, + client.getUserId(), + publicKey, + folder, + new FileDataStorageManager(user, context.getContentResolver()), + client, + user, + context); + } else { + return v1; } - } else { - return E2EVersion.UNKNOWN; + } catch (Exception e) { + // TODO do not crash, but show meaningful error + Log_OC.e(TAG, "Could not decrypt metadata for " + folder.getDecryptedFileName(), e); + return null; } + } else if (version == E2EVersion.UNKNOWN) { + Log_OC.e(TAG, "Unknown e2e state"); + return null; } - return E2EVersion.UNKNOWN; + return null; } /* @@ -1259,7 +1219,10 @@ public static Pair retrieveMetadataV1(OC // new metadata metadata = new DecryptedFolderMetadataFileV1(); metadata.setMetadata(new DecryptedMetadata()); - metadata.getMetadata().setVersion(Double.parseDouble(E2EVersion.V1_2.getValue())); + + final var latestV1E2EEVersion = E2EVersionHelper.INSTANCE.latestVersion(false); + + metadata.getMetadata().setVersion(Double.parseDouble(latestV1E2EEVersion.getValue())); metadata.getMetadata().setMetadataKeys(new HashMap<>()); String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey()); String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey); @@ -1318,11 +1281,13 @@ public static Pair retrieveMetadata(OCFile } else if (getMetadataOperationResult.getHttpCode() == HttpStatus.SC_NOT_FOUND || getMetadataOperationResult.getHttpCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR) { + final var latestE2EEV2Version = E2EVersionHelper.INSTANCE.latestVersion(true); + // new metadata metadata = new DecryptedFolderMetadataFile(new com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedMetadata(), new ArrayList<>(), new HashMap<>(), - E2EVersion.V2_0.getValue()); + latestE2EEV2Version.getValue()); metadata.getUsers().add(new DecryptedUser(client.getUserId(), publicKey, null)); byte[] metadataKey = EncryptionUtils.generateKey(); @@ -1352,7 +1317,7 @@ public static void uploadMetadata(ServerFileInterface parentFile, RemoteOperationResult uploadMetadataOperationResult; if (metadataExists) { // update metadata - if (version == E2EVersion.V2_0) { + if (E2EVersionHelper.INSTANCE.isV2Plus(version)) { uploadMetadataOperationResult = new UpdateMetadataV2RemoteOperation( parentFile.getRemoteId(), serializedFolderMetadata, @@ -1368,7 +1333,7 @@ public static void uploadMetadata(ServerFileInterface parentFile, } } else { // store metadata - if (version == E2EVersion.V2_0) { + if (E2EVersionHelper.INSTANCE.isV2Plus(version)) { uploadMetadataOperationResult = new StoreMetadataV2RemoteOperation( parentFile.getRemoteId(), serializedFolderMetadata, diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt index 778547f172af..6f8fdc011f09 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt @@ -13,6 +13,7 @@ import androidx.annotation.VisibleForTesting import com.google.gson.reflect.TypeToken import com.nextcloud.client.account.User import com.nextcloud.utils.autoRename.AutoRename +import com.nextcloud.utils.e2ee.E2EVersionHelper import com.nextcloud.utils.extensions.showToast import com.owncloud.android.MainApp import com.owncloud.android.R @@ -606,7 +607,9 @@ class EncryptionUtilsV2 { object : TypeToken() {} ) - val decryptedFolderMetadata = if (v2.version == "2.0" || v2.version == "2") { + val e2eeVersion = E2EVersionHelper.fromVersionString(v2.version) + + val decryptedFolderMetadata = if (E2EVersionHelper.isV2Plus(e2eeVersion)) { val userId = AccountManager.get(context).getUserData( user.toPlatformAccount(), AccountUtils.Constants.KEY_USER_ID diff --git a/app/src/test/java/com/owncloud/android/utils/E2EVersionHelperTest.kt b/app/src/test/java/com/owncloud/android/utils/E2EVersionHelperTest.kt new file mode 100644 index 000000000000..3671e3f53b76 --- /dev/null +++ b/app/src/test/java/com/owncloud/android/utils/E2EVersionHelperTest.kt @@ -0,0 +1,224 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.utils + +import com.google.gson.reflect.TypeToken +import com.nextcloud.utils.e2ee.E2EVersionHelper +import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1 +import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile +import com.owncloud.android.lib.resources.status.E2EVersion +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.assertFalse +import org.junit.After +import org.junit.Before +import org.junit.Test + +@Suppress("TooManyFunctions") +class E2EVersionHelperTest { + + @Before + fun setup() { + io.mockk.mockkStatic(EncryptionUtils::class) + } + + @After + fun teardown() { + io.mockk.unmockkAll() + } + + @Test + fun `isV2orAbove returns true for V2 versions`() { + assertTrue(E2EVersionHelper.isV2Plus(E2EVersion.V2_0)) + assertTrue(E2EVersionHelper.isV2Plus(E2EVersion.V2_1)) + } + + @Test + fun `isV2orAbove returns false for non V2 versions`() { + assertFalse(E2EVersionHelper.isV2Plus(E2EVersion.V1_0)) + assertFalse(E2EVersionHelper.isV2Plus(E2EVersion.V1_1)) + assertFalse(E2EVersionHelper.isV2Plus(E2EVersion.V1_2)) + assertFalse(E2EVersionHelper.isV2Plus(E2EVersion.UNKNOWN)) + } + + @Test + fun `isV1 returns true for all V1 versions`() { + assertTrue(E2EVersionHelper.isV1(E2EVersion.V1_0)) + assertTrue(E2EVersionHelper.isV1(E2EVersion.V1_1)) + assertTrue(E2EVersionHelper.isV1(E2EVersion.V1_2)) + } + + @Test + fun `isV1 returns false for non V1 versions`() { + assertFalse(E2EVersionHelper.isV1(E2EVersion.V2_0)) + assertFalse(E2EVersionHelper.isV1(E2EVersion.V2_1)) + assertFalse(E2EVersionHelper.isV1(E2EVersion.UNKNOWN)) + } + + @Test + fun `getLatestE2EVersion returns latest V2 when isV2 is true`() { + assertEquals(E2EVersion.V2_1, E2EVersionHelper.latestVersion(true)) + } + + @Test + fun `getLatestE2EVersion returns latest V1 when isV2 is false`() { + assertEquals(E2EVersion.V1_2, E2EVersionHelper.latestVersion(false)) + } + + @Test + fun `determineE2EVersion returns V1_0`() { + mockV1("1.0") + assertEquals(E2EVersion.V1_0, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EVersion via double returns V1_0`() { + mockV1Double(1.0) + assertEquals(E2EVersion.V1_0, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EVersion via second double returns V1_0`() { + mockV1Double(1.00) + assertEquals(E2EVersion.V1_0, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EVersion via third double returns V1_1`() { + mockV1Double(1.10) + assertEquals(E2EVersion.V1_1, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EVersion via fourth double returns V1_2`() { + mockV1Double(1.2) + assertEquals(E2EVersion.V1_2, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EVersion returns V1_1`() { + mockV1("1.1") + assertEquals(E2EVersion.V1_1, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EVersion returns V1_2`() { + mockV1("1.2") + assertEquals(E2EVersion.V1_2, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EVersion returns V2_0 for 2_0 or 2`() { + mockV1Throw() + mockV2("2.0") + assertEquals(E2EVersion.V2_0, E2EVersionHelper.fromMetadata("meta")) + + mockV2("2") + assertEquals(E2EVersion.V2_0, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EVersion returns V2_1`() { + mockV1Throw() + mockV2("2.1") + assertEquals(E2EVersion.V2_1, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EVersion returns UNKNOWN for unknown V2 version`() { + mockV1Throw() + mockV2("3.0") + assertEquals(E2EVersion.UNKNOWN, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EVersion returns UNKNOWN when both deserializations fail`() { + every { + EncryptionUtils.deserializeJSON( + any(), + ofType>() + ) + } throws RuntimeException() + + every { + EncryptionUtils.deserializeJSON( + any(), + ofType>() + ) + } throws RuntimeException() + + assertEquals(E2EVersion.UNKNOWN, E2EVersionHelper.fromMetadata("meta")) + } + + @Test + fun `determineE2EFromVersionString maps versions correctly`() { + assertEquals(E2EVersion.V1_0, E2EVersionHelper.fromVersionString("1.0")) + assertEquals(E2EVersion.V1_1, E2EVersionHelper.fromVersionString("1.1")) + assertEquals(E2EVersion.V1_2, E2EVersionHelper.fromVersionString("1.2")) + assertEquals(E2EVersion.V2_0, E2EVersionHelper.fromVersionString("2")) + assertEquals(E2EVersion.V2_0, E2EVersionHelper.fromVersionString("2.0")) + assertEquals(E2EVersion.V2_1, E2EVersionHelper.fromVersionString("2.1")) + } + + @Test + fun `determineE2EFromVersionString returns UNKNOWN for invalid input`() { + assertEquals(E2EVersion.UNKNOWN, E2EVersionHelper.fromVersionString(null)) + assertEquals(E2EVersion.UNKNOWN, E2EVersionHelper.fromVersionString("")) + assertEquals(E2EVersion.UNKNOWN, E2EVersionHelper.fromVersionString("3.0")) + } + + private fun mockV1(version: String) { + val v1 = mockk { + every { metadata.version } returns version.toDouble() + } + + every { + EncryptionUtils.deserializeJSON( + any(), + ofType>() + ) + } returns v1 + } + + private fun mockV1Double(version: Double) { + val v1 = mockk { + every { metadata.version } returns version + } + + every { + EncryptionUtils.deserializeJSON( + any(), + ofType>() + ) + } returns v1 + } + + private fun mockV1Throw() { + every { + EncryptionUtils.deserializeJSON( + any(), + ofType>() + ) + } throws RuntimeException() + } + + private fun mockV2(version: String) { + val v2 = mockk { + every { this@mockk.version } returns version + } + + every { + EncryptionUtils.deserializeJSON( + any(), + ofType>() + ) + } returns v2 + } +}