Report client health

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
tobiasKaminsky 2023-10-06 07:23:48 +02:00
parent 63ec726dbc
commit b336d01b4b
No known key found for this signature in database
GPG key ID: 0E00D4D47D0C5AF7
25 changed files with 1522 additions and 44 deletions

File diff suppressed because it is too large Load diff

View file

@ -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))
}
} }

View file

@ -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);

View file

@ -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
) )

View file

@ -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>
} }

View file

@ -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?
) )

View file

@ -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());
} }

View file

@ -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
)
}
} }

View file

@ -147,4 +147,6 @@ interface BackgroundJobManager {
fun pruneJobs() fun pruneJobs()
fun cancelAllJobs() fun cancelAllJobs()
fun schedulePeriodicHealthStatus()
fun startHealthStatus()
} }

View file

@ -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
)
}
} }

View 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"
}
}

View file

@ -349,6 +349,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
backgroundJobManager.scheduleMediaFoldersDetectionJob(); backgroundJobManager.scheduleMediaFoldersDetectionJob();
backgroundJobManager.startMediaFoldersDetectionJob(); backgroundJobManager.startMediaFoldersDetectionJob();
backgroundJobManager.schedulePeriodicHealthStatus();
registerGlobalPassCodeProtection(); registerGlobalPassCodeProtection();
} }

View file

@ -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"
} }
} }

View file

@ -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));

View file

@ -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;
}
} }

View file

@ -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();

View file

@ -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";

View file

@ -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) {

View file

@ -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);

View file

@ -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);

View file

@ -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")

View file

@ -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 {

View file

@ -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);

View file

@ -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 {

View file

@ -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;
}
}
} }