Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils

import com.nextcloud.utils.e2ee.E2EVersionHelper
import com.owncloud.android.lib.resources.status.E2EVersion
import org.junit.Assert.assertEquals
import org.junit.Test

class EndToEndEncryptionVersionTests {

@Test
fun testGetMaxCompatibleE2EEVersionWhenGivenUnknownShouldReturnUnknown() {
assertEquals(E2EVersion.UNKNOWN, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.UNKNOWN))
}

@Test
fun testGetMaxCompatibleE2EEVersionWhenGivenV1_0ShouldReturnV1_0() {
assertEquals(E2EVersion.V1_0, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_0))
}

@Test
fun testGetMaxCompatibleE2EEVersionWhenGivenV1_1ShouldReturnV1_1() {
assertEquals(E2EVersion.V1_1, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_1))
}

@Test
fun testGetMaxCompatibleE2EEVersionWhenGivenV1_2ShouldReturnV1_2() {
assertEquals(E2EVersion.V1_2, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2))
}

@Test
fun testGetMaxCompatibleE2EEVersionWhenGivenV1AboveClientMaxShouldReturnClientV1Max() {
assertEquals(E2EVersion.V1_2, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2))
}

@Test
fun testGetMaxCompatibleE2EEVersionWhenGivenV2_0ShouldReturnV2_0() {
assertEquals(E2EVersion.V2_0, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_0))
}

@Test
fun testGetMaxCompatibleE2EEVersionWhenGivenV2_1ShouldReturnV2_1() {
assertEquals(E2EVersion.V2_1, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_1))
}

@Test
fun testGetMaxCompatibleE2EEVersionWhenGivenV1_2ShouldNotApplyV2CeilingShouldReturnV1_2() {
val result = E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2)
assertEquals(E2EVersion.V1_2, result)
}

