diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt index 8e09ee62dbcb..ba6ba727f327 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only @@ -300,8 +301,7 @@ class EncryptionUtilsV2IT : EncryptionIT() { // random string, not real tag EncryptionUtils.generateUid(), EncryptionUtils.generateKey(), - metadataFile, - storageManager + metadataFile ) assertEquals(3, updatedMetadata.metadata.files.size) @@ -341,7 +341,6 @@ class EncryptionUtilsV2IT : EncryptionIT() { @Test fun addFolder() { - val folder = OCFile("/e/") val enc1 = MockUser("enc1", "Nextcloud") val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) assertEquals(2, metadataFile.metadata.files.size) @@ -350,9 +349,7 @@ class EncryptionUtilsV2IT : EncryptionIT() { val updatedMetadata = encryptionUtilsV2.addFolderToMetadata( EncryptionUtils.generateUid(), "new subfolder", - metadataFile, - folder, - storageManager + metadataFile ) assertEquals(2, updatedMetadata.metadata.files.size) @@ -361,7 +358,6 @@ class EncryptionUtilsV2IT : EncryptionIT() { @Test fun removeFolder() { - val folder = OCFile("/e/") val enc1 = MockUser("enc1", "Nextcloud") val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) assertEquals(2, metadataFile.metadata.files.size) @@ -371,9 +367,7 @@ class EncryptionUtilsV2IT : EncryptionIT() { var updatedMetadata = encryptionUtilsV2.addFolderToMetadata( encryptedFileName, "new subfolder", - metadataFile, - folder, - storageManager + metadataFile ) assertEquals(2, updatedMetadata.metadata.files.size) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 1df445d5d3e6..0b44558170cd 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -21,9 +21,9 @@ import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; -import android.media.MediaScannerConnection; import android.content.OperationApplicationException; import android.database.Cursor; +import android.media.MediaScannerConnection; import android.net.Uri; import android.os.RemoteException; import android.provider.MediaStore; @@ -50,8 +50,8 @@ import com.nextcloud.utils.e2ee.E2EVersionHelper; import com.nextcloud.utils.extensions.DateExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; -import com.nextcloud.utils.extensions.StringExtensionsKt; import com.owncloud.android.MainApp; +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile; import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; import com.owncloud.android.lib.common.network.WebdavEntry; import com.owncloud.android.lib.common.utils.Log_OC; @@ -2919,4 +2919,14 @@ public FileEntity getFileEntity(OCFile file) { public void updateFileEntity(@NonNull FileEntity entity) { fileDao.update(entity); } + + public void updateE2EECounter(OCFile file, DecryptedFolderMetadataFile metadata) { + updateE2EECounter(file, metadata.getMetadata().getCounter()); + } + + public void updateE2EECounter(OCFile file, long counter) { + Log_OC.d(TAG, "e2ee counter stored: " + counter + " for " + file.getDecryptedRemotePath()); + file.setE2eCounter(counter); + saveFile(file); + } } 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 3edc2ae2a2ec..6c57e9965b23 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2020-2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Andy Scherzinger @@ -336,9 +337,7 @@ private RemoteOperationResult encryptedCreateV2(OCFile parent, OwnCloudClient cl // update metadata DecryptedFolderMetadataFile updatedMetadataFile = encryptionUtilsV2.addFolderToMetadata(encryptedFileName, filename, - metadata, - parent, - getStorageManager()); + metadata); // upload metadata encryptionUtilsV2.serializeAndUploadMetadata(parent, @@ -351,9 +350,10 @@ private RemoteOperationResult encryptedCreateV2(OCFile parent, OwnCloudClient cl getStorageManager()); // unlock folder - RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token); + final var unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token); if (unlockFolderResult.isSuccess()) { + getStorageManager().updateE2EECounter(parent, metadata); token = null; } else { // TODO E2E: do better 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 e47fd552c88e..1e76a4d8bc17 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -554,10 +554,8 @@ private void synchronizeData(List folderAndFiles) { localFilesMap = prefillLocalFilesMap(metadataFileV1, fileDataStorageManager.getFolderContent(mLocalFolder, false)); } else { localFilesMap = prefillLocalFilesMap(object, fileDataStorageManager.getFolderContent(mLocalFolder, false)); - - // update counter - if (object != null) { - mLocalFolder.setE2eCounter(((DecryptedFolderMetadataFile) object).getMetadata().getCounter()); + if (object instanceof DecryptedFolderMetadataFile metadataFile) { + mLocalFolder.setE2eCounter(metadataFile.getMetadata().getCounter()); } } @@ -599,14 +597,10 @@ private void synchronizeData(List folderAndFiles) { FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName()); // update file name for encrypted files - if (E2EVersionHelper.INSTANCE.isV1(e2EVersion)) { - updateFileNameForEncryptedFileV1(fileDataStorageManager, - (DecryptedFolderMetadataFileV1) object, - updatedFile); - } else if (object != null) { - updateFileNameForEncryptedFile(fileDataStorageManager, - (DecryptedFolderMetadataFile) object, - updatedFile); + if (E2EVersionHelper.INSTANCE.isV1(e2EVersion) && object instanceof DecryptedFolderMetadataFileV1 metadata) { + updateFileNameForEncryptedFileV1(fileDataStorageManager, metadata, updatedFile); + } else if (object instanceof DecryptedFolderMetadataFile metadata) { + updateFileNameForEncryptedFile(fileDataStorageManager, metadata, updatedFile); if (localFile != null) { updatedFile.setE2eCounter(localFile.getE2eCounter()); } @@ -622,15 +616,12 @@ private void synchronizeData(List folderAndFiles) { // save updated contents in local database // update file name for encrypted files - if (E2EVersionHelper.INSTANCE.isV1(e2EVersion)) { - updateFileNameForEncryptedFileV1(fileDataStorageManager, - (DecryptedFolderMetadataFileV1) object, - mLocalFolder); - } else { - updateFileNameForEncryptedFile(fileDataStorageManager, - (DecryptedFolderMetadataFile) object, - mLocalFolder); + if (E2EVersionHelper.INSTANCE.isV1(e2EVersion) && object instanceof DecryptedFolderMetadataFileV1 metadata) { + updateFileNameForEncryptedFileV1(fileDataStorageManager, metadata, mLocalFolder); + } else if (object instanceof DecryptedFolderMetadataFile metadata) { + updateFileNameForEncryptedFile(fileDataStorageManager, metadata, mLocalFolder); } + fileDataStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values()); mChildren = updatedFiles; 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 a94178a10bc2..7f032bd3ae8c 100644 --- a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only @@ -28,15 +29,6 @@ import org.apache.commons.httpclient.HttpStatus import org.apache.commons.httpclient.NameValuePair import org.apache.jackrabbit.webdav.client.methods.DeleteMethod -/** - * Remote operation performing the removal of a remote encrypted file or folder - * - * Constructor - * - * @param remotePath RemotePath of the remote file or folder to remove from the server - * @param parentFolder parent folder - */ - @Suppress("LongParameterList") class RemoveRemoteEncryptedFileOperation internal constructor( private val remotePath: String, @@ -47,9 +39,6 @@ class RemoveRemoteEncryptedFileOperation internal constructor( private val isFolder: Boolean ) : RemoteOperation() { - /** - * Performs the remove operation. - */ @Deprecated("Deprecated in Java") @Suppress("TooGenericExceptionCaught") override fun run(client: OwnCloudClient): RemoteOperationResult { @@ -59,12 +48,14 @@ class RemoveRemoteEncryptedFileOperation internal constructor( val capability = CapabilityUtils.getCapability(context) val isE2EVersionAtLeast2 = (E2EVersionHelper.isV2Plus(capability)) val e2eeVersion = capability.endToEndEncryptionApiVersion + val storageManager = FileDataStorageManager(user, context.contentResolver) + val counter = parentFolder.e2eCounter + 1 try { - token = EncryptionUtils.lockFolder(parentFolder, client, parentFolder.e2eCounter + 1) + token = EncryptionUtils.lockFolder(parentFolder, client, counter) return if (isE2EVersionAtLeast2) { - val deleteResult = deleteForV2(client, token) + val deleteResult = deleteForV2(counter, storageManager, client, token) result = deleteResult.first delete = deleteResult.second result @@ -79,20 +70,28 @@ class RemoveRemoteEncryptedFileOperation internal constructor( Log_OC.e(TAG, "Remove " + remotePath + ": " + result.logMessage, e) } finally { delete?.releaseConnection() - token?.let { unlockFile(client, it, isE2EVersionAtLeast2) } + token?.let { unlockFile(counter, storageManager, client, it, isE2EVersionAtLeast2) } } return result } - private fun unlockFile(client: OwnCloudClient, token: String, isE2EVersionAtLeast2: Boolean) { + private fun unlockFile( + counter: Long, + storageManager: FileDataStorageManager, + client: OwnCloudClient, + token: String, + isE2EVersionAtLeast2: Boolean + ) { val unlockFileOperationResult = if (isE2EVersionAtLeast2) { EncryptionUtils.unlockFolder(parentFolder, client, token) } else { EncryptionUtils.unlockFolderV1(parentFolder, client, token) } - if (!unlockFileOperationResult.isSuccess) { + if (unlockFileOperationResult.isSuccess && isE2EVersionAtLeast2) { + storageManager.updateE2EECounter(parentFolder, counter) + } else { Log_OC.e(TAG, "Failed to unlock " + parentFolder.localId) } } @@ -165,7 +164,12 @@ class RemoveRemoteEncryptedFileOperation internal constructor( return Pair(result, delete) } - private fun deleteForV2(client: OwnCloudClient, token: String?): Pair, DeleteMethod> { + private fun deleteForV2( + counter: Long, + storageManager: FileDataStorageManager, + client: OwnCloudClient, + token: String? + ): Pair, DeleteMethod> { val encryptionUtilsV2 = EncryptionUtilsV2() val (metadataExists, metadata) = encryptionUtilsV2.retrieveMetadata( @@ -175,6 +179,9 @@ class RemoveRemoteEncryptedFileOperation internal constructor( context ) + // update counter before delete + metadata.metadata.counter = counter + val (result, delete) = deleteRemoteFile(client, token) if (isFolder) { @@ -183,10 +190,6 @@ class RemoveRemoteEncryptedFileOperation internal constructor( encryptionUtilsV2.removeFileFromMetadata(fileName, metadata) } - parentFolder.setE2eCounter(metadata.metadata.counter) - val storageManager = FileDataStorageManager(user, context.contentResolver) - storageManager.saveFile(parentFolder) - encryptionUtilsV2.serializeAndUploadMetadata( parentFolder, metadata, 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 d00aa6b4d56e..dd7f88d33588 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2020-2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2021 Chris Narkiewicz * SPDX-FileCopyrightText: 2017-2018 Andy Scherzinger @@ -601,7 +602,7 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare result = performE2EUpload(clientData); if (result.isSuccess()) { - updateMetadataForE2E(object, e2eData, clientData, e2eFiles, arbitraryDataProvider, encryptionUtilsV2, isV1MetadataExists); + upsertMetadata(object, e2eData, clientData, e2eFiles, arbitraryDataProvider, encryptionUtilsV2, isV1MetadataExists); } } catch (FileNotFoundException e) { Log_OC.e(TAG, mFile.getStoragePath() + " does not exist anymore"); @@ -613,13 +614,13 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare Log_OC.e(TAG, "UploadFileOperation exception: " + e.getLocalizedMessage()); result = new RemoteOperationResult<>(e); } finally { - result = cleanupE2EUpload(fileLock, channel, e2eFiles, result, object, client, token); + result = releaseLocksAndUnlockE2EFolder(fileLock, channel, e2eFiles, result, object, client, token); // update upload status uploadsStorageManager.updateDatabaseUploadResult(result, this); } - completeE2EUpload(result, e2eFiles, client); + handleLocalBehaviourOrSaveConflictAndDeleteTemporalFile(result, e2eFiles, client); return result; } @@ -811,7 +812,10 @@ private E2EData getE2EData(Object object) throws InvalidAlgorithmParameterExcept return new E2EData(key, iv, encryptedFile, encryptedFileName); } - private void updateMetadataForE2E(Object object, E2EData e2eData, E2EClientData clientData, E2EFiles e2eFiles, ArbitraryDataProvider arbitraryDataProvider, EncryptionUtilsV2 encryptionUtilsV2, boolean isV1MetadataExists) + /** + * Stores or updates metadata of the encrypted file along with e2ee counter. + */ + private void upsertMetadata(Object object, E2EData e2eData, E2EClientData clientData, E2EFiles e2eFiles, ArbitraryDataProvider arbitraryDataProvider, EncryptionUtilsV2 encryptionUtilsV2, boolean isV1MetadataExists) throws InvalidAlgorithmParameterException, UploadException, NoSuchPaddingException, IllegalBlockSizeException, CertificateException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { @@ -892,11 +896,7 @@ private void updateMetadataForV2(DecryptedFolderMetadataFile metadata, Encryptio e2eData.getIv(), e2eData.getEncryptedFile().getAuthenticationTag(), e2eData.getKey(), - metadata, - getStorageManager()); - - parentFile.setE2eCounter(metadata.getMetadata().getCounter()); - getStorageManager().saveFile(parentFile); + metadata); // upload metadata encryptionUtilsV2.serializeAndUploadMetadata(parentFile, @@ -909,7 +909,7 @@ private void updateMetadataForV2(DecryptedFolderMetadataFile metadata, Encryptio getStorageManager()); } - private void completeE2EUpload(RemoteOperationResult result, E2EFiles e2eFiles, OwnCloudClient client) { + private void handleLocalBehaviourOrSaveConflictAndDeleteTemporalFile(RemoteOperationResult result, E2EFiles e2eFiles, OwnCloudClient client) { if (result.isSuccess()) { handleLocalBehaviour(e2eFiles.getTemporalFile(), e2eFiles.getExpectedFile(), e2eFiles.getOriginalFile(), client); } else if (RemoteOperationResultExtensionsKt.isConflict(result.getCode())) { @@ -919,7 +919,7 @@ private void completeE2EUpload(RemoteOperationResult result, E2EFiles e2eFiles, e2eFiles.deleteTemporalFile(); } - private RemoteOperationResult cleanupE2EUpload(FileLock fileLock, FileChannel channel, E2EFiles e2eFiles, RemoteOperationResult result, Object object, OwnCloudClient client, String token) { + private RemoteOperationResult releaseLocksAndUnlockE2EFolder(FileLock fileLock, FileChannel channel, E2EFiles e2eFiles, RemoteOperationResult result, Object object, OwnCloudClient client, String token) { mUploadStarted.set(false); if (fileLock != null) { @@ -989,6 +989,12 @@ private RemoteOperationResult cleanupE2EUpload(FileLock fileLock, FileChannel ch return Unit.INSTANCE; }); } + + // only update local counter after unlock + if (object instanceof DecryptedFolderMetadataFile metadata) { + final var parentFile = e2eFiles.getParentFile(); + getStorageManager().updateE2EECounter(parentFile, metadata); + } } e2eFiles.deleteEncryptedTempFile(); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 32950fb06ebe..352bc6472b8d 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -305,8 +305,8 @@ public void updateFileEncryptionById(String fileId, boolean encrypted) { e2eCounter = EncryptionUtils.E2E_V2_INITIAL_COUNTER; } - file.setE2eCounter(e2eCounter); - mStorageManager.saveFile(file); + mStorageManager.updateE2EECounter(file, e2eCounter); + int position = getItemPosition(file); if (position != -1) { notifyItemChanged(position); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 928beeee83d5..cdacde73a97f 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -423,8 +423,7 @@ class FileDetailSharingFragment : val client = clientFactory.create(user) val metadata = RefreshFolderOperation.getDecryptedFolderMetadata(true, file, client, user, context) if (metadata is DecryptedFolderMetadataFile) { - file?.setE2eCounter(metadata.metadata.counter) - fileDataStorageManager?.saveFile(file) + fileDataStorageManager?.updateE2EECounter(file, metadata) } true } catch (e: Exception) { 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 2a4381e60906..432cef709305 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only @@ -150,7 +151,7 @@ class EncryptionUtilsV2 { ocFile: OCFile, storageManager: FileDataStorageManager, client: OwnCloudClient, - oldCounter: Long, + counterInDatabase: Long, signature: String, user: User, context: Context, @@ -235,7 +236,7 @@ class EncryptionUtilsV2 { ) } - if (!verifyMetadata(metadataFile, decryptedFolderMetadataFile, oldCounter, signature)) { + if (!verifyMetadata(metadataFile, decryptedFolderMetadataFile, counterInDatabase, signature)) { throw IllegalStateException("Metadata is corrupt!") } @@ -473,8 +474,7 @@ class EncryptionUtilsV2 { initializationVector: ByteArray, authenticationTag: String, key: ByteArray, - metadataFile: DecryptedFolderMetadataFile, - fileDataStorageManager: FileDataStorageManager + metadataFile: DecryptedFolderMetadataFile ): DecryptedFolderMetadataFile { val decryptedFile = DecryptedFile( ocFile.decryptedFileName, @@ -486,8 +486,6 @@ class EncryptionUtilsV2 { metadataFile.metadata.files[encryptedFileName] = decryptedFile metadataFile.metadata.counter++ - ocFile.setE2eCounter(metadataFile.metadata.counter) - fileDataStorageManager.saveFile(ocFile) return metadataFile } @@ -495,14 +493,10 @@ class EncryptionUtilsV2 { fun addFolderToMetadata( encryptedFileName: String, fileName: String, - metadataFile: DecryptedFolderMetadataFile, - ocFile: OCFile, - fileDataStorageManager: FileDataStorageManager + metadataFile: DecryptedFolderMetadataFile ): DecryptedFolderMetadataFile { metadataFile.metadata.folders[encryptedFileName] = fileName metadataFile.metadata.counter++ - ocFile.setE2eCounter(metadataFile.metadata.counter) - fileDataStorageManager.saveFile(ocFile) return metadataFile } @@ -857,10 +851,12 @@ class EncryptionUtilsV2 { fun verifyMetadata( encryptedFolderMetadataFile: EncryptedFolderMetadataFile, decryptedFolderMetadataFile: DecryptedFolderMetadataFile, - oldCounter: Long, + counterInDatabase: Long, signature: String ): Boolean { - if (decryptedFolderMetadataFile.metadata.counter < oldCounter) { + val counterInServer = decryptedFolderMetadataFile.metadata.counter + if (counterInServer < counterInDatabase) { + Log_OC.e(TAG, "counter in: $counterInDatabase, metadata counter $counterInServer") MainApp.showMessage(R.string.e2e_counter_too_old) return false }