diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index 368f67227318..f0913b8fdffa 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -21,6 +21,8 @@ import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.model.WorkerState import com.nextcloud.model.WorkerStateLiveData +import com.nextcloud.utils.extensions.showToast +import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.datamodel.UploadsStorageManager @@ -263,6 +265,7 @@ class FileUploadWorker( uploadFileOperation.user, File(uploadFileOperation.storagePath), uploadFileOperation.remotePath, context ) ) { + context.showToast(R.string.file_upload_worker_same_file_already_exists) return } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt index c032a63fcee9..dd4e4abc4e5b 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt @@ -13,6 +13,9 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build +import android.os.Handler +import android.os.Looper +import android.widget.Toast import com.owncloud.android.datamodel.ReceiverFlag @SuppressLint("UnspecifiedRegisterReceiverFlag") @@ -23,3 +26,11 @@ fun Context.registerBroadcastReceiver(receiver: BroadcastReceiver?, filter: Inte registerReceiver(receiver, filter) } } + +fun Context.showToast(message: String) { + Handler(Looper.getMainLooper()).post { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } +} + +fun Context.showToast(messageId: Int) = showToast(getString(messageId)) 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 b0d22cde137e..f2fc4223fa22 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -52,6 +52,9 @@ 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.operations.e2e.E2EClientData; +import com.owncloud.android.operations.e2e.E2EData; +import com.owncloud.android.operations.e2e.E2EFiles; import com.owncloud.android.utils.EncryptionUtils; import com.owncloud.android.utils.EncryptionUtilsV2; import com.owncloud.android.utils.FileStorageUtils; @@ -77,18 +80,28 @@ import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.spec.InvalidParameterSpecException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import kotlin.Triple; import static com.owncloud.android.ui.activity.FileDisplayActivity.REFRESH_FOLDER_EVENT_RECEIVER; @@ -435,14 +448,11 @@ protected RemoteOperationResult run(OwnCloudClient client) { } } - // TODO REFACTOR + // region E2E Upload @SuppressLint("AndroidLintUseSparseArrays") // gson cannot handle sparse arrays easily, therefore use hashmap private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile parentFile) { RemoteOperationResult result = null; - File temporalFile = null; - File originalFile = new File(mOriginalStoragePath); - File expectedFile = null; - File encryptedTempFile = null; + E2EFiles e2eFiles = new E2EFiles(parentFile, null, new File(mOriginalStoragePath), null, null); FileLock fileLock = null; long size; @@ -454,29 +464,14 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY); try { - // check conditions - result = checkConditions(originalFile); + result = checkConditions(e2eFiles.getOriginalFile()); if (result != null) { return result; } - /***** E2E *****/ - // Only on V2+: whenever we change something, increase counter - long counter = -1; - if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) { - counter = parentFile.getE2eCounter() + 1; - } - - // we might have an old token from interrupted upload - if (mFolderUnlockToken != null && !mFolderUnlockToken.isEmpty()) { - token = mFolderUnlockToken; - } else { - token = EncryptionUtils.lockFolder(parentFile, client, counter); - // immediately store it - mUpload.setFolderUnlockToken(token); - uploadsStorageManager.updateUpload(mUpload); - } + long counter = getE2ECounter(parentFile); + token = getFolderUnlockTokenOrLockFolder(client, parentFile, counter); // Update metadata EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2(); @@ -485,48 +480,17 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare metadataExists = true; } - if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) { + if (isEndToEndVersionAtLeastV2()) { if (object == null) { - // TODO return error return new RemoteOperationResult(new IllegalStateException("Metadata does not exist")); } } else { - // v1 is allowed to be null, thus create it - DecryptedFolderMetadataFileV1 metadata = new DecryptedFolderMetadataFileV1(); - metadata.setMetadata(new DecryptedMetadata()); - metadata.getMetadata().setVersion(1.2); - metadata.getMetadata().setMetadataKeys(new HashMap<>()); - String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey()); - String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey); - metadata.getMetadata().setMetadataKey(encryptedMetadataKey); - - if (object instanceof DecryptedFolderMetadataFileV1) { - metadata = (DecryptedFolderMetadataFileV1) object; - } - - object = metadata; + object = getDecryptedFolderMetadataV1(publicKey, object); } - // todo fail if no metadata + E2EClientData clientData = new E2EClientData(client, token, publicKey); -// metadataExists = metadataPair.getFirst(); -// DecryptedFolderMetadataFile metadata = metadataPair.getSecond(); - - // TODO E2E: check counter: must be less than our counter, check rest: signature, etc - /**** E2E *****/ - - // check name collision - List fileNames = new ArrayList<>(); - if (object instanceof DecryptedFolderMetadataFileV1 metadata) { - for (DecryptedFile file : metadata.getFiles().values()) { - fileNames.add(file.getEncrypted().getFilename()); - } - } else { - for (com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile file : - ((DecryptedFolderMetadataFile) object).getMetadata().getFiles().values()) { - fileNames.add(file.getFilename()); - } - } + List fileNames = getCollidedFileNames(object); RemoteOperationResult collisionResult = checkNameCollision(client, fileNames, parentFile.isEncrypted()); if (collisionResult != null) { @@ -534,246 +498,394 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare return collisionResult; } - mFile.setDecryptedRemotePath(parentFile.getDecryptedRemotePath() + originalFile.getName()); + mFile.setDecryptedRemotePath(parentFile.getDecryptedRemotePath() + e2eFiles.getOriginalFile().getName()); String expectedPath = FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), mFile); - expectedFile = new File(expectedPath); + e2eFiles.setExpectedFile(new File(expectedPath)); - result = copyFile(originalFile, expectedPath); + result = copyFile(e2eFiles.getOriginalFile(), expectedPath); if (!result.isSuccess()) { return result; } - // Get the last modification date of the file from the file system - long lastModifiedTimestamp = originalFile.lastModified() / 1000; + long lastModifiedTimestamp = e2eFiles.getOriginalFile().lastModified() / 1000; + Long creationTimestamp = FileUtil.getCreationTimestamp(e2eFiles.getOriginalFile()); + if (creationTimestamp == null) { + throw new NullPointerException("creationTimestamp cannot be null"); + } - Long creationTimestamp = FileUtil.getCreationTimestamp(originalFile); + E2EData e2eData = getE2EData(object); + e2eFiles.setEncryptedTempFile(e2eData.getEncryptedFile().getEncryptedFile()); + if (e2eFiles.getEncryptedTempFile() == null) { + throw new NullPointerException("encryptedTempFile cannot be null"); + } - /***** E2E *****/ - byte[] key = EncryptionUtils.generateKey(); - byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength); - Cipher cipher = EncryptionUtils.getCipher(Cipher.ENCRYPT_MODE, key, iv); - File file = new File(mFile.getStoragePath()); - EncryptedFile encryptedFile = EncryptionUtils.encryptFile(user.getAccountName(), file, cipher); + Triple channelResult = initFileChannel(result, fileLock, e2eFiles); + fileLock = channelResult.getFirst(); + result = channelResult.getSecond(); + FileChannel channel = channelResult.getThird(); - // new random file name, check if it exists in metadata - String encryptedFileName = EncryptionUtils.generateUid(); + size = getChannelSize(channel); + updateSize(size); + setUploadOperationForE2E(token, e2eFiles.getEncryptedTempFile(), e2eData.getEncryptedFileName(), lastModifiedTimestamp, creationTimestamp, size); - if (object instanceof DecryptedFolderMetadataFileV1 metadata) { - while (metadata.getFiles().get(encryptedFileName) != null) { - encryptedFileName = EncryptionUtils.generateUid(); - } - } else { - while (((DecryptedFolderMetadataFile) object).getMetadata().getFiles().get(encryptedFileName) != null) { - encryptedFileName = EncryptionUtils.generateUid(); - } + result = performE2EUpload(clientData); + + if (result.isSuccess()) { + updateMetadataForE2E(object, e2eData, clientData, e2eFiles, arbitraryDataProvider, encryptionUtilsV2, metadataExists); } + } catch (FileNotFoundException e) { + Log_OC.d(TAG, mFile.getStoragePath() + " not exists anymore"); + result = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND); + } catch (OverlappingFileLockException e) { + Log_OC.d(TAG, "Overlapping file lock exception"); + result = new RemoteOperationResult(ResultCode.LOCK_FAILED); + } catch (Exception e) { + result = new RemoteOperationResult(e); + } finally { + result = cleanupE2EUpload(fileLock, e2eFiles, result, object, client, token); + } - encryptedTempFile = encryptedFile.getEncryptedFile(); + completeE2EUpload(result, e2eFiles, client); - FileChannel channel = null; - try { - channel = new RandomAccessFile(mFile.getStoragePath(), "rw").getChannel(); - fileLock = channel.tryLock(); - } catch (FileNotFoundException e) { - // this basically means that the file is on SD card - // try to copy file to temporary dir if it doesn't exist - String temporalPath = FileStorageUtils.getInternalTemporalPath(user.getAccountName(), mContext) + - mFile.getRemotePath(); - mFile.setStoragePath(temporalPath); - temporalFile = new File(temporalPath); + return result; + } - Files.deleteIfExists(Paths.get(temporalPath)); - result = copy(originalFile, temporalFile); + private boolean isEndToEndVersionAtLeastV2() { + return getE2EVersion().compareTo(E2EVersion.V2_0) >= 0; + } - if (result.isSuccess()) { - if (temporalFile.length() == originalFile.length()) { - channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel(); - fileLock = channel.tryLock(); - } else { - result = new RemoteOperationResult(ResultCode.LOCK_FAILED); - } - } - } + private E2EVersion getE2EVersion() { + return CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion(); + } - try { - size = channel.size(); - } catch (IOException e1) { - size = new File(mFile.getStoragePath()).length(); - } + private long getE2ECounter(OCFile parentFile) { + long counter = -1; - updateSize(size); + if (isEndToEndVersionAtLeastV2()) { + counter = parentFile.getE2eCounter() + 1; + } - /// perform the upload - if (size > ChunkedFileUploadRemoteOperation.CHUNK_SIZE_MOBILE) { - boolean onWifiConnection = connectivityService.getConnectivity().isWifi(); + return counter; + } - mUploadOperation = new ChunkedFileUploadRemoteOperation(encryptedTempFile.getAbsolutePath(), - mFile.getParentRemotePath() + encryptedFileName, - mFile.getMimeType(), - mFile.getEtagInConflict(), - lastModifiedTimestamp, - onWifiConnection, - token, - creationTimestamp, - mDisableRetries - ); - } else { - mUploadOperation = new UploadFileRemoteOperation(encryptedTempFile.getAbsolutePath(), - mFile.getParentRemotePath() + encryptedFileName, - mFile.getMimeType(), - mFile.getEtagInConflict(), - lastModifiedTimestamp, - creationTimestamp, - token, - mDisableRetries - ); - } + private String getFolderUnlockTokenOrLockFolder(OwnCloudClient client, OCFile parentFile, long counter) throws UploadException { + if (mFolderUnlockToken != null && !mFolderUnlockToken.isEmpty()) { + return mFolderUnlockToken; + } - for (OnDatatransferProgressListener mDataTransferListener : mDataTransferListeners) { - mUploadOperation.addDataTransferProgressListener(mDataTransferListener); + String token = EncryptionUtils.lockFolder(parentFile, client, counter); + mUpload.setFolderUnlockToken(token); + uploadsStorageManager.updateUpload(mUpload); + + return token; + } + + private DecryptedFolderMetadataFileV1 getDecryptedFolderMetadataV1(String publicKey, Object object) + throws NoSuchPaddingException, IllegalBlockSizeException, CertificateException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + + DecryptedFolderMetadataFileV1 metadata = new DecryptedFolderMetadataFileV1(); + metadata.setMetadata(new DecryptedMetadata()); + metadata.getMetadata().setVersion(1.2); + metadata.getMetadata().setMetadataKeys(new HashMap<>()); + String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey()); + String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey); + metadata.getMetadata().setMetadataKey(encryptedMetadataKey); + + if (object instanceof DecryptedFolderMetadataFileV1) { + metadata = (DecryptedFolderMetadataFileV1) object; + } + + return metadata; + } + + private List getCollidedFileNames(Object object) { + List result = new ArrayList<>(); + + if (object instanceof DecryptedFolderMetadataFileV1 metadata) { + for (DecryptedFile file : metadata.getFiles().values()) { + result.add(file.getEncrypted().getFilename()); + } + } else if (object instanceof DecryptedFolderMetadataFile metadataFile) { + Map files = metadataFile.getMetadata().getFiles(); + for (com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile file : files.values()) { + result.add(file.getFilename()); } + } - if (mCancellationRequested.get()) { - throw new OperationCancelledException(); + return result; + } + + private String getEncryptedFileName(Object object) { + String encryptedFileName = EncryptionUtils.generateUid(); + + if (object instanceof DecryptedFolderMetadataFileV1 metadata) { + while (metadata.getFiles().get(encryptedFileName) != null) { + encryptedFileName = EncryptionUtils.generateUid(); + } + } else { + while (((DecryptedFolderMetadataFile) object).getMetadata().getFiles().get(encryptedFileName) != null) { + encryptedFileName = EncryptionUtils.generateUid(); } + } + + return encryptedFileName; + } + + private void setUploadOperationForE2E(String token, + File encryptedTempFile, + String encryptedFileName, + long lastModifiedTimestamp, + long creationTimestamp, + long size) { + + if (size > ChunkedFileUploadRemoteOperation.CHUNK_SIZE_MOBILE) { + boolean onWifiConnection = connectivityService.getConnectivity().isWifi(); + + mUploadOperation = new ChunkedFileUploadRemoteOperation(encryptedTempFile.getAbsolutePath(), + mFile.getParentRemotePath() + encryptedFileName, + mFile.getMimeType(), + mFile.getEtagInConflict(), + lastModifiedTimestamp, + onWifiConnection, + token, + creationTimestamp, + mDisableRetries + ); + } else { + mUploadOperation = new UploadFileRemoteOperation(encryptedTempFile.getAbsolutePath(), + mFile.getParentRemotePath() + encryptedFileName, + mFile.getMimeType(), + mFile.getEtagInConflict(), + lastModifiedTimestamp, + creationTimestamp, + token, + mDisableRetries + ); + } + } - result = mUploadOperation.execute(client); + private Triple initFileChannel(RemoteOperationResult result, FileLock fileLock, E2EFiles e2eFiles) throws IOException { + FileChannel channel = null; - /// move local temporal file or original file to its corresponding - // location in the Nextcloud local folder - if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED) { - result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); + try (RandomAccessFile randomAccessFile = new RandomAccessFile(mFile.getStoragePath(), "rw")) { + channel = randomAccessFile.getChannel(); + fileLock = channel.tryLock(); + } catch (IOException ioException) { + Log_OC.d(TAG, "Error caught at getChannelFromFile: " + ioException); + + // this basically means that the file is on SD card + // try to copy file to temporary dir if it doesn't exist + String temporalPath = FileStorageUtils.getInternalTemporalPath(user.getAccountName(), mContext) + + mFile.getRemotePath(); + mFile.setStoragePath(temporalPath); + e2eFiles.setTemporalFile(new File(temporalPath)); + + if (e2eFiles.getTemporalFile() == null) { + throw new NullPointerException("Original file cannot be null"); } + Files.deleteIfExists(Paths.get(temporalPath)); + result = copy(e2eFiles.getOriginalFile(), e2eFiles.getTemporalFile()); + if (result.isSuccess()) { - mFile.setDecryptedRemotePath(parentFile.getDecryptedRemotePath() + originalFile.getName()); - mFile.setRemotePath(parentFile.getRemotePath() + encryptedFileName); - - - if (object instanceof DecryptedFolderMetadataFileV1 metadata) { - // update metadata - DecryptedFile decryptedFile = new DecryptedFile(); - Data data = new Data(); - data.setFilename(mFile.getDecryptedFileName()); - data.setMimetype(mFile.getMimeType()); - data.setKey(EncryptionUtils.encodeBytesToBase64String(key)); - decryptedFile.setEncrypted(data); - decryptedFile.setInitializationVector(EncryptionUtils.encodeBytesToBase64String(iv)); - decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag()); - - metadata.getFiles().put(encryptedFileName, decryptedFile); - - EncryptedFolderMetadataFileV1 encryptedFolderMetadata = - EncryptionUtils.encryptFolderMetadata(metadata, - publicKey, - parentFile.getLocalId(), - user, - arbitraryDataProvider - ); - - String serializedFolderMetadata; - - // check if we need metadataKeys - if (metadata.getMetadata().getMetadataKey() != null) { - serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true); - } else { - serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata); + if (e2eFiles.getTemporalFile().length() == e2eFiles.getOriginalFile().length()) { + try (RandomAccessFile randomAccessFile = new RandomAccessFile(e2eFiles.getTemporalFile().getAbsolutePath(), "rw")) { + channel = randomAccessFile.getChannel(); + fileLock = channel.tryLock(); + } catch (IOException e) { + Log_OC.d(TAG, "Error caught at getChannelFromFile: " + e); } - - // upload metadata - EncryptionUtils.uploadMetadata(parentFile, - serializedFolderMetadata, - token, - client, - metadataExists, - E2EVersion.V1_2, - "", - arbitraryDataProvider, - user); } else { - DecryptedFolderMetadataFile metadata = (DecryptedFolderMetadataFile) object; - encryptionUtilsV2.addFileToMetadata( - encryptedFileName, - mFile, - iv, - encryptedFile.getAuthenticationTag(), - key, - metadata, - getStorageManager()); - - // upload metadata - encryptionUtilsV2.serializeAndUploadMetadata(parentFile, - metadata, - token, - client, - true, - mContext, - user, - getStorageManager()); + result = new RemoteOperationResult(ResultCode.LOCK_FAILED); } } - } catch (FileNotFoundException e) { - Log_OC.d(TAG, mFile.getStoragePath() + " not exists anymore"); - result = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND); - } catch (OverlappingFileLockException e) { - Log_OC.d(TAG, "Overlapping file lock exception"); - result = new RemoteOperationResult(ResultCode.LOCK_FAILED); - } catch (Exception e) { - result = new RemoteOperationResult(e); - } finally { - mUploadStarted.set(false); - sendRefreshFolderEventBroadcast(); + } - if (fileLock != null) { - try { - fileLock.release(); - } catch (IOException e) { - Log_OC.e(TAG, "Failed to unlock file with path " + mFile.getStoragePath()); - } - } + return new Triple<>(fileLock, result, channel); + } - if (temporalFile != null && !originalFile.equals(temporalFile)) { - temporalFile.delete(); - } - if (result == null) { - result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR); - } + private long getChannelSize(FileChannel channel) { + try { + return channel.size(); + } catch (IOException e1) { + return new File(mFile.getStoragePath()).length(); + } + } - logResult(result, mFile.getStoragePath(), mFile.getRemotePath()); + private RemoteOperationResult performE2EUpload(E2EClientData data) throws OperationCancelledException { + for (OnDatatransferProgressListener mDataTransferListener : mDataTransferListeners) { + mUploadOperation.addDataTransferProgressListener(mDataTransferListener); + } - // Unlock must be done otherwise folder stays locked and user can't upload any file - RemoteOperationResult unlockFolderResult; - if (object instanceof DecryptedFolderMetadataFileV1) { - unlockFolderResult = EncryptionUtils.unlockFolderV1(parentFile, client, token); - } else { - unlockFolderResult = EncryptionUtils.unlockFolder(parentFile, client, token); - } + if (mCancellationRequested.get()) { + throw new OperationCancelledException(); + } - if (unlockFolderResult != null && !unlockFolderResult.isSuccess()) { - result = unlockFolderResult; - } + RemoteOperationResult result = mUploadOperation.execute(data.getClient()); - if (encryptedTempFile != null) { - boolean isTempEncryptedFileDeleted = encryptedTempFile.delete(); - Log_OC.e(TAG, "isTempEncryptedFileDeleted: " + isTempEncryptedFileDeleted); - } else { - Log_OC.e(TAG, "Encrypted temp file cannot be found"); - } + /// move local temporal file or original file to its corresponding + // location in the Nextcloud local folder + if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED) { + result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); + } + + return result; + } + + private E2EData getE2EData(Object object) throws InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidParameterSpecException, IOException { + byte[] key = EncryptionUtils.generateKey(); + byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength); + Cipher cipher = EncryptionUtils.getCipher(Cipher.ENCRYPT_MODE, key, iv); + File file = new File(mFile.getStoragePath()); + EncryptedFile encryptedFile = EncryptionUtils.encryptFile(user.getAccountName(), file, cipher); + String encryptedFileName = getEncryptedFileName(object); + + if (key == null) { + throw new NullPointerException("key cannot be null"); + } + + return new E2EData(key, iv, encryptedFile, encryptedFileName); + } + + private void updateMetadataForE2E(Object object, E2EData e2eData, E2EClientData clientData, E2EFiles e2eFiles, ArbitraryDataProvider arbitraryDataProvider, EncryptionUtilsV2 encryptionUtilsV2, boolean metadataExists) + + throws InvalidAlgorithmParameterException, UploadException, NoSuchPaddingException, IllegalBlockSizeException, CertificateException, + NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + + mFile.setDecryptedRemotePath(e2eFiles.getParentFile().getDecryptedRemotePath() + e2eFiles.getOriginalFile().getName()); + mFile.setRemotePath(e2eFiles.getParentFile().getRemotePath() + e2eData.getEncryptedFileName()); + + + if (object instanceof DecryptedFolderMetadataFileV1 metadata) { + updateMetadataForV1(metadata, + e2eData, + clientData, + e2eFiles.getParentFile(), + arbitraryDataProvider, + metadataExists); + } else if (object instanceof DecryptedFolderMetadataFile metadata) { + updateMetadataForV2(metadata, + encryptionUtilsV2, + e2eData, + clientData, + e2eFiles.getParentFile()); + } + } + + private void updateMetadataForV1(DecryptedFolderMetadataFileV1 metadata, E2EData e2eData, E2EClientData clientData, + OCFile parentFile, ArbitraryDataProvider arbitraryDataProvider, boolean metadataExists) + + throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, + CertificateException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, UploadException { + + DecryptedFile decryptedFile = new DecryptedFile(); + Data data = new Data(); + data.setFilename(mFile.getDecryptedFileName()); + data.setMimetype(mFile.getMimeType()); + data.setKey(EncryptionUtils.encodeBytesToBase64String(e2eData.getKey())); + decryptedFile.setEncrypted(data); + decryptedFile.setInitializationVector(EncryptionUtils.encodeBytesToBase64String(e2eData.getIv())); + decryptedFile.setAuthenticationTag(e2eData.getEncryptedFile().getAuthenticationTag()); + + metadata.getFiles().put(e2eData.getEncryptedFileName(), decryptedFile); + + EncryptedFolderMetadataFileV1 encryptedFolderMetadata = + EncryptionUtils.encryptFolderMetadata(metadata, + clientData.getPublicKey(), + parentFile.getLocalId(), + user, + arbitraryDataProvider + ); + + String serializedFolderMetadata; + + if (metadata.getMetadata().getMetadataKey() != null) { + serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true); + } else { + serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata); } + // upload metadata + EncryptionUtils.uploadMetadata(parentFile, + serializedFolderMetadata, + clientData.getToken(), + clientData.getClient(), + metadataExists, + E2EVersion.V1_2, + "", + arbitraryDataProvider, + user); + } + + + private void updateMetadataForV2(DecryptedFolderMetadataFile metadata, EncryptionUtilsV2 encryptionUtilsV2, E2EData e2eData, E2EClientData clientData, OCFile parentFile) throws UploadException { + encryptionUtilsV2.addFileToMetadata( + e2eData.getEncryptedFileName(), + mFile, + e2eData.getIv(), + e2eData.getEncryptedFile().getAuthenticationTag(), + e2eData.getKey(), + metadata, + getStorageManager()); + + // upload metadata + encryptionUtilsV2.serializeAndUploadMetadata(parentFile, + metadata, + clientData.getToken(), + clientData.getClient(), + true, + mContext, + user, + getStorageManager()); + } + + private void completeE2EUpload(RemoteOperationResult result, E2EFiles e2eFiles, OwnCloudClient client) { if (result.isSuccess()) { - handleSuccessfulUpload(temporalFile, expectedFile, originalFile, client); + handleSuccessfulUpload(e2eFiles.getTemporalFile(), e2eFiles.getExpectedFile(), e2eFiles.getOriginalFile(), client); } else if (result.getCode() == ResultCode.SYNC_CONFLICT) { getStorageManager().saveConflict(mFile, mFile.getEtagInConflict()); } - // delete temporal file - if (temporalFile != null && temporalFile.exists() && !temporalFile.delete()) { - Log_OC.e(TAG, "Could not delete temporal file " + temporalFile.getAbsolutePath()); + e2eFiles.deleteTemporalFile(); + } + + private RemoteOperationResult cleanupE2EUpload(FileLock fileLock, E2EFiles e2eFiles, RemoteOperationResult result, Object object, OwnCloudClient client, String token) { + mUploadStarted.set(false); + sendRefreshFolderEventBroadcast(); + + if (fileLock != null) { + try { + fileLock.release(); + } catch (IOException e) { + Log_OC.e(TAG, "Failed to unlock file with path " + mFile.getStoragePath()); + } } + e2eFiles.deleteTemporalFileWithOriginalFileComparison(); + + if (result == null) { + result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR); + } + + logResult(result, mFile.getStoragePath(), mFile.getRemotePath()); + + // Unlock must be done otherwise folder stays locked and user can't upload any file + RemoteOperationResult unlockFolderResult; + if (object instanceof DecryptedFolderMetadataFileV1) { + unlockFolderResult = EncryptionUtils.unlockFolderV1(e2eFiles.getParentFile(), client, token); + } else { + unlockFolderResult = EncryptionUtils.unlockFolder(e2eFiles.getParentFile(), client, token); + } + + if (unlockFolderResult != null && !unlockFolderResult.isSuccess()) { + result = unlockFolderResult; + } + + e2eFiles.deleteEncryptedTempFile(); + return result; } + // endregion private void sendRefreshFolderEventBroadcast() { Intent intent = new Intent(REFRESH_FOLDER_EVENT_RECEIVER); diff --git a/app/src/main/java/com/owncloud/android/operations/e2e/E2EClientData.kt b/app/src/main/java/com/owncloud/android/operations/e2e/E2EClientData.kt new file mode 100644 index 000000000000..604f890031a0 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EClientData.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.e2e + +import com.owncloud.android.lib.common.OwnCloudClient + +data class E2EClientData(val client: OwnCloudClient, val token: String, val publicKey: String) diff --git a/app/src/main/java/com/owncloud/android/operations/e2e/E2EData.kt b/app/src/main/java/com/owncloud/android/operations/e2e/E2EData.kt new file mode 100644 index 000000000000..2063708d74af --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EData.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.e2e + +import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFile + +data class E2EData( + val key: ByteArray, + val iv: ByteArray, + val encryptedFile: EncryptedFile, + val encryptedFileName: String +) diff --git a/app/src/main/java/com/owncloud/android/operations/e2e/E2EFiles.kt b/app/src/main/java/com/owncloud/android/operations/e2e/E2EFiles.kt new file mode 100644 index 000000000000..b201775b2d3f --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EFiles.kt @@ -0,0 +1,46 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.e2e + +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import java.io.File + +data class E2EFiles( + var parentFile: OCFile, + var temporalFile: File?, + var originalFile: File, + var expectedFile: File?, + var encryptedTempFile: File? +) { + private val tag = "E2EFiles" + + fun deleteTemporalFile() { + if (temporalFile?.exists() == true && temporalFile?.delete() == false) { + Log_OC.e(tag, "Could not delete temporal file " + temporalFile?.absolutePath) + } + } + + fun deleteTemporalFileWithOriginalFileComparison() { + if (originalFile == temporalFile) { + return + } + + val isTemporalFileDeleted = temporalFile?.delete() + Log_OC.d(tag, "isTemporalFileDeleted: $isTemporalFileDeleted") + } + + fun deleteEncryptedTempFile() { + if (encryptedTempFile != null) { + val isTempEncryptedFileDeleted = encryptedTempFile?.delete() + Log_OC.e(tag, "isTempEncryptedFileDeleted: $isTempEncryptedFileDeleted") + } else { + Log_OC.e(tag, "Encrypted temp file cannot be found") + } + } +} diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 91c82f0b948e..e0c3ef5096db 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -88,6 +88,8 @@ Sunucu adı bulunamadı %1$s birden çok hesabı desteklemiyor Bağlantı kurulamadı + Oturum açmaktan vazgeç + Lütfen oturum açma işlemini tarayıcınızdan tamamlayın salt okunur olduğundan özgün klasörde kaldı Yalnızca kullanıma göre ücretlendirilmeyen kablosuz ağ üzerinden yüklensin /OtomatikYükleme diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d0045cfd9a5..80ed1b46b753 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,6 +127,7 @@ Keep file in source folder Delete file from source folder seconds ago + Same file already exists, no conflict detected LIVE No files here No folders here