@Test
fun testGetMaxCompatibleE2EEVersionWhenGivenV2_0ShouldNotApplyV1CeilingShouldReturnV2_0() {
val result = E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_0)
assertEquals(E2EVersion.V2_0, result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedUser
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFiledrop
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFiledropUser
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile
import com.owncloud.android.lib.resources.status.E2EVersion
import com.owncloud.android.operations.RefreshFolderOperation
import com.owncloud.android.util.EncryptionTestIT
import junit.framework.TestCase.assertEquals
Expand Down Expand Up @@ -469,6 +470,7 @@ class EncryptionUtilsV2IT : EncryptionIT() {
val v2 = encryptionUtilsV2.migrateV1ToV2(
v1,
enc1UserId,
storageManager.user,
enc1Cert,
folder,
storageManager
Expand Down Expand Up @@ -601,7 +603,7 @@ class EncryptionUtilsV2IT : EncryptionIT() {

metadata.keyChecksums.add(encryptionUtilsV2.hashMetadataKey(metadata.metadataKey))

return DecryptedFolderMetadataFile(metadata, users, mutableMapOf())
return DecryptedFolderMetadataFile(metadata, users, mutableMapOf(), E2EVersion.V2_1.value)
}

@Test
Expand Down
43 changes: 10 additions & 33 deletions app/src/main/java/com/nextcloud/utils/e2ee/E2EVersionHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,55 +37,32 @@ object E2EVersionHelper {
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) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to the library since we have there E2EVersion

E2EVersion.V2_1
} else {
E2EVersion.V1_2
}
fun getMaxCompatibleE2EEVersion(serverE2EEVersion: E2EVersion): E2EVersion {
if (serverE2EEVersion == E2EVersion.UNKNOWN) {
return E2EVersion.UNKNOWN
}

/**
* 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
val clientMax = E2EVersion.V2_1
return minOf(serverE2EEVersion, clientMax)
}

/**
* 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<EncryptedFolderMetadataFileV1>(
val v1 = EncryptionUtils.deserializeJSON(
metadata,
object : TypeToken<EncryptedFolderMetadataFileV1>() {}
)

fromVersionString(v1?.metadata?.version.toString()).also {
E2EVersion.fromValue(v1?.metadata?.version.toString()).also {
if (it == E2EVersion.UNKNOWN) {
throw IllegalStateException("Unknown V1 version")
}
}
}.recoverCatching {
val v2 = EncryptionUtils.deserializeJSON<EncryptedFolderMetadataFile>(
val v2 = EncryptionUtils.deserializeJSON(
metadata,
object : TypeToken<EncryptedFolderMetadataFile>() {}
)

fromVersionString(v2.version)
E2EVersion.fromValue(v2.version)
}.getOrDefault(E2EVersion.UNKNOWN)
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import com.nextcloud.model.OfflineOperationType;
import com.nextcloud.model.ShareeEntry;
import com.nextcloud.utils.date.DateFormatPattern;
import com.nextcloud.utils.e2ee.E2EVersionHelper;
import com.nextcloud.utils.extensions.DateExtensionsKt;
import com.nextcloud.utils.extensions.FileExtensionsKt;
import com.nextcloud.utils.extensions.StringExtensionsKt;
Expand Down Expand Up @@ -2452,6 +2453,16 @@ private Cursor getCapabilityCursorForAccount(String accountName) {
return cursor;
}

public String getE2EEVersion(@NonNull User user) {
return getE2EEVersionObject(user).getValue();
}

public E2EVersion getE2EEVersionObject(@NonNull User user) {
final var capabilities = getCapability(user);
final var serverE2EEVersion = capabilities.getEndToEndEncryptionApiVersion();
return E2EVersionHelper.INSTANCE.getMaxCompatibleE2EEVersion(serverE2EEVersion);
}

@NonNull
public OCCapability getCapability(User user) {
return getCapability(user.getAccountName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,10 @@
*/
package com.owncloud.android.datamodel.e2e.v2.decrypted

import com.nextcloud.utils.e2ee.E2EVersionHelper

/**
* Decrypted class representation of metadata json of folder metadata.
*/
data class DecryptedFolderMetadataFile(
val metadata: DecryptedMetadata,
var users: MutableList<DecryptedUser> = mutableListOf(),
@Transient
val filedrop: MutableMap<String, DecryptedFile> = HashMap(),
val version: String = E2EVersionHelper.latestVersion(true).value
val version: String

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must be injected from capabilities we should not give latest value. @tobiasKaminsky as we discussed.

)
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ data class EncryptedFolderMetadataFile(
val metadata: EncryptedMetadata,
val users: List<EncryptedUser>,
@Transient val filedrop: MutableMap<String, EncryptedFiledrop>?,
val version: String = E2EVersionHelper.latestVersion(true).value
val version: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
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;
Expand Down Expand Up @@ -139,14 +140,16 @@ private RemoteOperationResult encryptedCreateV1(OCFile parent, OwnCloudClient cl
// lock folder
token = EncryptionUtils.lockFolder(parent, client, EncryptionUtils.E2E_V1_INITIAL_COUNTER);

final var e2eeVersion = getStorageManager().getE2EEVersion(user);

// get metadata
Pair<Boolean, DecryptedFolderMetadataFileV1> metadataPair = EncryptionUtils.retrieveMetadataV1(parent,
client,
privateKey,
publicKey,
arbitraryDataProvider,
user
);
user,
e2eeVersion);

metadataExists = metadataPair.first;
metadata = metadataPair.second;
Expand Down Expand Up @@ -177,13 +180,15 @@ private RemoteOperationResult encryptedCreateV1(OCFile parent, OwnCloudClient cl
);
String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);

final var e2eeVersionAsObject = getStorageManager().getE2EEVersionObject(user);

// upload metadata
EncryptionUtils.uploadMetadata(parent,
serializedFolderMetadata,
token,
client,
metadataExists,
E2EVersionHelper.INSTANCE.latestVersion(false),
e2eeVersionAsObject,
"",
arbitraryDataProvider,
user);
Expand Down Expand Up @@ -304,7 +309,8 @@ private RemoteOperationResult encryptedCreateV2(OCFile parent, OwnCloudClient cl
String remoteId = result.getResultData();

if (result.isSuccess()) {
DecryptedFolderMetadataFile subFolderMetadata = encryptionUtilsV2.createDecryptedFolderMetadataFile();
String e2eeVersion = getStorageManager().getE2EEVersion(user);
DecryptedFolderMetadataFile subFolderMetadata = encryptionUtilsV2.createDecryptedFolderMetadataFile(e2eeVersion);

// upload metadata
encryptionUtilsV2.serializeAndUploadMetadata(remoteId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ protected RemoteOperationResult run(OwnCloudClient client) {
boolean metadataExists;
if (metadata == null) {
String cert = EncryptionUtils.retrievePublicKeyForUser(user, context);
metadata = new EncryptionUtilsV2().createDecryptedFolderMetadataFile();
String e2eeVersion = getStorageManager().getE2EEVersion(user);
metadata = new EncryptionUtilsV2().createDecryptedFolderMetadataFile(e2eeVersion);
metadata.getUsers().add(new DecryptedUser(client.getUserId(), cert, null));

metadataExists = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,12 +549,10 @@ private void synchronizeData(List<Object> folderAndFiles) {

// get current data about local contents of the folder to synchronize
Map<String, OCFile> localFilesMap;
E2EVersion e2EVersion;
E2EVersion e2EVersion = fileDataStorageManager.getE2EEVersionObject(user);
if (object instanceof DecryptedFolderMetadataFileV1 metadataFileV1) {
e2EVersion = E2EVersionHelper.INSTANCE.latestVersion(false);
localFilesMap = prefillLocalFilesMap(metadataFileV1, fileDataStorageManager.getFolderContent(mLocalFolder, false));
} else {
e2EVersion = E2EVersionHelper.INSTANCE.latestVersion(true);
localFilesMap = prefillLocalFilesMap(object, fileDataStorageManager.getFolderContent(mLocalFolder, false));

// update counter
Expand Down Expand Up @@ -601,7 +599,7 @@ private void synchronizeData(List<Object> folderAndFiles) {
FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName());

// update file name for encrypted files
if (e2EVersion == E2EVersionHelper.INSTANCE.latestVersion(false)) {
if (E2EVersionHelper.INSTANCE.isV1(e2EVersion)) {
updateFileNameForEncryptedFileV1(fileDataStorageManager,
(DecryptedFolderMetadataFileV1) object,
updatedFile);
Expand All @@ -624,7 +622,7 @@ private void synchronizeData(List<Object> folderAndFiles) {

// save updated contents in local database
// update file name for encrypted files
if (e2EVersion == E2EVersionHelper.INSTANCE.latestVersion(false)) {
if (E2EVersionHelper.INSTANCE.isV1(e2EVersion)) {
updateFileNameForEncryptedFileV1(fileDataStorageManager,
(DecryptedFolderMetadataFileV1) object,
mLocalFolder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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
Expand Down Expand Up @@ -57,6 +58,7 @@ class RemoveRemoteEncryptedFileOperation internal constructor(
var token: String? = null
val capability = CapabilityUtils.getCapability(context)
val isE2EVersionAtLeast2 = (E2EVersionHelper.isV2Plus(capability))
val e2eeVersion = capability.endToEndEncryptionApiVersion

try {
token = EncryptionUtils.lockFolder(parentFolder, client, parentFolder.e2eCounter + 1)
Expand All @@ -67,7 +69,7 @@ class RemoveRemoteEncryptedFileOperation internal constructor(
delete = deleteResult.second
result
} else {
val deleteResult = deleteForV1(client, token)
val deleteResult = deleteForV1(client, token, e2eeVersion)
result = deleteResult.first
delete = deleteResult.second
result
Expand Down Expand Up @@ -112,7 +114,11 @@ class RemoveRemoteEncryptedFileOperation internal constructor(
return Pair(result, delete)
}

private fun deleteForV1(client: OwnCloudClient, token: String?): Pair<RemoteOperationResult<Void>, DeleteMethod> {
private fun deleteForV1(
client: OwnCloudClient,
token: String?,
e2eeVersion: E2EVersion
): Pair<RemoteOperationResult<Void>, DeleteMethod> {
@Suppress("DEPRECATION")
val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(context)
val privateKey = arbitraryDataProvider.getValue(user.accountName, EncryptionUtils.PRIVATE_KEY)
Expand All @@ -124,7 +130,8 @@ class RemoveRemoteEncryptedFileOperation internal constructor(
privateKey,
publicKey,
arbitraryDataProvider,
user
user,
e2eeVersion.value
)

val (result, delete) = deleteRemoteFile(client, token)
Expand All @@ -149,7 +156,7 @@ class RemoveRemoteEncryptedFileOperation internal constructor(
token,
client,
metadataExists,
E2EVersionHelper.latestVersion(false),
e2eeVersion,
"",
arbitraryDataProvider,
user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
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;
Expand Down Expand Up @@ -877,13 +878,15 @@ private void updateMetadataForV1(DecryptedFolderMetadataFileV1 metadata, E2EData
serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
}

final var e2eeVersion = getStorageManager().getE2EEVersionObject(user);

// upload metadata
EncryptionUtils.uploadMetadata(parentFile,
serializedFolderMetadata,
clientData.getToken(),
clientData.getClient(),
metadataExists,
E2EVersionHelper.INSTANCE.latestVersion(false),
e2eeVersion,
"",
arbitraryDataProvider,
user);
Expand Down
Loading
Loading