mirror of
https://github.com/nextcloud/android.git
synced 2024-11-22 05:05:31 +03:00
Report client health
Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
parent
63ec726dbc
commit
b336d01b4b
25 changed files with 1522 additions and 44 deletions
1179
app/schemas/com.nextcloud.client.database.NextcloudDatabase/76.json
Normal file
1179
app/schemas/com.nextcloud.client.database.NextcloudDatabase/76.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -75,4 +75,24 @@ class ArbitraryDataProviderIT : AbstractIT() {
|
||||||
arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, key, value.toString())
|
arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, key, value.toString())
|
||||||
assertEquals(value, arbitraryDataProvider.getIntegerValue(user.accountName, key))
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -765,7 +765,12 @@ public class EncryptionTestIT extends AbstractIT {
|
||||||
// verify authentication tag
|
// verify authentication tag
|
||||||
assertTrue(Arrays.equals(expectedAuthTag, authenticationTag));
|
assertTrue(Arrays.equals(expectedAuthTag, authenticationTag));
|
||||||
|
|
||||||
byte[] decryptedBytes = decryptFile(encryptedTempFile, key, iv, authenticationTag);
|
byte[] decryptedBytes = decryptFile(encryptedTempFile,
|
||||||
|
key,
|
||||||
|
iv,
|
||||||
|
authenticationTag,
|
||||||
|
new ArbitraryDataProviderImpl(targetContext),
|
||||||
|
user);
|
||||||
|
|
||||||
File decryptedFile = File.createTempFile("file", "dec");
|
File decryptedFile = File.createTempFile("file", "dec");
|
||||||
FileOutputStream fileOutputStream1 = new FileOutputStream(decryptedFile);
|
FileOutputStream fileOutputStream1 = new FileOutputStream(decryptedFile);
|
||||||
|
|
|
@ -68,7 +68,8 @@ import com.owncloud.android.db.ProviderMeta
|
||||||
AutoMigration(from = 71, to = 72),
|
AutoMigration(from = 71, to = 72),
|
||||||
AutoMigration(from = 72, to = 73),
|
AutoMigration(from = 72, to = 73),
|
||||||
AutoMigration(from = 73, to = 74, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
|
AutoMigration(from = 73, to = 74, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
|
||||||
AutoMigration(from = 74, to = 75)
|
AutoMigration(from = 74, to = 75),
|
||||||
|
AutoMigration(from = 75, to = 76)
|
||||||
],
|
],
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
|
|
|
@ -61,4 +61,7 @@ interface FileDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM filelist WHERE path LIKE :pathPattern AND file_owner = :fileOwner ORDER BY path ASC")
|
@Query("SELECT * FROM filelist WHERE path LIKE :pathPattern AND file_owner = :fileOwner ORDER BY path ASC")
|
||||||
fun getFolderWithDescendants(pathPattern: String, fileOwner: String): List<FileEntity>
|
fun getFolderWithDescendants(pathPattern: String, fileOwner: String): List<FileEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL")
|
||||||
|
fun getFilesWithSyncConflict(fileOwner: String): List<FileEntity>
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,5 +131,7 @@ data class CapabilityEntity(
|
||||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_GROUPFOLDERS)
|
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_GROUPFOLDERS)
|
||||||
val groupfolders: Int?,
|
val groupfolders: Int?,
|
||||||
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)
|
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)
|
||||||
val dropAccount: Int?
|
val dropAccount: Int?,
|
||||||
|
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)
|
||||||
|
val securityGuard: Int?
|
||||||
)
|
)
|
||||||
|
|
|
@ -145,8 +145,8 @@ class AppModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
UploadsStorageManager uploadsStorageManager(Context context,
|
UploadsStorageManager uploadsStorageManager(CurrentAccountProvider currentAccountProvider,
|
||||||
CurrentAccountProvider currentAccountProvider) {
|
Context context) {
|
||||||
return new UploadsStorageManager(currentAccountProvider, context.getContentResolver());
|
return new UploadsStorageManager(currentAccountProvider, context.getContentResolver());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ class BackgroundJobFactory @Inject constructor(
|
||||||
private val deviceInfo: DeviceInfo,
|
private val deviceInfo: DeviceInfo,
|
||||||
private val accountManager: UserAccountManager,
|
private val accountManager: UserAccountManager,
|
||||||
private val resources: Resources,
|
private val resources: Resources,
|
||||||
private val dataProvider: ArbitraryDataProvider,
|
private val arbitraryDataProvider: ArbitraryDataProvider,
|
||||||
private val uploadsStorageManager: UploadsStorageManager,
|
private val uploadsStorageManager: UploadsStorageManager,
|
||||||
private val connectivityService: ConnectivityService,
|
private val connectivityService: ConnectivityService,
|
||||||
private val notificationManager: NotificationManager,
|
private val notificationManager: NotificationManager,
|
||||||
|
@ -103,6 +103,7 @@ class BackgroundJobFactory @Inject constructor(
|
||||||
FilesExportWork::class -> createFilesExportWork(context, workerParameters)
|
FilesExportWork::class -> createFilesExportWork(context, workerParameters)
|
||||||
FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters)
|
FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters)
|
||||||
GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
|
GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
|
||||||
|
HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
|
||||||
else -> null // caller falls back to default factory
|
else -> null // caller falls back to default factory
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,7 +140,7 @@ class BackgroundJobFactory @Inject constructor(
|
||||||
context,
|
context,
|
||||||
params,
|
params,
|
||||||
resources,
|
resources,
|
||||||
dataProvider,
|
arbitraryDataProvider,
|
||||||
contentResolver,
|
contentResolver,
|
||||||
accountManager
|
accountManager
|
||||||
)
|
)
|
||||||
|
@ -260,4 +261,13 @@ class BackgroundJobFactory @Inject constructor(
|
||||||
params = params
|
params = params
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork {
|
||||||
|
return HealthStatusWork(
|
||||||
|
context,
|
||||||
|
params,
|
||||||
|
accountManager,
|
||||||
|
arbitraryDataProvider
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,4 +147,6 @@ interface BackgroundJobManager {
|
||||||
|
|
||||||
fun pruneJobs()
|
fun pruneJobs()
|
||||||
fun cancelAllJobs()
|
fun cancelAllJobs()
|
||||||
|
fun schedulePeriodicHealthStatus()
|
||||||
|
fun startHealthStatus()
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,8 @@ internal class BackgroundJobManagerImpl(
|
||||||
const val JOB_PDF_GENERATION = "pdf_generation"
|
const val JOB_PDF_GENERATION = "pdf_generation"
|
||||||
const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
|
const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
|
||||||
const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export"
|
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"
|
const val JOB_TEST = "test_job"
|
||||||
|
|
||||||
|
@ -507,4 +509,25 @@ internal class BackgroundJobManagerImpl(
|
||||||
override fun cancelAllJobs() {
|
override fun cancelAllJobs() {
|
||||||
workManager.cancelAllWorkByTag(TAG_ALL)
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
131
app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt
Normal file
131
app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Problem>().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<UploadResult>): List<Problem> {
|
||||||
|
val uploadsStorageManager = UploadsStorageManager(userAccountManager, 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -349,6 +349,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
|
||||||
backgroundJobManager.scheduleMediaFoldersDetectionJob();
|
backgroundJobManager.scheduleMediaFoldersDetectionJob();
|
||||||
backgroundJobManager.startMediaFoldersDetectionJob();
|
backgroundJobManager.startMediaFoldersDetectionJob();
|
||||||
|
|
||||||
|
backgroundJobManager.schedulePeriodicHealthStatus();
|
||||||
|
|
||||||
registerGlobalPassCodeProtection();
|
registerGlobalPassCodeProtection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,8 @@ interface ArbitraryDataProvider {
|
||||||
fun deleteKeyForAccount(account: String, key: String)
|
fun deleteKeyForAccount(account: String, key: String)
|
||||||
|
|
||||||
fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Long)
|
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: Boolean)
|
||||||
fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: String)
|
fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: String)
|
||||||
|
|
||||||
|
@ -43,5 +45,7 @@ interface ArbitraryDataProvider {
|
||||||
const val DIRECT_EDITING = "DIRECT_EDITING"
|
const val DIRECT_EDITING = "DIRECT_EDITING"
|
||||||
const val DIRECT_EDITING_ETAG = "DIRECT_EDITING_ETAG"
|
const val DIRECT_EDITING_ETAG = "DIRECT_EDITING_ETAG"
|
||||||
const val PREDEFINED_STATUS = "PREDEFINED_STATUS"
|
const val PREDEFINED_STATUS = "PREDEFINED_STATUS"
|
||||||
|
const val E2E_ERRORS = "E2E_ERRORS"
|
||||||
|
const val E2E_ERRORS_TIMESTAMP = "E2E_ERRORS_TIMESTAMP"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,17 @@ public class ArbitraryDataProviderImpl implements ArbitraryDataProvider {
|
||||||
storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue));
|
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
|
@Override
|
||||||
public void storeOrUpdateKeyValue(@NonNull final String accountName, @NonNull final String key, final boolean newValue) {
|
public void storeOrUpdateKeyValue(@NonNull final String accountName, @NonNull final String key, final boolean newValue) {
|
||||||
storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue));
|
storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue));
|
||||||
|
|
|
@ -1954,6 +1954,7 @@ public class FileDataStorageManager {
|
||||||
capability.getFilesLockingVersion());
|
capability.getFilesLockingVersion());
|
||||||
contentValues.put(ProviderTableMeta.CAPABILITIES_GROUPFOLDERS, capability.getGroupfolders().getValue());
|
contentValues.put(ProviderTableMeta.CAPABILITIES_GROUPFOLDERS, capability.getGroupfolders().getValue());
|
||||||
contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue());
|
contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue());
|
||||||
|
contentValues.put(ProviderTableMeta.CAPABILITIES_SECURITY_GUARD, capability.getSecurityGuard().getValue());
|
||||||
|
|
||||||
return contentValues;
|
return contentValues;
|
||||||
}
|
}
|
||||||
|
@ -2111,6 +2112,7 @@ public class FileDataStorageManager {
|
||||||
getString(cursor, ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION));
|
getString(cursor, ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION));
|
||||||
capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS));
|
capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS));
|
||||||
capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT));
|
capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT));
|
||||||
|
capability.setSecurityGuard(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SECURITY_GUARD));
|
||||||
}
|
}
|
||||||
return capability;
|
return capability;
|
||||||
}
|
}
|
||||||
|
@ -2290,4 +2292,15 @@ public class FileDataStorageManager {
|
||||||
public OCFile getDefaultRootPath() {
|
public OCFile getDefaultRootPath() {
|
||||||
return new OCFile(OCFile.ROOT_PATH);
|
return new OCFile(OCFile.ROOT_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<OCFile> getFilesWithSyncConflict(User user) {
|
||||||
|
List<FileEntity> fileEntities = fileDao.getFilesWithSyncConflict(user.getAccountName());
|
||||||
|
List<OCFile> files = new ArrayList<>(fileEntities.size());
|
||||||
|
|
||||||
|
for (FileEntity fileEntity : fileEntities) {
|
||||||
|
files.add(createFileInstance(fileEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -567,6 +567,10 @@ public class UploadsStorageManager extends Observable {
|
||||||
, String.valueOf(UploadStatus.UPLOAD_FAILED.value));
|
, String.valueOf(UploadStatus.UPLOAD_FAILED.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OCUpload[] getUploadsForAccount(final @NonNull String accountName) {
|
||||||
|
return getUploads(ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", accountName);
|
||||||
|
}
|
||||||
|
|
||||||
public OCUpload[] getFinishedUploadsForCurrentAccount() {
|
public OCUpload[] getFinishedUploadsForCurrentAccount() {
|
||||||
User user = currentAccountProvider.getUser();
|
User user = currentAccountProvider.getUser();
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public class ProviderMeta {
|
public class ProviderMeta {
|
||||||
public static final String DB_NAME = "filelist";
|
public static final String DB_NAME = "filelist";
|
||||||
public static final int DB_VERSION = 75;
|
public static final int DB_VERSION = 76;
|
||||||
|
|
||||||
private ProviderMeta() {
|
private ProviderMeta() {
|
||||||
// No instance
|
// No instance
|
||||||
|
@ -264,6 +264,7 @@ public class ProviderMeta {
|
||||||
public static final String CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI = "user_status_supports_emoji";
|
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_GROUPFOLDERS = "groupfolders";
|
||||||
public static final String CAPABILITIES_DROP_ACCOUNT = "drop_account";
|
public static final String CAPABILITIES_DROP_ACCOUNT = "drop_account";
|
||||||
|
public static final String CAPABILITIES_SECURITY_GUARD = "security_guard";
|
||||||
|
|
||||||
//Columns of Uploads table
|
//Columns of Uploads table
|
||||||
public static final String UPLOADS_LOCAL_PATH = "local_path";
|
public static final String UPLOADS_LOCAL_PATH = "local_path";
|
||||||
|
|
|
@ -163,7 +163,9 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
|
||||||
serializedFolderMetadata,
|
serializedFolderMetadata,
|
||||||
token,
|
token,
|
||||||
client,
|
client,
|
||||||
metadataExists);
|
metadataExists,
|
||||||
|
arbitraryDataProvider,
|
||||||
|
user);
|
||||||
|
|
||||||
// unlock folder
|
// unlock folder
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import android.text.TextUtils;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
import com.nextcloud.client.account.User;
|
import com.nextcloud.client.account.User;
|
||||||
|
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
|
||||||
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
|
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
|
||||||
import com.owncloud.android.datamodel.FileDataStorageManager;
|
import com.owncloud.android.datamodel.FileDataStorageManager;
|
||||||
import com.owncloud.android.datamodel.OCFile;
|
import com.owncloud.android.datamodel.OCFile;
|
||||||
|
@ -213,7 +214,12 @@ public class DownloadFileOperation extends RemoteOperation {
|
||||||
.get(file.getEncryptedFileName()).getAuthenticationTag());
|
.get(file.getEncryptedFileName()).getAuthenticationTag());
|
||||||
|
|
||||||
try {
|
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)) {
|
try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
|
||||||
fileOutputStream.write(decryptedBytes);
|
fileOutputStream.write(decryptedBytes);
|
||||||
|
|
|
@ -638,7 +638,9 @@ public class UploadFileOperation extends SyncOperation {
|
||||||
serializedFolderMetadata,
|
serializedFolderMetadata,
|
||||||
token,
|
token,
|
||||||
client,
|
client,
|
||||||
metadataExists);
|
metadataExists,
|
||||||
|
arbitraryDataProvider,
|
||||||
|
user);
|
||||||
|
|
||||||
// unlock
|
// unlock
|
||||||
result = EncryptionUtils.unlockFolder(parentFile, client, token);
|
result = EncryptionUtils.unlockFolder(parentFile, client, token);
|
||||||
|
|
|
@ -223,6 +223,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
|
||||||
val secondKey = EncryptionUtils.decodeStringToBase64Bytes(decryptedString)
|
val secondKey = EncryptionUtils.decodeStringToBase64Bytes(decryptedString)
|
||||||
|
|
||||||
if (!Arrays.equals(firstKey, secondKey)) {
|
if (!Arrays.equals(firstKey, secondKey)) {
|
||||||
|
EncryptionUtils.reportE2eError(arbitraryDataProvider, user)
|
||||||
throw Exception("Keys do not match")
|
throw Exception("Keys do not match")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,6 +405,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
publicKeyString = result.data[0] as String
|
publicKeyString = result.data[0] as String
|
||||||
if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) {
|
if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) {
|
||||||
|
EncryptionUtils.reportE2eError(arbitraryDataProvider, user)
|
||||||
throw RuntimeException("Wrong CSR returned")
|
throw RuntimeException("Wrong CSR returned")
|
||||||
}
|
}
|
||||||
Log_OC.d(TAG, "public key success")
|
Log_OC.d(TAG, "public key success")
|
||||||
|
|
|
@ -125,8 +125,8 @@ class GroupfolderListFragment : OCFileListFragment(), Injectable, GroupfolderLis
|
||||||
val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context)
|
val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context)
|
||||||
if (!fetchResult.isSuccess) {
|
if (!fetchResult.isSuccess) {
|
||||||
logger.e(SHARED_TAG, "Error fetching file")
|
logger.e(SHARED_TAG, "Error fetching file")
|
||||||
if (fetchResult.isException) {
|
if (fetchResult.isException && fetchResult.exception != null) {
|
||||||
logger.e(SHARED_TAG, "exception: ", fetchResult.exception)
|
logger.e(SHARED_TAG, "exception: ", fetchResult.exception!!)
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1766,7 +1766,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
||||||
serializedFolderMetadata,
|
serializedFolderMetadata,
|
||||||
token,
|
token,
|
||||||
client,
|
client,
|
||||||
metadataExists);
|
metadataExists,
|
||||||
|
arbitraryDataProvider,
|
||||||
|
user);
|
||||||
|
|
||||||
// unlock folder
|
// unlock folder
|
||||||
EncryptionUtils.unlockFolder(folder, client, token);
|
EncryptionUtils.unlockFolder(folder, client, token);
|
||||||
|
|
|
@ -87,8 +87,8 @@ class SharedListFragment : OCFileListFragment(), Injectable {
|
||||||
val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context)
|
val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context)
|
||||||
if (!fetchResult.isSuccess) {
|
if (!fetchResult.isSuccess) {
|
||||||
logger.e(SHARED_TAG, "Error fetching file")
|
logger.e(SHARED_TAG, "Error fetching file")
|
||||||
if (fetchResult.isException) {
|
if (fetchResult.isException && fetchResult.exception != null) {
|
||||||
logger.e(SHARED_TAG, "exception: ", fetchResult.exception)
|
logger.e(SHARED_TAG, "exception: ", fetchResult.exception!!)
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -47,6 +47,8 @@ import com.owncloud.android.lib.resources.e2ee.StoreMetadataRemoteOperation;
|
||||||
import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation;
|
import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation;
|
||||||
import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation;
|
import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation;
|
||||||
import com.owncloud.android.lib.resources.status.NextcloudVersion;
|
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 com.owncloud.android.operations.UploadException;
|
||||||
|
|
||||||
import org.apache.commons.httpclient.HttpStatus;
|
import org.apache.commons.httpclient.HttpStatus;
|
||||||
|
@ -326,10 +328,12 @@ public final class EncryptionUtils {
|
||||||
|
|
||||||
if (TextUtils.isEmpty(decryptedFolderChecksum) &&
|
if (TextUtils.isEmpty(decryptedFolderChecksum) &&
|
||||||
isFolderMigrated(remoteId, user, arbitraryDataProvider)) {
|
isFolderMigrated(remoteId, user, arbitraryDataProvider)) {
|
||||||
|
reportE2eError(arbitraryDataProvider, user);
|
||||||
throw new IllegalStateException("Possible downgrade attack detected!");
|
throw new IllegalStateException("Possible downgrade attack detected!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(decryptedFolderChecksum) && !decryptedFolderChecksum.equals(checksum)) {
|
if (!TextUtils.isEmpty(decryptedFolderChecksum) && !decryptedFolderChecksum.equals(checksum)) {
|
||||||
|
reportE2eError(arbitraryDataProvider, user);
|
||||||
throw new IllegalStateException("Wrong checksum!");
|
throw new IllegalStateException("Wrong checksum!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,7 +353,9 @@ public final class EncryptionUtils {
|
||||||
encryptedFile.getEncrypted(),
|
encryptedFile.getEncrypted(),
|
||||||
decodeStringToBase64Bytes(encryptedKey),
|
decodeStringToBase64Bytes(encryptedKey),
|
||||||
decodeStringToBase64Bytes(encryptedFile.getEncryptedInitializationVector()),
|
decodeStringToBase64Bytes(encryptedFile.getEncryptedInitializationVector()),
|
||||||
decodeStringToBase64Bytes(encryptedFile.getEncryptedTag())
|
decodeStringToBase64Bytes(encryptedFile.getEncryptedTag()),
|
||||||
|
arbitraryDataProvider,
|
||||||
|
user
|
||||||
);
|
);
|
||||||
|
|
||||||
DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
|
DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
|
||||||
|
@ -430,7 +436,9 @@ public final class EncryptionUtils {
|
||||||
serializedFolderMetadata,
|
serializedFolderMetadata,
|
||||||
token,
|
token,
|
||||||
client,
|
client,
|
||||||
true);
|
true,
|
||||||
|
arbitraryDataProvider,
|
||||||
|
user);
|
||||||
|
|
||||||
// unlock folder
|
// unlock folder
|
||||||
RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token);
|
RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token);
|
||||||
|
@ -534,7 +542,12 @@ public final class EncryptionUtils {
|
||||||
* @param authenticationTag authenticationTag from metadata
|
* @param authenticationTag authenticationTag from metadata
|
||||||
* @return decrypted byte[]
|
* @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,
|
throws NoSuchAlgorithmException,
|
||||||
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
|
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
|
||||||
BadPaddingException, IllegalBlockSizeException, IOException {
|
BadPaddingException, IllegalBlockSizeException, IOException {
|
||||||
|
@ -554,6 +567,7 @@ public final class EncryptionUtils {
|
||||||
fileBytes.length - (128 / 8), fileBytes.length);
|
fileBytes.length - (128 / 8), fileBytes.length);
|
||||||
|
|
||||||
if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
|
if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
|
||||||
|
reportE2eError(arbitraryDataProvider, user);
|
||||||
throw new SecurityException("Tag not correct");
|
throw new SecurityException("Tag not correct");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -713,7 +727,9 @@ public final class EncryptionUtils {
|
||||||
public static String decryptStringSymmetric(String string,
|
public static String decryptStringSymmetric(String string,
|
||||||
byte[] encryptionKeyBytes,
|
byte[] encryptionKeyBytes,
|
||||||
byte[] iv,
|
byte[] iv,
|
||||||
byte[] authenticationTag)
|
byte[] authenticationTag,
|
||||||
|
ArbitraryDataProvider arbitraryDataProvider,
|
||||||
|
User user)
|
||||||
throws NoSuchPaddingException,
|
throws NoSuchPaddingException,
|
||||||
NoSuchAlgorithmException,
|
NoSuchAlgorithmException,
|
||||||
InvalidAlgorithmParameterException,
|
InvalidAlgorithmParameterException,
|
||||||
|
@ -733,6 +749,7 @@ public final class EncryptionUtils {
|
||||||
bytes.length);
|
bytes.length);
|
||||||
|
|
||||||
if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
|
if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
|
||||||
|
reportE2eError(arbitraryDataProvider, user);
|
||||||
throw new SecurityException("Tag not correct");
|
throw new SecurityException("Tag not correct");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1057,7 +1074,7 @@ public final class EncryptionUtils {
|
||||||
|
|
||||||
return new Pair<>(Boolean.FALSE, metadata);
|
return new Pair<>(Boolean.FALSE, metadata);
|
||||||
} else {
|
} else {
|
||||||
// TODO error
|
reportE2eError(arbitraryDataProvider, user);
|
||||||
throw new UploadException("something wrong");
|
throw new UploadException("something wrong");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1066,7 +1083,9 @@ public final class EncryptionUtils {
|
||||||
String serializedFolderMetadata,
|
String serializedFolderMetadata,
|
||||||
String token,
|
String token,
|
||||||
OwnCloudClient client,
|
OwnCloudClient client,
|
||||||
boolean metadataExists) throws UploadException {
|
boolean metadataExists,
|
||||||
|
ArbitraryDataProvider arbitraryDataProvider,
|
||||||
|
User user) throws UploadException {
|
||||||
RemoteOperationResult uploadMetadataOperationResult;
|
RemoteOperationResult uploadMetadataOperationResult;
|
||||||
if (metadataExists) {
|
if (metadataExists) {
|
||||||
// update metadata
|
// update metadata
|
||||||
|
@ -1081,6 +1100,7 @@ public final class EncryptionUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uploadMetadataOperationResult.isSuccess()) {
|
if (!uploadMetadataOperationResult.isSuccess()) {
|
||||||
|
reportE2eError(arbitraryDataProvider, user);
|
||||||
throw new UploadException("Storing/updating metadata was not successful");
|
throw new UploadException("Storing/updating metadata was not successful");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1207,4 +1227,37 @@ public final class EncryptionUtils {
|
||||||
|
|
||||||
return arrayList.contains(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue