diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java index f3c6e4c76494..1cb33bc54eeb 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java @@ -379,7 +379,7 @@ public boolean isPowerSavingExclusionAvailable() { }; UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext); - UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager, + UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager.getUser(), targetContext.getContentResolver()); UploadFileOperation newUpload = new UploadFileOperation( diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java index d89a63ff944b..71c2e2a62e5e 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java @@ -215,7 +215,7 @@ public boolean isPowerSavingExclusionAvailable() { }; UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext); - UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager, + UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager.getUser(), targetContext.getContentResolver()); UploadFileOperation newUpload = new UploadFileOperation( diff --git a/app/src/androidTest/java/com/owncloud/android/UploadIT.java b/app/src/androidTest/java/com/owncloud/android/UploadIT.java index f9e23bf498c4..750612a33f81 100644 --- a/app/src/androidTest/java/com/owncloud/android/UploadIT.java +++ b/app/src/androidTest/java/com/owncloud/android/UploadIT.java @@ -66,7 +66,7 @@ public class UploadIT extends AbstractOnServerIT { private static final String FOLDER = "/testUpload/"; private UploadsStorageManager uploadsStorageManager = - new UploadsStorageManager(UserAccountManagerImpl.fromContext(targetContext), + new UploadsStorageManager(UserAccountManagerImpl.fromContext(targetContext).getUser(), targetContext.getContentResolver()); private ConnectivityService connectivityServiceMock = new ConnectivityService() { diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt b/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt index dad2be82bac0..76c174e66a7b 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt @@ -75,4 +75,24 @@ class ArbitraryDataProviderIT : AbstractIT() { arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, key, value.toString()) assertEquals(value, arbitraryDataProvider.getIntegerValue(user.accountName, key)) } + + @Test + fun testIncrement() { + val key = "INCREMENT" + + // key does not exist + assertEquals(-1, arbitraryDataProvider.getIntegerValue(user.accountName, key)) + + // increment -> 1 + arbitraryDataProvider.incrementValue(user.accountName, key) + assertEquals(1, arbitraryDataProvider.getIntegerValue(user.accountName, key)) + + // increment -> 2 + arbitraryDataProvider.incrementValue(user.accountName, key) + assertEquals(2, arbitraryDataProvider.getIntegerValue(user.accountName, key)) + + // delete + arbitraryDataProvider.deleteKeyForAccount(user.accountName, key) + assertEquals(-1, arbitraryDataProvider.getIntegerValue(user.accountName, key)) + } } diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java b/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java index cf752346e50f..93068c1322f2 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java @@ -54,7 +54,7 @@ public class UploadStorageManagerTest extends AbstractIT { public void setUp() { Context instrumentationCtx = ApplicationProvider.getApplicationContext(); ContentResolver contentResolver = instrumentationCtx.getContentResolver(); - uploadsStorageManager = new UploadsStorageManager(currentAccountProvider, contentResolver); + uploadsStorageManager = new UploadsStorageManager(currentAccountProvider.getUser(), contentResolver); userAccountManager = UserAccountManagerImpl.fromContext(targetContext); Account temp = new Account("test2@test.com", MainApp.getAccountType(targetContext)); diff --git a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt index 2c6a4d03e39f..5502aa7fff43 100644 --- a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt @@ -65,7 +65,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() { fun setUp() { val contentResolver = targetContext.contentResolver val accountManager: UserAccountManager = UserAccountManagerImpl.fromContext(targetContext) - uploadsStorageManager = UploadsStorageManager(accountManager, contentResolver) + uploadsStorageManager = UploadsStorageManager(accountManager.user, contentResolver) } /** diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index 21a7e34d7032..c895dad97e1f 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -61,4 +61,7 @@ interface FileDao { @Query("SELECT * FROM filelist WHERE path LIKE :pathPattern AND file_owner = :fileOwner ORDER BY path ASC") fun getFolderWithDescendants(pathPattern: String, fileOwner: String): List + + @Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL") + fun getFilesWithSyncConflict(fileOwner: String): List } diff --git a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt index 910fba051786..20f9cb95714e 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt @@ -131,5 +131,7 @@ data class CapabilityEntity( @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_GROUPFOLDERS) val groupfolders: Int?, @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT) - val dropAccount: Int? + val dropAccount: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD) + val securityGuard: Int? ) diff --git a/app/src/main/java/com/nextcloud/client/di/AppModule.java b/app/src/main/java/com/nextcloud/client/di/AppModule.java index a6c2e67e1adb..77c1d9320f28 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppModule.java +++ b/app/src/main/java/com/nextcloud/client/di/AppModule.java @@ -145,9 +145,9 @@ FilesRepository filesRepository(UserAccountManager accountManager, ClientFactory } @Provides - UploadsStorageManager uploadsStorageManager(Context context, - CurrentAccountProvider currentAccountProvider) { - return new UploadsStorageManager(currentAccountProvider, context.getContentResolver()); + UploadsStorageManager uploadsStorageManager(CurrentAccountProvider currentAccountProvider, + Context context) { + return new UploadsStorageManager(currentAccountProvider.getUser(), context.getContentResolver()); } @Provides diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 50deb4beb977..1fb333f5ce37 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -62,7 +62,7 @@ class BackgroundJobFactory @Inject constructor( private val deviceInfo: DeviceInfo, private val accountManager: UserAccountManager, private val resources: Resources, - private val dataProvider: ArbitraryDataProvider, + private val arbitraryDataProvider: ArbitraryDataProvider, private val uploadsStorageManager: UploadsStorageManager, private val connectivityService: ConnectivityService, private val notificationManager: NotificationManager, @@ -103,6 +103,7 @@ class BackgroundJobFactory @Inject constructor( FilesExportWork::class -> createFilesExportWork(context, workerParameters) FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters) GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) + HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) else -> null // caller falls back to default factory } } @@ -139,7 +140,7 @@ class BackgroundJobFactory @Inject constructor( context, params, resources, - dataProvider, + arbitraryDataProvider, contentResolver, accountManager ) @@ -260,4 +261,13 @@ class BackgroundJobFactory @Inject constructor( params = params ) } + + private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork { + return HealthStatusWork( + context, + params, + accountManager, + arbitraryDataProvider + ) + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index d42b0568b5d9..cc8bc6a52f69 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -147,4 +147,6 @@ interface BackgroundJobManager { fun pruneJobs() fun cancelAllJobs() + fun schedulePeriodicHealthStatus() + fun startHealthStatus() } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 9da528a7fc23..7e954a0b29db 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -82,6 +82,8 @@ internal class BackgroundJobManagerImpl( const val JOB_PDF_GENERATION = "pdf_generation" const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup" const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" + const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status" + const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status" const val JOB_TEST = "test_job" @@ -507,4 +509,25 @@ internal class BackgroundJobManagerImpl( override fun cancelAllJobs() { workManager.cancelAllWorkByTag(TAG_ALL) } + + override fun schedulePeriodicHealthStatus() { + val request = periodicRequestBuilder( + jobClass = HealthStatusWork::class, + jobName = JOB_PERIODIC_HEALTH_STATUS, + intervalMins = PERIODIC_BACKUP_INTERVAL_MINUTES + ).build() + + workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_HEALTH_STATUS, ExistingPeriodicWorkPolicy.KEEP, request) + } + + override fun startHealthStatus() { + val request = oneTimeRequestBuilder(HealthStatusWork::class, JOB_IMMEDIATE_HEALTH_STATUS) + .build() + + workManager.enqueueUniqueWork( + JOB_IMMEDIATE_HEALTH_STATUS, + ExistingWorkPolicy.KEEP, + request + ) + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt b/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt new file mode 100644 index 000000000000..89625cf08d19 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt @@ -0,0 +1,131 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2023 Tobias Kaminsky + * Copyright (C) 2023 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.client.jobs + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.UploadResult +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.status.Problem +import com.owncloud.android.lib.resources.status.SendClientDiagnosticRemoteOperation +import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.theme.CapabilityUtils + +class HealthStatusWork( + private val context: Context, + params: WorkerParameters, + private val userAccountManager: UserAccountManager, + private val arbitraryDataProvider: ArbitraryDataProvider +) : Worker(context, params) { + override fun doWork(): Result { + for (user in userAccountManager.allUsers) { + // only if security guard is enabled + if (!CapabilityUtils.getCapability(user, context).securityGuard.isTrue) { + continue + } + + val syncConflicts = collectSyncConflicts(user) + + val problems = mutableListOf().apply { + addAll( + collectUploadProblems( + user, + listOf( + UploadResult.CREDENTIAL_ERROR, + UploadResult.CANNOT_CREATE_FILE, + UploadResult.FOLDER_ERROR, + UploadResult.SERVICE_INTERRUPTED + ) + ) + ) + } + + val virusDetected = collectUploadProblems(user, listOf(UploadResult.VIRUS_DETECTED)).firstOrNull() + + val e2eErrors = EncryptionUtils.readE2eError(arbitraryDataProvider, user) + + val nextcloudClient = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(user.toOwnCloudAccount(), context) + val result = + SendClientDiagnosticRemoteOperation( + syncConflicts, + problems, + virusDetected, + e2eErrors + ).execute( + nextcloudClient + ) + + if (!result.isSuccess) { + if (result.exception == null) { + Log_OC.e(TAG, "Update client health NOT successful!") + } else { + Log_OC.e(TAG, "Update client health NOT successful!", result.exception) + } + } + } + + return Result.success() + } + + private fun collectSyncConflicts(user: User): Problem? { + val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver) + + val conflicts = fileDataStorageManager.getFilesWithSyncConflict(user) + + return if (conflicts.isEmpty()) { + null + } else { + Problem("sync_conflicts", conflicts.size, conflicts.minOf { it.lastSyncDateForData }) + } + } + + private fun collectUploadProblems(user: User, errorCodes: List): List { + val uploadsStorageManager = UploadsStorageManager(user, context.contentResolver) + + val problems = uploadsStorageManager + .getUploadsForAccount(user.accountName) + .filter { + errorCodes.contains(it.lastResult) + }.groupBy { it.lastResult } + + return if (problems.isEmpty()) { + emptyList() + } else { + return problems.map { problem -> + Problem(problem.key.toString(), problem.value.size, problem.value.minOf { it.uploadEndTimestamp }) + } + } + } + + companion object { + private const val TAG = "Health Status" + } +} diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 4fa3d54fceaf..4911950ed51a 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -349,6 +349,8 @@ public void onCreate() { backgroundJobManager.scheduleMediaFoldersDetectionJob(); backgroundJobManager.startMediaFoldersDetectionJob(); + backgroundJobManager.schedulePeriodicHealthStatus(); + registerGlobalPassCodeProtection(); } diff --git a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt index 782c48a0ff8a..d4e0fbc537fd 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt @@ -28,6 +28,8 @@ interface ArbitraryDataProvider { fun deleteKeyForAccount(account: String, key: String) fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Long) + + fun incrementValue(accountName: String, key: String) fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Boolean) fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: String) @@ -43,5 +45,7 @@ interface ArbitraryDataProvider { const val DIRECT_EDITING = "DIRECT_EDITING" const val DIRECT_EDITING_ETAG = "DIRECT_EDITING_ETAG" const val PREDEFINED_STATUS = "PREDEFINED_STATUS" + const val E2E_ERRORS = "E2E_ERRORS" + const val E2E_ERRORS_TIMESTAMP = "E2E_ERRORS_TIMESTAMP" } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java index f44c842c087d..338b7cda17b6 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java @@ -63,6 +63,17 @@ public void storeOrUpdateKeyValue(@NonNull String accountName, @NonNull String k storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue)); } + @Override + public void incrementValue(@NonNull String accountName, @NonNull String key) { + int oldValue = getIntegerValue(accountName, key); + + int value = 1; + if (oldValue > 0) { + value = oldValue + 1; + } + storeOrUpdateKeyValue(accountName, key, value); + } + @Override public void storeOrUpdateKeyValue(@NonNull final String accountName, @NonNull final String key, final boolean newValue) { storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue)); 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 f72343457b6c..a3c7afb7b51f 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -1954,6 +1954,7 @@ private ContentValues createContentValues(String accountName, OCCapability capab capability.getFilesLockingVersion()); contentValues.put(ProviderTableMeta.CAPABILITIES_GROUPFOLDERS, capability.getGroupfolders().getValue()); contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_SECURITY_GUARD, capability.getSecurityGuard().getValue()); return contentValues; } @@ -2111,6 +2112,7 @@ private OCCapability createCapabilityInstance(Cursor cursor) { getString(cursor, ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION)); capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS)); capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)); + capability.setSecurityGuard(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)); } return capability; } @@ -2287,7 +2289,18 @@ public User getUser() { return user; } - public OCFile getDefaultRootPath(){ + public OCFile getDefaultRootPath() { return new OCFile(OCFile.ROOT_PATH); } + + public List getFilesWithSyncConflict(User user) { + List fileEntities = fileDao.getFilesWithSyncConflict(user.getAccountName()); + List files = new ArrayList<>(fileEntities.size()); + + for (FileEntity fileEntity : fileEntities) { + files.add(createFileInstance(fileEntity)); + } + + return files; + } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java index b6cc8a5dde63..bc36815bee1e 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java @@ -32,7 +32,6 @@ import android.net.Uri; import android.os.RemoteException; -import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.account.User; import com.owncloud.android.MainApp; import com.owncloud.android.db.OCUpload; @@ -67,17 +66,17 @@ public class UploadsStorageManager extends Observable { private static final long QUERY_PAGE_SIZE = 100; private final ContentResolver contentResolver; - private final CurrentAccountProvider currentAccountProvider; + private final User user; public UploadsStorageManager( - CurrentAccountProvider currentAccountProvider, + User user, ContentResolver contentResolver ) { if (contentResolver == null) { throw new IllegalArgumentException("Cannot create an instance with a NULL contentResolver"); } this.contentResolver = contentResolver; - this.currentAccountProvider = currentAccountProvider; + this.user = user; } /** @@ -511,8 +510,6 @@ private OCUpload createOCUploadFromCursor(Cursor c) { } public OCUpload[] getCurrentAndPendingUploadsForCurrentAccount() { - User user = currentAccountProvider.getUser(); - return getCurrentAndPendingUploadsForAccount(user.getAccountName()); } @@ -567,9 +564,11 @@ public OCUpload[] getFailedUploads() { , String.valueOf(UploadStatus.UPLOAD_FAILED.value)); } - public OCUpload[] getFinishedUploadsForCurrentAccount() { - User user = currentAccountProvider.getUser(); + public OCUpload[] getUploadsForAccount(final @NonNull String accountName) { + return getUploads(ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", accountName); + } + public OCUpload[] getFinishedUploadsForCurrentAccount() { return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", user.getAccountName()); } @@ -583,8 +582,6 @@ public OCUpload[] getFinishedUploads() { } public OCUpload[] getFailedButNotDelayedUploadsForCurrentAccount() { - User user = currentAccountProvider.getUser(); - return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + @@ -622,7 +619,6 @@ private ContentResolver getDB() { } public long clearFailedButNotDelayedUploads() { - User user = currentAccountProvider.getUser(); final long deleted = getDB().delete( ProviderTableMeta.CONTENT_URI_UPLOADS, ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + @@ -645,7 +641,6 @@ public long clearFailedButNotDelayedUploads() { } public long clearSuccessfulUploads() { - User user = currentAccountProvider.getUser(); final long deleted = getDB().delete( ProviderTableMeta.CONTENT_URI_UPLOADS, ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND + diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index e165fce1a92e..e04392ffe2a7 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -264,6 +264,7 @@ static public class ProviderTableMeta implements BaseColumns { public static final String CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI = "user_status_supports_emoji"; public static final String CAPABILITIES_GROUPFOLDERS = "groupfolders"; public static final String CAPABILITIES_DROP_ACCOUNT = "drop_account"; + public static final String CAPABILITIES_SECURITY_GUARD = "security_guard"; //Columns of Uploads table public static final String UPLOADS_LOCAL_PATH = "local_path"; 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 f5d2f1f43a5f..cb1dab978e54 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -163,7 +163,9 @@ private RemoteOperationResult encryptedCreate(OCFile parent, OwnCloudClient clie serializedFolderMetadata, token, client, - metadataExists); + metadataExists, + arbitraryDataProvider, + user); // unlock folder if (token != null) { diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java index ff1a682b5d52..9da4a78e2033 100644 --- a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java @@ -26,6 +26,7 @@ import android.webkit.MimeTypeMap; import com.nextcloud.client.account.User; +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.DecryptedFolderMetadata; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; @@ -213,7 +214,12 @@ protected RemoteOperationResult run(OwnCloudClient client) { .get(file.getEncryptedFileName()).getAuthenticationTag()); try { - byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile, key, iv, authenticationTag); + byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile, + key, + iv, + authenticationTag, + new ArbitraryDataProviderImpl(context), + user); try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) { fileOutputStream.write(decryptedBytes); 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 d0ee77afc191..d2a8b982ca5f 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -638,7 +638,9 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare serializedFolderMetadata, token, client, - metadataExists); + metadataExists, + arbitraryDataProvider, + user); // unlock result = EncryptionUtils.unlockFolder(parentFile, client, token); diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt index c78f4ed7716e..30a193d930c3 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt @@ -223,6 +223,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable { val secondKey = EncryptionUtils.decodeStringToBase64Bytes(decryptedString) if (!Arrays.equals(firstKey, secondKey)) { + EncryptionUtils.reportE2eError(arbitraryDataProvider, user) throw Exception("Keys do not match") } @@ -404,6 +405,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable { if (result.isSuccess) { publicKeyString = result.data[0] as String if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) { + EncryptionUtils.reportE2eError(arbitraryDataProvider, user) throw RuntimeException("Wrong CSR returned") } Log_OC.d(TAG, "public key success") diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt index 6252a57414c8..cefcc0b64801 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt @@ -125,8 +125,8 @@ class GroupfolderListFragment : OCFileListFragment(), Injectable, GroupfolderLis val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context) if (!fetchResult.isSuccess) { logger.e(SHARED_TAG, "Error fetching file") - if (fetchResult.isException) { - logger.e(SHARED_TAG, "exception: ", fetchResult.exception) + if (fetchResult.isException && fetchResult.exception != null) { + logger.e(SHARED_TAG, "exception: ", fetchResult.exception!!) } null } else { 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 420cabc849d0..057b72d51472 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 @@ -1765,7 +1765,9 @@ private void encryptFolder(OCFile folder, serializedFolderMetadata, token, client, - metadataExists); + metadataExists, + arbitraryDataProvider, + user); // unlock folder EncryptionUtils.unlockFolder(folder, client, token); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt index dfd4a2952f25..a2f1c28f3d50 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt @@ -87,8 +87,8 @@ class SharedListFragment : OCFileListFragment(), Injectable { val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context) if (!fetchResult.isSuccess) { logger.e(SHARED_TAG, "Error fetching file") - if (fetchResult.isException) { - logger.e(SHARED_TAG, "exception: ", fetchResult.exception) + if (fetchResult.isException && fetchResult.exception != null) { + logger.e(SHARED_TAG, "exception: ", fetchResult.exception!!) } null } else { 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 7bf42ccb73b7..988b4db27719 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java @@ -47,6 +47,8 @@ import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation; import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation; import com.owncloud.android.lib.resources.status.NextcloudVersion; +import com.owncloud.android.lib.resources.status.Problem; +import com.owncloud.android.lib.resources.status.SendClientDiagnosticRemoteOperation; import com.owncloud.android.operations.UploadException; import org.apache.commons.httpclient.HttpStatus; @@ -326,10 +328,12 @@ public static DecryptedFolderMetadata decryptFolderMetaData(EncryptedFolderMetad if (TextUtils.isEmpty(decryptedFolderChecksum) && isFolderMigrated(remoteId, user, arbitraryDataProvider)) { + reportE2eError(arbitraryDataProvider, user); throw new IllegalStateException("Possible downgrade attack detected!"); } if (!TextUtils.isEmpty(decryptedFolderChecksum) && !decryptedFolderChecksum.equals(checksum)) { + reportE2eError(arbitraryDataProvider, user); throw new IllegalStateException("Wrong checksum!"); } @@ -349,7 +353,9 @@ public static DecryptedFolderMetadata decryptFolderMetaData(EncryptedFolderMetad encryptedFile.getEncrypted(), decodeStringToBase64Bytes(encryptedKey), decodeStringToBase64Bytes(encryptedFile.getEncryptedInitializationVector()), - decodeStringToBase64Bytes(encryptedFile.getEncryptedTag()) + decodeStringToBase64Bytes(encryptedFile.getEncryptedTag()), + arbitraryDataProvider, + user ); DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile(); @@ -430,7 +436,9 @@ DecryptedFolderMetadata downloadFolderMetadata(OCFile folder, serializedFolderMetadata, token, client, - true); + true, + arbitraryDataProvider, + user); // unlock folder RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token); @@ -534,7 +542,12 @@ public static EncryptedFile encryptFile(File file, byte[] encryptionKeyBytes, by * @param authenticationTag authenticationTag from metadata * @return decrypted byte[] */ - public static byte[] decryptFile(File file, byte[] encryptionKeyBytes, byte[] iv, byte[] authenticationTag) + public static byte[] decryptFile(File file, + byte[] encryptionKeyBytes, + byte[] iv, + byte[] authenticationTag, + ArbitraryDataProvider arbitraryDataProvider, + User user) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, IOException { @@ -554,6 +567,7 @@ public static byte[] decryptFile(File file, byte[] encryptionKeyBytes, byte[] iv fileBytes.length - (128 / 8), fileBytes.length); if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) { + reportE2eError(arbitraryDataProvider, user); throw new SecurityException("Tag not correct"); } @@ -713,7 +727,9 @@ private static String encryptStringSymmetric(String string, public static String decryptStringSymmetric(String string, byte[] encryptionKeyBytes, byte[] iv, - byte[] authenticationTag) + byte[] authenticationTag, + ArbitraryDataProvider arbitraryDataProvider, + User user) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, @@ -733,6 +749,7 @@ public static String decryptStringSymmetric(String string, bytes.length); if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) { + reportE2eError(arbitraryDataProvider, user); throw new SecurityException("Tag not correct"); } @@ -1057,7 +1074,7 @@ public static Pair retrieveMetadata(OCFile par return new Pair<>(Boolean.FALSE, metadata); } else { - // TODO error + reportE2eError(arbitraryDataProvider, user); throw new UploadException("something wrong"); } } @@ -1066,7 +1083,9 @@ public static void uploadMetadata(OCFile parentFile, String serializedFolderMetadata, String token, OwnCloudClient client, - boolean metadataExists) throws UploadException { + boolean metadataExists, + ArbitraryDataProvider arbitraryDataProvider, + User user) throws UploadException { RemoteOperationResult uploadMetadataOperationResult; if (metadataExists) { // update metadata @@ -1081,6 +1100,7 @@ public static void uploadMetadata(OCFile parentFile, } if (!uploadMetadataOperationResult.isSuccess()) { + reportE2eError(arbitraryDataProvider, user); throw new UploadException("Storing/updating metadata was not successful"); } } @@ -1207,4 +1227,37 @@ public static boolean isFolderMigrated(long id, return arrayList.contains(id); } + + public static void reportE2eError(ArbitraryDataProvider arbitraryDataProvider, User user) { + arbitraryDataProvider.incrementValue(user.getAccountName(), ArbitraryDataProvider.E2E_ERRORS); + + if (arbitraryDataProvider.getLongValue(user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP) == -1L) { + arbitraryDataProvider.storeOrUpdateKeyValue( + user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP, + System.currentTimeMillis() / 1000 + ); + } + } + + @Nullable + public static Problem readE2eError(ArbitraryDataProvider arbitraryDataProvider, User user) { + int value = arbitraryDataProvider.getIntegerValue(user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS); + long timestamp = arbitraryDataProvider.getLongValue(user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP); + + arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS); + + arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP); + + if (value > 0 && timestamp > 0) { + return new Problem(SendClientDiagnosticRemoteOperation.E2E_ERRORS, value, timestamp); + } else { + return null; + } + } }