mirror of
https://github.com/nextcloud/android.git
synced 2024-11-21 12:45:32 +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())
|
||||
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
|
||||
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");
|
||||
FileOutputStream fileOutputStream1 = new FileOutputStream(decryptedFile);
|
||||
|
|
|
@ -68,7 +68,8 @@ import com.owncloud.android.db.ProviderMeta
|
|||
AutoMigration(from = 71, to = 72),
|
||||
AutoMigration(from = 72, to = 73),
|
||||
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
|
||||
)
|
||||
|
|
|
@ -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<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)
|
||||
val groupfolders: Int?,
|
||||
@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
|
||||
UploadsStorageManager uploadsStorageManager(Context context,
|
||||
CurrentAccountProvider currentAccountProvider) {
|
||||
UploadsStorageManager uploadsStorageManager(CurrentAccountProvider currentAccountProvider,
|
||||
Context context) {
|
||||
return new UploadsStorageManager(currentAccountProvider, context.getContentResolver());
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,4 +147,6 @@ interface BackgroundJobManager {
|
|||
|
||||
fun pruneJobs()
|
||||
fun cancelAllJobs()
|
||||
fun schedulePeriodicHealthStatus()
|
||||
fun startHealthStatus()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
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.startMediaFoldersDetectionJob();
|
||||
|
||||
backgroundJobManager.schedulePeriodicHealthStatus();
|
||||
|
||||
registerGlobalPassCodeProtection();
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,17 @@ public class ArbitraryDataProviderImpl implements ArbitraryDataProvider {
|
|||
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));
|
||||
|
|
|
@ -1954,6 +1954,7 @@ public class FileDataStorageManager {
|
|||
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 @@ public class FileDataStorageManager {
|
|||
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 class FileDataStorageManager {
|
|||
return user;
|
||||
}
|
||||
|
||||
public OCFile getDefaultRootPath(){
|
||||
public OCFile getDefaultRootPath() {
|
||||
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));
|
||||
}
|
||||
|
||||
public OCUpload[] getUploadsForAccount(final @NonNull String accountName) {
|
||||
return getUploads(ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", accountName);
|
||||
}
|
||||
|
||||
public OCUpload[] getFinishedUploadsForCurrentAccount() {
|
||||
User user = currentAccountProvider.getUser();
|
||||
|
||||
|
@ -586,14 +590,14 @@ public class UploadsStorageManager extends Observable {
|
|||
User user = currentAccountProvider.getUser();
|
||||
|
||||
return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.LOCK_FAILED.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.LOCK_FAILED.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?",
|
||||
user.getAccountName());
|
||||
}
|
||||
|
@ -624,15 +628,15 @@ public class UploadsStorageManager extends Observable {
|
|||
public long clearFailedButNotDelayedUploads() {
|
||||
User user = currentAccountProvider.getUser();
|
||||
final long deleted = getDB().delete(
|
||||
ProviderTableMeta.CONTENT_URI_UPLOADS,
|
||||
ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.LOCK_FAILED.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
ProviderTableMeta.CONTENT_URI_UPLOADS,
|
||||
ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.LOCK_FAILED.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
|
||||
"<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() +
|
||||
AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?",
|
||||
new String[]{user.getAccountName()}
|
||||
|
@ -647,10 +651,10 @@ public class UploadsStorageManager extends Observable {
|
|||
public long clearSuccessfulUploads() {
|
||||
User user = currentAccountProvider.getUser();
|
||||
final long deleted = getDB().delete(
|
||||
ProviderTableMeta.CONTENT_URI_UPLOADS,
|
||||
ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND +
|
||||
ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{user.getAccountName()}
|
||||
);
|
||||
ProviderTableMeta.CONTENT_URI_UPLOADS,
|
||||
ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND +
|
||||
ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{user.getAccountName()}
|
||||
);
|
||||
|
||||
Log_OC.d(TAG, "delete all successful uploads");
|
||||
if (deleted > 0) {
|
||||
|
|
|
@ -35,7 +35,7 @@ import java.util.List;
|
|||
*/
|
||||
public class ProviderMeta {
|
||||
public static final String DB_NAME = "filelist";
|
||||
public static final int DB_VERSION = 75;
|
||||
public static final int DB_VERSION = 76;
|
||||
|
||||
private ProviderMeta() {
|
||||
// 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_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";
|
||||
|
|
|
@ -163,7 +163,9 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
|
|||
serializedFolderMetadata,
|
||||
token,
|
||||
client,
|
||||
metadataExists);
|
||||
metadataExists,
|
||||
arbitraryDataProvider,
|
||||
user);
|
||||
|
||||
// unlock folder
|
||||
if (token != null) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import android.text.TextUtils;
|
|||
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 @@ public class DownloadFileOperation extends RemoteOperation {
|
|||
.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);
|
||||
|
|
|
@ -638,7 +638,9 @@ public class UploadFileOperation extends SyncOperation {
|
|||
serializedFolderMetadata,
|
||||
token,
|
||||
client,
|
||||
metadataExists);
|
||||
metadataExists,
|
||||
arbitraryDataProvider,
|
||||
user);
|
||||
|
||||
// unlock
|
||||
result = EncryptionUtils.unlockFolder(parentFile, client, token);
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1766,7 +1766,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
serializedFolderMetadata,
|
||||
token,
|
||||
client,
|
||||
metadataExists);
|
||||
metadataExists,
|
||||
arbitraryDataProvider,
|
||||
user);
|
||||
|
||||
// unlock folder
|
||||
EncryptionUtils.unlockFolder(folder, client, token);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.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 final class EncryptionUtils {
|
|||
|
||||
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 final class EncryptionUtils {
|
|||
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 @@ public final class EncryptionUtils {
|
|||
serializedFolderMetadata,
|
||||
token,
|
||||
client,
|
||||
true);
|
||||
true,
|
||||
arbitraryDataProvider,
|
||||
user);
|
||||
|
||||
// unlock folder
|
||||
RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token);
|
||||
|
@ -534,7 +542,12 @@ public final class EncryptionUtils {
|
|||
* @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 final class EncryptionUtils {
|
|||
fileBytes.length - (128 / 8), fileBytes.length);
|
||||
|
||||
if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
|
||||
reportE2eError(arbitraryDataProvider, user);
|
||||
throw new SecurityException("Tag not correct");
|
||||
}
|
||||
|
||||
|
@ -713,7 +727,9 @@ public final class EncryptionUtils {
|
|||
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 final class EncryptionUtils {
|
|||
bytes.length);
|
||||
|
||||
if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
|
||||
reportE2eError(arbitraryDataProvider, user);
|
||||
throw new SecurityException("Tag not correct");
|
||||
}
|
||||
|
||||
|
@ -1057,7 +1074,7 @@ public final class EncryptionUtils {
|
|||
|
||||
return new Pair<>(Boolean.FALSE, metadata);
|
||||
} else {
|
||||
// TODO error
|
||||
reportE2eError(arbitraryDataProvider, user);
|
||||
throw new UploadException("something wrong");
|
||||
}
|
||||
}
|
||||
|
@ -1066,7 +1083,9 @@ public final class EncryptionUtils {
|
|||
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 final class EncryptionUtils {
|
|||
}
|
||||
|
||||
if (!uploadMetadataOperationResult.isSuccess()) {
|
||||
reportE2eError(arbitraryDataProvider, user);
|
||||
throw new UploadException("Storing/updating metadata was not successful");
|
||||
}
|
||||
}
|
||||
|
@ -1207,4 +1227,37 @@ public final class EncryptionUtils {
|
|||
|
||||
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