From 36fd5b2a8aefbdd7c419e3d047d24525dbbee71e Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 9 Mar 2021 13:47:04 +0100 Subject: [PATCH 001/135] Refactor unlock and cloud authentication from UseCase to Activity --- .../data/cloud/crypto/CryptoCloudFactory.java | 9 +- .../CloudAuthenticationService.java | 13 - presentation/src/main/AndroidManifest.xml | 5 +- .../di/component/ActivityComponent.java | 6 + .../intent/UnlockVaultIntent.java | 21 ++ .../presentation/model/VaultModel.kt | 2 + .../AutoUploadChooseVaultPresenter.kt | 62 +-- .../BiometricAuthSettingsPresenter.kt | 148 +++----- .../presenter/SharedFilesPresenter.kt | 89 ++--- .../presenter/UnlockVaultPresenter.kt | 357 ++++++++++++++++++ .../presenter/VaultListPresenter.kt | 286 +------------- .../activity/AutoUploadChooseVaultActivity.kt | 47 +-- .../activity/BiometricAuthSettingsActivity.kt | 49 --- .../ui/activity/SharedFilesActivity.kt | 45 +-- .../ui/activity/UnlockVaultActivity.kt | 109 ++++++ .../ui/activity/VaultListActivity.kt | 75 +--- .../view/AutoUploadChooseVaultView.kt | 2 - .../view/BiometricAuthSettingsView.kt | 3 - .../ui/activity/view/SharedFilesView.kt | 2 - .../ui/activity/view/UnlockVaultView.kt | 16 + .../ui/activity/view/VaultListView.kt | 7 - .../ui/callback/VaultListCallback.kt | 7 +- .../ui/fragment/UnlockVaultFragment.kt | 17 + .../main/res/layout/activity_unlock_vault.xml | 15 + .../main/res/layout/fragment_unlock_vault.xml | 7 + presentation/src/main/res/values/styles.xml | 4 + .../presenter/VaultListPresenterTest.java | 52 --- 27 files changed, 674 insertions(+), 781 deletions(-) delete mode 100644 domain/src/main/java/org/cryptomator/domain/repository/CloudAuthenticationService.java create mode 100644 presentation/src/main/java/org/cryptomator/presentation/intent/UnlockVaultIntent.java create mode 100644 presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/UnlockVaultView.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/fragment/UnlockVaultFragment.kt create mode 100644 presentation/src/main/res/layout/activity_unlock_vault.xml create mode 100644 presentation/src/main/res/layout/fragment_unlock_vault.xml diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java index 876ec599..de46b4d4 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java @@ -90,11 +90,14 @@ public class CryptoCloudFactory { throw new CancellationException(); } - cryptoCloudContentRepositoryFactory.registerCryptor(impl.getVault(), cryptor); - - return aCopyOf(token.getVault()) // + Vault vault = aCopyOf(token.getVault()) // .withVersion(impl.getKeyFile().getVersion()) // + .withUnlocked(true) // .build(); + + cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor); + + return vault; } public UnlockTokenImpl createUnlockToken(Vault vault) throws BackendException { diff --git a/domain/src/main/java/org/cryptomator/domain/repository/CloudAuthenticationService.java b/domain/src/main/java/org/cryptomator/domain/repository/CloudAuthenticationService.java deleted file mode 100644 index 3427075f..00000000 --- a/domain/src/main/java/org/cryptomator/domain/repository/CloudAuthenticationService.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.cryptomator.domain.repository; - -import org.cryptomator.domain.Cloud; -import org.cryptomator.domain.exception.BackendException; - -public interface CloudAuthenticationService { - - boolean isAuthenticated(Cloud cloud) throws BackendException; - - boolean canAuthenticate(Cloud cloud); - - Cloud updateAuthenticatedCloud(Cloud cloud); -} diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index 882a5e27..bd460eef 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -97,10 +97,11 @@ - + - diff --git a/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java b/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java index 673b0d2e..b66a81a3 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java +++ b/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java @@ -21,6 +21,7 @@ import org.cryptomator.presentation.ui.activity.SettingsActivity; import org.cryptomator.presentation.ui.activity.SharedFilesActivity; import org.cryptomator.presentation.ui.activity.SplashActivity; import org.cryptomator.presentation.ui.activity.TextEditorActivity; +import org.cryptomator.presentation.ui.activity.UnlockVaultActivity; import org.cryptomator.presentation.ui.activity.VaultListActivity; import org.cryptomator.presentation.ui.activity.WebDavAddOrChangeActivity; import org.cryptomator.presentation.ui.fragment.AutoUploadChooseVaultFragment; @@ -34,6 +35,7 @@ import org.cryptomator.presentation.ui.fragment.ImagePreviewFragment; import org.cryptomator.presentation.ui.fragment.SetPasswordFragment; import org.cryptomator.presentation.ui.fragment.SharedFilesFragment; import org.cryptomator.presentation.ui.fragment.TextEditorFragment; +import org.cryptomator.presentation.ui.fragment.UnlockVaultFragment; import org.cryptomator.presentation.ui.fragment.VaultListFragment; import org.cryptomator.presentation.ui.fragment.WebDavAddOrChangeFragment; import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow; @@ -114,4 +116,8 @@ public interface ActivityComponent { void inject(AutoUploadChooseVaultFragment autoUploadChooseVaultFragment); void inject(LicenseCheckActivity licenseCheckActivity); + + void inject(UnlockVaultActivity unlockVaultActivity); + + void inject(UnlockVaultFragment unlockVaultFragment); } diff --git a/presentation/src/main/java/org/cryptomator/presentation/intent/UnlockVaultIntent.java b/presentation/src/main/java/org/cryptomator/presentation/intent/UnlockVaultIntent.java new file mode 100644 index 00000000..8d6f5f89 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/intent/UnlockVaultIntent.java @@ -0,0 +1,21 @@ +package org.cryptomator.presentation.intent; + +import org.cryptomator.generator.Intent; +import org.cryptomator.presentation.model.VaultModel; +import org.cryptomator.presentation.ui.activity.UnlockVaultActivity; + +@Intent(UnlockVaultActivity.class) +public interface UnlockVaultIntent { + + VaultModel vaultModel(); + + VaultAction vaultAction(); + + enum VaultAction { + UNLOCK, + UNLOCK_FOR_BIOMETRIC_AUTH, + ENCRYPT_PASSWORD, + CHANGE_PASSWORD + } + +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt index f15f1c50..ed8e6e96 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt @@ -15,6 +15,8 @@ class VaultModel(private val vault: Vault) : Serializable { get() = !vault.isUnlocked val position: Int get() = vault.position + val version: Int + get() = vault.version fun toVault(): Vault { return vault diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt index fe9f6630..52d41780 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/AutoUploadChooseVaultPresenter.kt @@ -7,17 +7,14 @@ import org.cryptomator.domain.di.PerView import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase import org.cryptomator.domain.usecases.vault.GetVaultListUseCase -import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase -import org.cryptomator.domain.usecases.vault.UnlockVaultUseCase -import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken import org.cryptomator.generator.Callback import org.cryptomator.presentation.R import org.cryptomator.presentation.exception.ExceptionHandlers import org.cryptomator.presentation.intent.ChooseCloudNodeSettings import org.cryptomator.presentation.intent.Intents +import org.cryptomator.presentation.intent.UnlockVaultIntent import org.cryptomator.presentation.model.CloudFolderModel import org.cryptomator.presentation.model.CloudModel -import org.cryptomator.presentation.model.ProgressModel import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper import org.cryptomator.presentation.ui.activity.view.AutoUploadChooseVaultView @@ -32,8 +29,6 @@ class AutoUploadChooseVaultPresenter @Inject constructor( // private val getVaultListUseCase: GetVaultListUseCase, // private val getRootFolderUseCase: GetRootFolderUseCase, // private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, // - private val unlockVaultUseCase: UnlockVaultUseCase, // - private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, // private val cloudFolderModelMapper: CloudFolderModelMapper, // private val sharedPreferencesHandler: SharedPreferencesHandler, // private val authenticationExceptionHandler: AuthenticationExceptionHandler, // @@ -83,11 +78,24 @@ class AutoUploadChooseVaultPresenter @Inject constructor( // decryptedCloudFor(authenticatedVault) } else { if (!isPaused) { - view?.showEnterPasswordDialog(VaultModel(authenticatedVault)) + requestActivityResult( // + ActivityResultCallbacks.vaultUnlockedAutoUpload(), // + Intents.unlockVaultIntent().withVaultModel(VaultModel(authenticatedVault)).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK)) } } } + @Callback + fun vaultUnlockedAutoUpload(result: ActivityResult) { + val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud + when { + result.isResultOk -> rootFolderFor(cloud) + else -> TODO("Not yet implemented") + } + } + + + private fun decryptedCloudFor(vault: Vault) { getDecryptedCloudForVaultUseCase // .withVault(vault) // @@ -151,49 +159,11 @@ class AutoUploadChooseVaultPresenter @Inject constructor( // location?.let { view?.showChosenLocation(it) } } - fun onUnlockCanceled() { - unlockVaultUseCase.cancel() - } - - fun onUnlockPressed(vaultModel: VaultModel, password: String?) { - view?.showProgress(ProgressModel.GENERIC) - unlockVaultUseCase // - .withVaultOrUnlockToken(VaultOrUnlockToken.from(vaultModel.toVault())) // - .andPassword(password) // - .run(object : DefaultResultHandler() { - override fun onSuccess(cloud: Cloud) { - view?.showProgress(ProgressModel.COMPLETED) - rootFolderFor(cloud) - } - - override fun onError(e: Throwable) { - if (!authenticationExceptionHandler.handleAuthenticationException( // - this@AutoUploadChooseVaultPresenter, // - e, // - ActivityResultCallbacks.unlockVaultAfterAuth(vaultModel.toVault(), password))) { - showError(e) - } - } - }) - } - - fun onBiometricKeyInvalidated(vaultModel: VaultModel?) { - removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler() { - override fun onSuccess(void: Void?) { - view?.showBiometricAuthKeyInvalidatedDialog() - } - }) - } - - fun useConfirmationInFaceUnlockBiometricAuthentication(): Boolean { - return sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication() - } - enum class AuthenticationState { CHOOSE_LOCATION, INIT_ROOT } init { - unsubscribeOnDestroy(getVaultListUseCase) + unsubscribeOnDestroy(getVaultListUseCase, getRootFolderUseCase, getDecryptedCloudForVaultUseCase) } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt index 4a3ee4fe..e4673511 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt @@ -2,21 +2,21 @@ package org.cryptomator.presentation.presenter import android.content.Intent import android.provider.Settings -import org.cryptomator.cryptolib.api.InvalidPassphraseException import org.cryptomator.domain.Cloud import org.cryptomator.domain.Vault import org.cryptomator.domain.di.PerView -import org.cryptomator.domain.usecases.vault.* +import org.cryptomator.domain.usecases.vault.GetVaultListUseCase +import org.cryptomator.domain.usecases.vault.LockVaultUseCase +import org.cryptomator.domain.usecases.vault.SaveVaultUseCase import org.cryptomator.generator.Callback import org.cryptomator.presentation.exception.ExceptionHandlers -import org.cryptomator.presentation.model.CloudModel -import org.cryptomator.presentation.model.ProgressModel +import org.cryptomator.presentation.intent.Intents +import org.cryptomator.presentation.intent.UnlockVaultIntent import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.ui.activity.view.BiometricAuthSettingsView import org.cryptomator.presentation.workflow.ActivityResult -import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler import org.cryptomator.util.SharedPreferencesHandler -import java.util.* +import java.util.ArrayList import javax.inject.Inject import timber.log.Timber @@ -24,13 +24,9 @@ import timber.log.Timber class BiometricAuthSettingsPresenter @Inject constructor( // private val getVaultListUseCase: GetVaultListUseCase, // private val saveVaultUseCase: SaveVaultUseCase, // - private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, // - private val checkVaultPasswordUseCase: CheckVaultPasswordUseCase, // - private val unlockVaultUseCase: UnlockVaultUseCase, // private val lockVaultUseCase: LockVaultUseCase, // exceptionMappings: ExceptionHandlers, // - private val sharedPreferencesHandler: SharedPreferencesHandler, // - private val authenticationExceptionHandler: AuthenticationExceptionHandler) : Presenter(exceptionMappings) { + private val sharedPreferencesHandler: SharedPreferencesHandler) : Presenter(exceptionMappings) { fun loadVaultList() { updateVaultListView() @@ -49,92 +45,56 @@ class BiometricAuthSettingsPresenter @Inject constructor( // fun updateVaultEntityWithChangedBiometricAuthSettings(vaultModel: VaultModel, useBiometricAuth: Boolean) { if (useBiometricAuth) { - view?.showEnterPasswordDialog(VaultModel(vaultModel.toVault())) + verifyPassword(vaultModel) } else { removePasswordAndSave(vaultModel.toVault()) } } - fun verifyPassword(vaultModel: VaultModel) { + private fun verifyPassword(vaultModel: VaultModel) { Timber.tag("BiomtricAuthSettngsPres").i("Checking entered vault password") if (vaultModel.isLocked) { - unlockVault(vaultModel) + requestActivityResult( // + ActivityResultCallbacks.vaultUnlockedBiometricAuthPres(vaultModel), // + Intents.unlockVaultIntent().withVaultModel(vaultModel).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH)) } else { - checkPassword(vaultModel) + lockVaultUseCase + .withVault(vaultModel.toVault()) + .run(object : DefaultResultHandler() { + override fun onSuccess(vault: Vault) { + super.onSuccess(vault) + requestActivityResult( // + ActivityResultCallbacks.vaultUnlockedBiometricAuthPres(vaultModel), // + Intents.unlockVaultIntent().withVaultModel(vaultModel).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH)) + } + }) } } - private fun checkPassword(vaultModel: VaultModel) { - view?.showProgress(ProgressModel.GENERIC) - checkVaultPasswordUseCase // - .withVault(vaultModel.toVault()) // - .andPassword(vaultModel.password) // - .run(object : DefaultResultHandler() { - override fun onSuccess(passwordCorrect: Boolean) { - if (passwordCorrect) { - Timber.tag("BiomtricAuthSettngsPres").i("Password is correct") - onPasswordCheckSucceeded(vaultModel) - } else { - Timber.tag("BiomtricAuthSettngsPres").i("Password is wrong") - showError(InvalidPassphraseException()) - } - } - - override fun onError(e: Throwable) { - super.onError(e) - Timber.tag("BiomtricAuthSettngsPres").e(e, "Password check failed") - } - }) - } - - private fun unlockVault(vaultModel: VaultModel) { - view?.showProgress(ProgressModel.GENERIC) - unlockVaultUseCase // - .withVaultOrUnlockToken(VaultOrUnlockToken.from(vaultModel.toVault())) // - .andPassword(vaultModel.password) // - .run(object : DefaultResultHandler() { - override fun onSuccess(cloud: Cloud) { - Timber.tag("BiomtricAuthSettngsPres").i("Password is correct") - onUnlockSucceeded(vaultModel) - } - - override fun onError(e: Throwable) { - if (!authenticationExceptionHandler.handleAuthenticationException(this@BiometricAuthSettingsPresenter, e, ActivityResultCallbacks.unlockVaultAfterAuth(vaultModel.toVault()))) { - showError(e) - Timber.tag("BiomtricAuthSettngsPres").e(e, "Password check failed") - } - } - }) - } - - private fun onUnlockSucceeded(vaultModel: VaultModel) { - lockVaultUseCase - .withVault(vaultModel.toVault()) - .run(object : DefaultResultHandler() { - override fun onSuccess(vault: Vault) { - super.onSuccess(vault) - onPasswordCheckSucceeded(vaultModel) - } - - override fun onError(e: Throwable) { - Timber.tag("BiomtricAuthSettngsPres").e(e, "Locking vault after unlocking failed but continue to save changes") - onPasswordCheckSucceeded(vaultModel) - } - }) + @Callback + fun vaultUnlockedBiometricAuthPres(result: ActivityResult, vaultModel: VaultModel) { + val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud + val password = result.intent().getStringExtra(UnlockVaultPresenter.PASSWORD) + val vault = Vault.aCopyOf(vaultModel.toVault()).withCloud(cloud).withSavedPassword(password).build() + when { + result.isResultOk -> requestActivityResult( // + ActivityResultCallbacks.encryptVaultPassword(vaultModel), // + Intents.unlockVaultIntent().withVaultModel(VaultModel(vault)).withVaultAction(UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD)) + else -> TODO("Not yet implemented") + } } @Callback - fun unlockVaultAfterAuth(result: ActivityResult, vault: Vault?) { - val cloud = result.getSingleResult(CloudModel::class.java).toCloud() - val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build() - unlockVault(VaultModel(vaultWithUpdatedCloud)) + fun encryptVaultPassword(result: ActivityResult, vaultModel: VaultModel) { + val tmpVault = result.intent().getSerializableExtra(SINGLE_RESULT) as VaultModel + val vault = Vault.aCopyOf(vaultModel.toVault()).withSavedPassword(tmpVault.password).build() + when { + result.isResultOk -> saveVault(vault) + else -> TODO("Not yet implemented") + } } - private fun onPasswordCheckSucceeded(vaultModel: VaultModel) { - view?.showBiometricAuthenticationDialog(vaultModel) - } - - fun saveVault(vault: Vault?) { + private fun saveVault(vault: Vault?) { saveVaultUseCase // .withVault(vault) // .run(object : ProgressCompletingResultHandler() { @@ -145,8 +105,7 @@ class BiometricAuthSettingsPresenter @Inject constructor( // } fun switchedGeneralBiometricAuthSettings(isChecked: Boolean) { - sharedPreferencesHandler // - .changeUseBiometricAuthentication(isChecked) + sharedPreferencesHandler.changeUseBiometricAuthentication(isChecked) if (isChecked) { loadVaultList() } else { @@ -173,32 +132,15 @@ class BiometricAuthSettingsPresenter @Inject constructor( // fun onSetupBiometricAuthInSystemClicked() { val openSecuritySettings = Intent(Settings.ACTION_SECURITY_SETTINGS) - requestActivityResult(ActivityResultCallbacks.onSetupFingerCompleted(), openSecuritySettings) + requestActivityResult(ActivityResultCallbacks.onSetupBiometricAuthInSystemCompleted(), openSecuritySettings) } @Callback - fun onSetupFingerCompleted(result: ActivityResult?) { + fun onSetupBiometricAuthInSystemCompleted(result: ActivityResult?) { view?.showSetupBiometricAuthDialog() } - fun onBiometricAuthKeyInvalidated(vaultModel: VaultModel?) { - removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler() { - override fun onSuccess(void: Void?) { - view?.showBiometricAuthKeyInvalidatedDialog() - } - }) - } - - fun onUnlockCanceled() { - unlockVaultUseCase.cancel() - loadVaultList() - } - - fun useConfirmationInFaceUnlockBiometricAuthentication(): Boolean { - return sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication() - } - init { - unsubscribeOnDestroy(getVaultListUseCase, saveVaultUseCase, checkVaultPasswordUseCase, removeStoredVaultPasswordsUseCase, unlockVaultUseCase) + unsubscribeOnDestroy(getVaultListUseCase, saveVaultUseCase, lockVaultUseCase) } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt index 7d2ee9e7..81931577 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/SharedFilesPresenter.kt @@ -8,15 +8,13 @@ import org.cryptomator.domain.di.PerView import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase import org.cryptomator.domain.usecases.cloud.* import org.cryptomator.domain.usecases.vault.GetVaultListUseCase -import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase -import org.cryptomator.domain.usecases.vault.UnlockVaultUseCase -import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken import org.cryptomator.generator.Callback import org.cryptomator.generator.InstanceState import org.cryptomator.presentation.R import org.cryptomator.presentation.exception.ExceptionHandlers import org.cryptomator.presentation.intent.ChooseCloudNodeSettings import org.cryptomator.presentation.intent.Intents +import org.cryptomator.presentation.intent.UnlockVaultIntent import org.cryptomator.presentation.model.* import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper import org.cryptomator.presentation.model.mappers.ProgressModelMapper @@ -27,7 +25,6 @@ import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler import org.cryptomator.presentation.workflow.PermissionsResult import org.cryptomator.util.Optional -import org.cryptomator.util.SharedPreferencesHandler import org.cryptomator.util.file.FileCacheUtils import java.util.* import javax.inject.Inject @@ -36,14 +33,11 @@ import timber.log.Timber @PerView class SharedFilesPresenter @Inject constructor( // private val getVaultListUseCase: GetVaultListUseCase, // - private val unlockVaultUseCase: UnlockVaultUseCase, // private val getRootFolderUseCase: GetRootFolderUseCase, // private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, // private val uploadFilesUseCase: UploadFilesUseCase, // private val getCloudListUseCase: GetCloudListUseCase, // - private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, // private val contentResolverUtil: ContentResolverUtil, // - private val sharedPreferencesHandler: SharedPreferencesHandler, // private val fileCacheUtils: FileCacheUtils, // private val authenticationExceptionHandler: AuthenticationExceptionHandler, // private val cloudFolderModelMapper: CloudFolderModelMapper, // @@ -128,6 +122,28 @@ class SharedFilesPresenter @Inject constructor( // vaultModel?.let { onCloudOfVaultAuthenticated(it.toVault()) } } + private fun onCloudOfVaultAuthenticated(authenticatedVault: Vault) { + if (authenticatedVault.isUnlocked) { + decryptedCloudFor(authenticatedVault) + } else { + if (!isPaused) { + requestActivityResult( // + ActivityResultCallbacks.vaultUnlockedSharedFiles(), // + Intents.unlockVaultIntent().withVaultModel(VaultModel(authenticatedVault)).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK)) + } + } + } + + + @Callback + fun vaultUnlockedSharedFiles(result: ActivityResult) { + val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud + when { + result.isResultOk -> rootFolderFor(cloud) + else -> TODO("Not yet implemented") + } + } + private fun decryptedCloudFor(vault: Vault) { getDecryptedCloudForVaultUseCase // .withVault(vault) // @@ -172,32 +188,6 @@ class SharedFilesPresenter @Inject constructor( // } } - fun onUnlockPressed(vaultModel: VaultModel, password: String?) { - view?.showProgress(ProgressModel.GENERIC) - unlockVaultUseCase // - .withVaultOrUnlockToken(VaultOrUnlockToken.from(vaultModel.toVault())) // - .andPassword(password) // - .run(object : DefaultResultHandler() { - override fun onSuccess(cloud: Cloud) { - view?.showProgress(ProgressModel.COMPLETED) - rootFolderFor(cloud) - } - - override fun onError(e: Throwable) { - if (!authenticationExceptionHandler.handleAuthenticationException(this@SharedFilesPresenter, e, ActivityResultCallbacks.unlockVaultAfterAuth(vaultModel.toVault(), password))) { - showError(e) - } - } - }) - } - - @Callback - fun unlockVaultAfterAuth(result: ActivityResult, vault: Vault?, password: String?) { - val cloud = result.getSingleResult(CloudModel::class.java).toCloud() - val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build() - onUnlockPressed(VaultModel(vaultWithUpdatedCloud), password) - } - private fun setLocation(location: CloudFolderModel) { this.location = location } @@ -372,16 +362,6 @@ class SharedFilesPresenter @Inject constructor( // } } - private fun onCloudOfVaultAuthenticated(authenticatedVault: Vault) { - if (authenticatedVault.isUnlocked) { - decryptedCloudFor(authenticatedVault) - } else { - if (!isPaused) { - view?.showEnterPasswordDialog(VaultModel(authenticatedVault)) - } - } - } - fun onChooseLocationPressed() { authenticate(selectedVault) } @@ -410,25 +390,6 @@ class SharedFilesPresenter @Inject constructor( // view?.closeDialog() } - fun onBiometricAuthKeyInvalidated() { - removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler() { - override fun onFinished() { - view?.showBiometricAuthKeyInvalidatedDialog() - } - }) - selectedVault?.let { - selectedVault = VaultModel(Vault.aCopyOf(it.toVault()).withSavedPassword(null).build()) - } - } - - fun onUnlockCanceled() { - unlockVaultUseCase.cancel() - } - - fun useConfirmationInFaceUnlockBiometricAuthentication(): Boolean { - return sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication() - } - private enum class AuthenticationState { CHOOSE_LOCATION, INIT_ROOT } @@ -444,11 +405,9 @@ class SharedFilesPresenter @Inject constructor( // init { unsubscribeOnDestroy( // getRootFolderUseCase, // - unlockVaultUseCase, // getVaultListUseCase, // getDecryptedCloudForVaultUseCase, // uploadFilesUseCase, // - getCloudListUseCase, // - removeStoredVaultPasswordsUseCase) + getCloudListUseCase) } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt new file mode 100644 index 00000000..61aad8d4 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -0,0 +1,357 @@ +package org.cryptomator.presentation.presenter + +import android.os.Handler +import androidx.biometric.BiometricManager +import org.cryptomator.domain.Cloud +import org.cryptomator.domain.Vault +import org.cryptomator.domain.di.PerView +import org.cryptomator.domain.exception.NetworkConnectionException +import org.cryptomator.domain.exception.authentication.AuthenticationException +import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase +import org.cryptomator.domain.usecases.vault.LockVaultUseCase +import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase +import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase +import org.cryptomator.domain.usecases.vault.SaveVaultUseCase +import org.cryptomator.domain.usecases.vault.UnlockToken +import org.cryptomator.domain.usecases.vault.UnlockVaultUseCase +import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken +import org.cryptomator.generator.Callback +import org.cryptomator.generator.InjectIntent +import org.cryptomator.presentation.R +import org.cryptomator.presentation.exception.ExceptionHandlers +import org.cryptomator.presentation.intent.UnlockVaultIntent +import org.cryptomator.presentation.model.CloudModel +import org.cryptomator.presentation.model.ProgressModel +import org.cryptomator.presentation.model.ProgressStateModel +import org.cryptomator.presentation.model.VaultModel +import org.cryptomator.presentation.ui.activity.view.UnlockVaultView +import org.cryptomator.presentation.workflow.ActivityResult +import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler +import org.cryptomator.util.SharedPreferencesHandler +import java.io.Serializable +import javax.inject.Inject +import timber.log.Timber + +@PerView +class UnlockVaultPresenter @Inject constructor( + private val changePasswordUseCase: ChangePasswordUseCase, + private val lockVaultUseCase: LockVaultUseCase, + private val unlockVaultUseCase: UnlockVaultUseCase, + private val prepareUnlockUseCase: PrepareUnlockUseCase, + private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, + private val saveVaultUseCase: SaveVaultUseCase, + private val authenticationExceptionHandler: AuthenticationExceptionHandler, + private val sharedPreferencesHandler: SharedPreferencesHandler, + exceptionMappings: ExceptionHandlers) : Presenter(exceptionMappings) { + + private var startedUsingPrepareUnlock = false + private var retryUnlockHandler: Handler? = null + private var pendingUnlock: PendingUnlock? = null + + @InjectIntent + lateinit var intent: UnlockVaultIntent + + @Volatile + private var running: Boolean = false + + override fun destroyed() { + super.destroyed() + if (retryUnlockHandler != null) { + running = false + retryUnlockHandler?.removeCallbacks(null) + } + } + + fun setup() { + when (intent.vaultAction()) { + UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD -> view?.getEncryptedPasswordWithBiometricAuthentication(intent.vaultModel()) + UnlockVaultIntent.VaultAction.UNLOCK, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> unlockVault(intent.vaultModel()) + UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(intent.vaultModel()) + else -> TODO("Not yet implemented") + } + } + + private fun unlockVault(vaultModel: VaultModel) { + if (canUseBiometricOn(vaultModel)) { + if (startedUsingPrepareUnlock) { + startPrepareUnlockUseCase(vaultModel.toVault()) + } + view?.showBiometricDialog(vaultModel) + } else { + startPrepareUnlockUseCase(vaultModel.toVault()) + view?.showEnterPasswordDialog(vaultModel) + } + } + + fun onWindowFocusChanged(hasFocus: Boolean) { + if (hasFocus) { + if (retryUnlockHandler != null) { + running = false + retryUnlockHandler?.removeCallbacks(null) + } + } + } + + private fun pendingUnlockFor(vault: Vault): PendingUnlock? { + if (pendingUnlock == null) { + pendingUnlock = PendingUnlock(vault) + } + return if (pendingUnlock?.belongsTo(vault) == true) { + pendingUnlock + } else { + PendingUnlock.NO_OP_PENDING_UNLOCK + } + } + + private fun canUseBiometricOn(vault: VaultModel): Boolean { + return vault.password != null && BiometricManager.from(context()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS + } + + fun onUnlockCanceled() { + prepareUnlockUseCase.unsubscribe() + unlockVaultUseCase.cancel() + finish() + } + + fun startPrepareUnlockUseCase(vault: Vault) { + pendingUnlock = null + prepareUnlockUseCase // + .withVault(vault) // + .run(object : DefaultResultHandler() { + override fun onSuccess(unlockToken: UnlockToken) { + if (!startedUsingPrepareUnlock && vault.password != null) { + doUnlock(unlockToken, vault.password) + } else { + unlockTokenObtained(unlockToken) + } + } + + override fun onError(e: Throwable) { + if (e is AuthenticationException) { + view?.cancelBasicAuthIfRunning() + } + if (!authenticationExceptionHandler.handleAuthenticationException(this@UnlockVaultPresenter, e, ActivityResultCallbacks.authenticatedAfterUnlock(vault))) { + super.onError(e) + if (e is NetworkConnectionException) { + running = true + retryUnlockHandler = Handler() + restartUnlockUseCase(vault) + } + } + } + }) + } + + @Callback(dispatchResultOkOnly = false) + fun authenticatedAfterUnlock(result: ActivityResult, vault: Vault) { + if (result.isResultOk) { + val cloud = result.getSingleResult(CloudModel::class.java).toCloud() + if (startedUsingPrepareUnlock) { + startPrepareUnlockUseCase(Vault.aCopyOf(vault).withCloud(cloud).build()) + if (view?.stoppedBiometricAuthDuringCloudAuthentication() == true) { + view?.showBiometricDialog(VaultModel(vault)) + } + } else { + view?.showProgress(ProgressModel.GENERIC) + startPrepareUnlockUseCase(vault) + } + } else { + view?.closeDialog() + val error = result.getSingleResult(Throwable::class.java) + error?.let { showError(it) } + } + } + + private fun restartUnlockUseCase(vault: Vault) { + retryUnlockHandler?.postDelayed({ + if (running) { + prepareUnlockUseCase // + .withVault(vault) // + .run(object : DefaultResultHandler() { + override fun onSuccess(unlockToken: UnlockToken) { + if (!startedUsingPrepareUnlock && vault.password != null) { + doUnlock(unlockToken, vault.password) + } else { + unlockTokenObtained(unlockToken) + } + } + + override fun onError(e: Throwable) { + if (e is NetworkConnectionException) { + restartUnlockUseCase(vault) + } + } + }) + } + }, 1000) + } + + private fun unlockTokenObtained(unlockToken: UnlockToken) { + pendingUnlockFor(unlockToken.vault)?.setUnlockToken(unlockToken, this) + } + + fun onUnlockClick(vault: VaultModel, password: String?) { + view?.showProgress(ProgressModel.GENERIC) + pendingUnlockFor(vault.toVault())?.setPassword(password, this) + } + + private fun doUnlock(token: UnlockToken, password: String) { + unlockVaultUseCase // + .withVaultOrUnlockToken(VaultOrUnlockToken.from(token)) // + .andPassword(password) // + .run(object : DefaultResultHandler() { + override fun onSuccess(cloud: Cloud) { + when (intent.vaultAction()) { + UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> { + handleUnlockVaultSuccess(token.vault, cloud, password) + } + UnlockVaultIntent.VaultAction.UNLOCK -> finishWithResult(cloud) + else -> TODO("Not yet implemented") + } + } + }) + } + + private fun handleUnlockVaultSuccess(vault: Vault, cloud: Cloud, password: String) { + lockVaultUseCase.withVault(vault).run(object : DefaultResultHandler() { + override fun onFinished() { + finishWithResultAndExtra(cloud, PASSWORD, password) + } + }) + + } + + fun startedUsingPrepareUnlock(): Boolean { + return startedUsingPrepareUnlock + } + + fun useConfirmationInFaceUnlockBiometricAuthentication(): Boolean { + return sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication() + } + + fun onBiometricKeyInvalidated() { + removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler() { + override fun onSuccess(void: Void?) { + view?.showBiometricAuthKeyInvalidatedDialog() + } + + override fun onError(e: Throwable) { + Timber.tag("VaultListPresenter").e(e, "Error while removing vault passwords") + } + }) + } + + fun onBiometricAuthenticationSucceeded(vaultModel: VaultModel) { + when (intent.vaultAction()) { + UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD -> finishWithResult(vaultModel) + UnlockVaultIntent.VaultAction.UNLOCK, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> { + if (startedUsingPrepareUnlock) { + onUnlockClick(vaultModel, vaultModel.password) + } else { + view?.showProgress(ProgressModel.GENERIC) + startPrepareUnlockUseCase(vaultModel.toVault()) + } + } + UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> { + saveVaultUseCase // + .withVault(vaultModel.toVault()) // + .run(object : DefaultResultHandler() { + override fun onSuccess(vault: Vault) { + finishWithResult(vaultModel) + } + }) + } + else -> TODO("Not yet implemented") + } + } + + fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String) { + view?.showProgress(ProgressModel(ProgressStateModel.CHANGING_PASSWORD)) + changePasswordUseCase.withVault(vaultModel.toVault()) // + .andOldPassword(oldPassword) // + .andNewPassword(newPassword) // + .run(object : DefaultResultHandler() { + override fun onSuccess(void: Void?) { + view?.showProgress(ProgressModel.COMPLETED) + view?.showMessage(R.string.screen_vault_list_change_password_successful) + if (canUseBiometricOn(vaultModel)) { + view?.getEncryptedPasswordWithBiometricAuthentication(VaultModel( // + Vault.aCopyOf(vaultModel.toVault()) // + .withSavedPassword(newPassword) // + .build())) + } else { + finishWithResult(vaultModel) + } + } + + override fun onError(e: Throwable) { + if (!authenticationExceptionHandler.handleAuthenticationException( // + this@UnlockVaultPresenter, e, // + ActivityResultCallbacks.changePasswordAfterAuthentication(vaultModel.toVault(), oldPassword, newPassword))) { + showError(e) + } + } + }) + } + + @Callback + fun changePasswordAfterAuthentication(result: ActivityResult, vault: Vault, oldPassword: String, newPassword: String) { + val cloud = result.getSingleResult(CloudModel::class.java).toCloud() + val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build() + onChangePasswordClick(VaultModel(vaultWithUpdatedCloud), oldPassword, newPassword) + } + + fun saveVaultAfterChangePasswordButFailedBiometricAuth(vault: Vault) { + Timber.tag("UnlockVaultPresenter").e("Save vault without password because biometric auth failed after changing vault password") + saveVaultUseCase // + .withVault(vault) // + .run(object : DefaultResultHandler() { + override fun onSuccess(vault: Vault) { + finishWithResult(vault) + } + }) + } + + private open class PendingUnlock(private val vault: Vault?) : Serializable { + + private var unlockToken: UnlockToken? = null + private var password: String? = null + + fun setUnlockToken(unlockToken: UnlockToken?, presenter: UnlockVaultPresenter) { + this.unlockToken = unlockToken + continueIfComplete(presenter) + } + + fun setPassword(password: String?, presenter: UnlockVaultPresenter) { + this.password = password + continueIfComplete(presenter) + } + + open fun continueIfComplete(presenter: UnlockVaultPresenter) { + unlockToken?.let { token -> password?.let { password -> presenter.doUnlock(token, password) } } + } + + fun belongsTo(vault: Vault): Boolean { + return vault == this.vault + } + + companion object { + + val NO_OP_PENDING_UNLOCK: PendingUnlock = object : PendingUnlock(null) { + override fun continueIfComplete(presenter: UnlockVaultPresenter) { + // empty + } + } + } + } + + companion object { + + const val PASSWORD = "password" + } + + init { + unsubscribeOnDestroy(changePasswordUseCase, lockVaultUseCase, unlockVaultUseCase, prepareUnlockUseCase, removeStoredVaultPasswordsUseCase, saveVaultUseCase) + } + +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt index e3555a74..c512c674 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -6,16 +6,12 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Handler -import androidx.biometric.BiometricManager import org.cryptomator.data.cloud.crypto.CryptoCloud import org.cryptomator.data.util.NetworkConnectionCheck import org.cryptomator.domain.Cloud import org.cryptomator.domain.CloudFolder import org.cryptomator.domain.Vault import org.cryptomator.domain.di.PerView -import org.cryptomator.domain.exception.NetworkConnectionException -import org.cryptomator.domain.exception.authentication.AuthenticationException import org.cryptomator.domain.exception.license.LicenseNotValidException import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException import org.cryptomator.domain.usecases.DoLicenseCheckUseCase @@ -26,27 +22,21 @@ import org.cryptomator.domain.usecases.LicenseCheck import org.cryptomator.domain.usecases.NoOpResultHandler import org.cryptomator.domain.usecases.UpdateCheck import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase -import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase import org.cryptomator.domain.usecases.vault.GetVaultListUseCase import org.cryptomator.domain.usecases.vault.LockVaultUseCase import org.cryptomator.domain.usecases.vault.MoveVaultPositionUseCase -import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase -import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase import org.cryptomator.domain.usecases.vault.RenameVaultUseCase import org.cryptomator.domain.usecases.vault.SaveVaultUseCase -import org.cryptomator.domain.usecases.vault.UnlockToken -import org.cryptomator.domain.usecases.vault.UnlockVaultUseCase -import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken import org.cryptomator.generator.Callback import org.cryptomator.presentation.BuildConfig import org.cryptomator.presentation.R import org.cryptomator.presentation.exception.ExceptionHandlers import org.cryptomator.presentation.intent.Intents +import org.cryptomator.presentation.intent.UnlockVaultIntent import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.ProgressModel -import org.cryptomator.presentation.model.ProgressStateModel import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper import org.cryptomator.presentation.service.AutoUploadService @@ -65,7 +55,6 @@ import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow import org.cryptomator.presentation.workflow.Workflow import org.cryptomator.util.Optional import org.cryptomator.util.SharedPreferencesHandler -import java.io.Serializable import javax.inject.Inject import timber.log.Timber @@ -76,15 +65,11 @@ class VaultListPresenter @Inject constructor( // private val renameVaultUseCase: RenameVaultUseCase, // private val lockVaultUseCase: LockVaultUseCase, // private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, // - private val prepareUnlockUseCase: PrepareUnlockUseCase, // - private val unlockVaultUseCase: UnlockVaultUseCase, // private val getRootFolderUseCase: GetRootFolderUseCase, // private val addExistingVaultWorkflow: AddExistingVaultWorkflow, // private val createNewVaultWorkflow: CreateNewVaultWorkflow, // private val saveVaultUseCase: SaveVaultUseCase, // private val moveVaultPositionUseCase: MoveVaultPositionUseCase, // - private val changePasswordUseCase: ChangePasswordUseCase, // - private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, // private val licenseCheckUseCase: DoLicenseCheckUseCase, // private val updateCheckUseCase: DoUpdateCheckUseCase, // private val updateUseCase: DoUpdateUseCase, // @@ -96,31 +81,14 @@ class VaultListPresenter @Inject constructor( // exceptionMappings: ExceptionHandlers) : Presenter(exceptionMappings) { private var vaultAction: VaultAction? = null - private var changedVaultPassword = false - private var startedUsingPrepareUnlock = false - private var retryUnlockHandler: Handler? = null - @Volatile - private var running = false override fun workflows(): Iterable> { return listOf(addExistingVaultWorkflow, createNewVaultWorkflow) } - override fun destroyed() { - super.destroyed() - if (retryUnlockHandler != null) { - running = false - retryUnlockHandler?.removeCallbacks(null) - } - } - fun onWindowFocusChanged(hasFocus: Boolean) { if (hasFocus) { loadVaultList() - if (retryUnlockHandler != null) { - running = false - retryUnlockHandler?.removeCallbacks(null) - } } } @@ -257,11 +225,6 @@ class VaultListPresenter @Inject constructor( // renameVault(VaultModel(vaultWithUpdatedCloud), newVaultName) } - fun onUnlockCanceled() { - prepareUnlockUseCase.unsubscribe() - unlockVaultUseCase.cancel() - } - private fun browseFilesOf(vault: VaultModel) { getDecryptedCloudForVaultUseCase // .withVault(vault.toVault()) // @@ -318,7 +281,6 @@ class VaultListPresenter @Inject constructor( // } fun onVaultClicked(vault: VaultModel) { - startedUsingPrepareUnlock = sharedPreferencesHandler.backgroundUnlockPreparation() startVaultAction(vault, VaultAction.UNLOCK) } @@ -378,7 +340,6 @@ class VaultListPresenter @Inject constructor( // when (vaultAction) { VaultAction.UNLOCK -> requireUserAuthentication(authenticatedVaultModel) VaultAction.RENAME -> view?.showRenameDialog(authenticatedVaultModel) - VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(authenticatedVaultModel) } vaultAction = null } @@ -387,83 +348,22 @@ class VaultListPresenter @Inject constructor( // view?.addOrUpdateVault(authenticatedVault) if (authenticatedVault.isLocked) { if (!isPaused) { - if (canUseBiometricOn(authenticatedVault)) { - if (startedUsingPrepareUnlock) { - startPrepareUnlockUseCase(authenticatedVault.toVault()) - } - view?.showBiometricDialog(authenticatedVault) - } else { - startPrepareUnlockUseCase(authenticatedVault.toVault()) - view?.showEnterPasswordDialog(authenticatedVault) - } + requestActivityResult( // + ActivityResultCallbacks.vaultUnlockedVaultList(), // + Intents.unlockVaultIntent().withVaultModel(authenticatedVault).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK)) } } else { browseFilesOf(authenticatedVault) } } - fun startPrepareUnlockUseCase(vault: Vault) { - pendingUnlock = null - prepareUnlockUseCase // - .withVault(vault) // - .run(object : DefaultResultHandler() { - override fun onSuccess(unlockToken: UnlockToken) { - if (!startedUsingPrepareUnlock && vault.password != null) { - doUnlock(unlockToken, vault.password) - } else { - unlockTokenObtained(unlockToken) - } - } - - override fun onError(e: Throwable) { - if (e is AuthenticationException) { - view?.cancelBasicAuthIfRunning() - } - if (!authenticationExceptionHandler.handleAuthenticationException(this@VaultListPresenter, e, ActivityResultCallbacks.authenticatedAfterUnlock(vault))) { - super.onError(e) - if (e is NetworkConnectionException) { - running = true - retryUnlockHandler = Handler() - restartUnlockUseCase(vault) - } - } - } - }) - } - - private fun restartUnlockUseCase(vault: Vault) { - retryUnlockHandler?.postDelayed({ - if (running) { - prepareUnlockUseCase // - .withVault(vault) // - .run(object : DefaultResultHandler() { - override fun onSuccess(unlockToken: UnlockToken) { - if (!startedUsingPrepareUnlock && vault.password != null) { - doUnlock(unlockToken, vault.password) - } else { - unlockTokenObtained(unlockToken) - } - } - - override fun onError(e: Throwable) { - if (e is NetworkConnectionException) { - restartUnlockUseCase(vault) - } - } - }) - } - }, 1000) - } - - private fun doUnlock(token: UnlockToken, password: String) { - unlockVaultUseCase // - .withVaultOrUnlockToken(VaultOrUnlockToken.from(token)) // - .andPassword(password) // - .run(object : DefaultResultHandler() { - override fun onSuccess(cloud: Cloud) { - navigateToVaultContent(cloud) - } - }) + @Callback + fun vaultUnlockedVaultList(result: ActivityResult) { + val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud + when { + result.isResultOk -> navigateToVaultContent(cloud) + else -> TODO("Not yet implemented") + } } private fun navigateToVaultContent(cloud: Cloud) { @@ -488,51 +388,6 @@ class VaultListPresenter @Inject constructor( // } else false } - private fun unlockTokenObtained(unlockToken: UnlockToken) { - pendingUnlockFor(unlockToken.vault)?.setUnlockToken(unlockToken, this) - } - - fun onUnlockClick(vault: VaultModel, password: String?) { - view?.showProgress(ProgressModel.GENERIC) - pendingUnlockFor(vault.toVault())?.setPassword(password, this) - } - - private var pendingUnlock: PendingUnlock? = null - private fun pendingUnlockFor(vault: Vault): PendingUnlock? { - if (pendingUnlock == null) { - pendingUnlock = PendingUnlock(vault) - } - return if (pendingUnlock?.belongsTo(vault) == true) { - pendingUnlock - } else { - PendingUnlock.NO_OP_PENDING_UNLOCK - } - } - - @Callback(dispatchResultOkOnly = false) - fun authenticatedAfterUnlock(result: ActivityResult, vault: Vault) { - if (result.isResultOk) { - val cloud = result.getSingleResult(CloudModel::class.java).toCloud() - if (startedUsingPrepareUnlock) { - startPrepareUnlockUseCase(Vault.aCopyOf(vault).withCloud(cloud).build()) - if (view?.stoppedBiometricAuthDuringCloudAuthentication() == true) { - view?.showBiometricDialog(VaultModel(vault)) - } - } else { - view?.showProgress(ProgressModel.GENERIC) - startPrepareUnlockUseCase(vault) - } - } else { - view?.closeDialog() - val error = result.getSingleResult(Throwable::class.java) - error?.let { showError(it) } - } - } - - private fun canUseBiometricOn(vault: VaultModel): Boolean { - return vault.password != null && BiometricManager.from(context()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS - } - fun onAddExistingVault() { addExistingVaultWorkflow.start() } @@ -548,60 +403,11 @@ class VaultListPresenter @Inject constructor( // } fun onChangePasswordClicked(vaultModel: VaultModel) { - startVaultAction(vaultModel, VaultAction.CHANGE_PASSWORD) - } - - fun onChangePasswordClicked(vaultModel: VaultModel, oldPassword: String?, newPassword: String?) { - view?.showProgress(ProgressModel(ProgressStateModel.CHANGING_PASSWORD)) - changePasswordUseCase.withVault(vaultModel.toVault()) // - .andOldPassword(oldPassword) // - .andNewPassword(newPassword) // - .run(object : DefaultResultHandler() { - override fun onSuccess(void: Void?) { - view?.showProgress(ProgressModel.COMPLETED) - view?.showMessage(R.string.screen_vault_list_change_password_successful) - if (canUseBiometricOn(vaultModel)) { - changedVaultPassword = true - view?.getEncryptedPasswordWithBiometricAuthentication(VaultModel( // - Vault.aCopyOf(vaultModel.toVault()) // - .withSavedPassword(newPassword) // - .build())) - } - } - - override fun onError(e: Throwable) { - if (!authenticationExceptionHandler.handleAuthenticationException( // - this@VaultListPresenter, e, // - ActivityResultCallbacks.changePasswordAfterAuthentication(vaultModel.toVault(), oldPassword, newPassword))) { - showError(e) - } - } - }) - } - - @Callback - fun changePasswordAfterAuthentication(result: ActivityResult, vault: Vault?, oldPassword: String?, newPassword: String?) { - val cloud = result.getSingleResult(CloudModel::class.java).toCloud() - val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build() - onChangePasswordClicked(VaultModel(vaultWithUpdatedCloud), oldPassword, newPassword) - } - - private fun save(vaultModel: VaultModel) { - saveVaultUseCase // - .withVault(vaultModel.toVault()) // - .run(DefaultResultHandler()) - } - - fun onBiometricKeyInvalidated() { - removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler() { - override fun onSuccess(void: Void?) { - view?.showBiometricAuthKeyInvalidatedDialog() - } - - override fun onError(e: Throwable) { - Timber.tag("VaultListPresenter").e(e, "Error while removing vault passwords") - } - }) + Intents + .unlockVaultIntent() + .withVaultModel(vaultModel) + .withVaultAction(UnlockVaultIntent.VaultAction.CHANGE_PASSWORD) + .startActivity(this) } fun onVaultSettingsClicked(vaultModel: VaultModel) { @@ -654,26 +460,8 @@ class VaultListPresenter @Inject constructor( // }) } - fun onBiometricAuthenticationSucceeded(vaultModel: VaultModel) { - if (changedVaultPassword) { - changedVaultPassword = false - save(vaultModel) - } else { - if (startedUsingPrepareUnlock) { - onUnlockClick(vaultModel, vaultModel.password) - } else { - view?.showProgress(ProgressModel.GENERIC) - startPrepareUnlockUseCase(vaultModel.toVault()) - } - } - } - - fun useConfirmationInFaceUnlockBiometricAuthentication(): Boolean { - return sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication() - } - private enum class VaultAction { - UNLOCK, RENAME, CHANGE_PASSWORD + UNLOCK, RENAME } fun installUpdate() { @@ -698,43 +486,6 @@ class VaultListPresenter @Inject constructor( // }) } - fun startedUsingPrepareUnlock(): Boolean { - return startedUsingPrepareUnlock - } - - private open class PendingUnlock(private val vault: Vault?) : Serializable { - - private var unlockToken: UnlockToken? = null - private var password: String? = null - - fun setUnlockToken(unlockToken: UnlockToken?, presenter: VaultListPresenter) { - this.unlockToken = unlockToken - continueIfComplete(presenter) - } - - fun setPassword(password: String?, presenter: VaultListPresenter) { - this.password = password - continueIfComplete(presenter) - } - - open fun continueIfComplete(presenter: VaultListPresenter) { - unlockToken?.let { token -> password?.let { password -> presenter.doUnlock(token, password) } } - } - - fun belongsTo(vault: Vault): Boolean { - return vault == this.vault - } - - companion object { - - val NO_OP_PENDING_UNLOCK: PendingUnlock = object : PendingUnlock(null) { - override fun continueIfComplete(presenter: VaultListPresenter) { - // empty - } - } - } - } - init { unsubscribeOnDestroy( // deleteVaultUseCase, // @@ -743,9 +494,6 @@ class VaultListPresenter @Inject constructor( // getVaultListUseCase, // saveVaultUseCase, // moveVaultPositionUseCase, // - removeStoredVaultPasswordsUseCase, // - unlockVaultUseCase, // - prepareUnlockUseCase, // licenseCheckUseCase, // updateCheckUseCase, // updateUseCase) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/AutoUploadChooseVaultActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/AutoUploadChooseVaultActivity.kt index d4c54335..be33f1bf 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/AutoUploadChooseVaultActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/AutoUploadChooseVaultActivity.kt @@ -1,7 +1,5 @@ package org.cryptomator.presentation.ui.activity -import android.os.Build -import androidx.annotation.RequiresApi import androidx.fragment.app.Fragment import org.cryptomator.generator.Activity import org.cryptomator.presentation.R @@ -9,20 +7,15 @@ import org.cryptomator.presentation.model.CloudFolderModel import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.presenter.AutoUploadChooseVaultPresenter import org.cryptomator.presentation.ui.activity.view.AutoUploadChooseVaultView -import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog -import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog import org.cryptomator.presentation.ui.dialog.NotEnoughVaultsDialog import org.cryptomator.presentation.ui.fragment.AutoUploadChooseVaultFragment -import org.cryptomator.presentation.util.BiometricAuthentication import javax.inject.Inject import kotlinx.android.synthetic.main.toolbar_layout.toolbar @Activity class AutoUploadChooseVaultActivity : BaseActivity(), // AutoUploadChooseVaultView, // - NotEnoughVaultsDialog.Callback, // - EnterPasswordDialog.Callback, - BiometricAuthentication.Callback { + NotEnoughVaultsDialog.Callback { @Inject lateinit var presenter: AutoUploadChooseVaultPresenter @@ -40,7 +33,7 @@ class AutoUploadChooseVaultActivity : BaseActivity(), // } } - override fun createFragment(): Fragment? = AutoUploadChooseVaultFragment() + override fun createFragment(): Fragment = AutoUploadChooseVaultFragment() override fun displayVaults(vaults: List) { @@ -69,41 +62,5 @@ class AutoUploadChooseVaultActivity : BaseActivity(), // autoUploadChooseVaultFragment().showChosenLocation(location) } - override fun onUnlockCanceled() { - presenter.onUnlockCanceled() - } - - override fun onUnlockClick(vaultModel: VaultModel, password: String) { - presenter.onUnlockPressed(vaultModel, password) - } - - @RequiresApi(api = Build.VERSION_CODES.M) - override fun showEnterPasswordDialog(vaultModel: VaultModel) { - if (vaultWithBiometricAuthEnabled(vaultModel)) { - BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.DECRYPT, presenter.useConfirmationInFaceUnlockBiometricAuthentication()) - .startListening(autoUploadChooseVaultFragment(), vaultModel) - } else { - showDialog(EnterPasswordDialog.newInstance(vaultModel)) - } - } - - override fun onBiometricAuthenticated(vault: VaultModel) { - presenter.onUnlockPressed(vault, vault.password) - } - - override fun onBiometricAuthenticationFailed(vault: VaultModel) { - showDialog(EnterPasswordDialog.newInstance(vault)) - } - - override fun onBiometricKeyInvalidated(vault: VaultModel) { - presenter.onBiometricKeyInvalidated(vault) - } - - override fun showBiometricAuthKeyInvalidatedDialog() { - showDialog(BiometricAuthKeyInvalidatedDialog.newInstance()) - } - - private fun vaultWithBiometricAuthEnabled(vault: VaultModel): Boolean = vault.password != null - private fun autoUploadChooseVaultFragment(): AutoUploadChooseVaultFragment = getCurrentFragment(R.id.fragmentContainer) as AutoUploadChooseVaultFragment } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BiometricAuthSettingsActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BiometricAuthSettingsActivity.kt index a4de1005..0cf91bd2 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BiometricAuthSettingsActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BiometricAuthSettingsActivity.kt @@ -1,28 +1,20 @@ package org.cryptomator.presentation.ui.activity -import android.os.Build -import androidx.annotation.RequiresApi import androidx.biometric.BiometricManager import androidx.fragment.app.Fragment -import org.cryptomator.domain.Vault import org.cryptomator.generator.Activity import org.cryptomator.presentation.R import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.presenter.BiometricAuthSettingsPresenter import org.cryptomator.presentation.ui.activity.view.BiometricAuthSettingsView -import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog import org.cryptomator.presentation.ui.dialog.EnrollSystemBiometricDialog -import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog import org.cryptomator.presentation.ui.fragment.BiometricAuthSettingsFragment -import org.cryptomator.presentation.util.BiometricAuthentication import javax.inject.Inject import kotlinx.android.synthetic.main.toolbar_layout.toolbar @Activity class BiometricAuthSettingsActivity : BaseActivity(), // - EnterPasswordDialog.Callback, // BiometricAuthSettingsView, // - BiometricAuthentication.Callback, // EnrollSystemBiometricDialog.Callback { @Inject @@ -42,10 +34,6 @@ class BiometricAuthSettingsActivity : BaseActivity(), // } } - override fun showBiometricAuthKeyInvalidatedDialog() { - showDialog(BiometricAuthKeyInvalidatedDialog.newInstance()) - } - override fun createFragment(): Fragment? = BiometricAuthSettingsFragment() override fun renderVaultList(vaultModelCollection: List) { @@ -56,30 +44,6 @@ class BiometricAuthSettingsActivity : BaseActivity(), // biometricAuthSettingsFragment().clearVaultList() } - override fun showEnterPasswordDialog(vaultModel: VaultModel) { - showDialog(EnterPasswordDialog.newInstance(vaultModel)) - } - - override fun onUnlockClick(vaultModel: VaultModel, password: String) { - val vaultModelWithSavedPassword = VaultModel( // - Vault // - .aCopyOf(vaultModel.toVault()) // - .withSavedPassword(password) // - .build()) - - presenter.verifyPassword(vaultModelWithSavedPassword) - } - - override fun onUnlockCanceled() { - presenter.onUnlockCanceled() - } - - @RequiresApi(api = Build.VERSION_CODES.M) - override fun showBiometricAuthenticationDialog(vaultModel: VaultModel) { - BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.ENCRYPT, presenter.useConfirmationInFaceUnlockBiometricAuthentication()) - .startListening(biometricAuthSettingsFragment(), vaultModel) - } - private fun biometricAuthSettingsFragment(): BiometricAuthSettingsFragment = getCurrentFragment(R.id.fragmentContainer) as BiometricAuthSettingsFragment override fun onSetupBiometricAuthInSystemClicked() { @@ -89,17 +53,4 @@ class BiometricAuthSettingsActivity : BaseActivity(), // override fun onCancelSetupBiometricAuthInSystemClicked() { finish() } - - override fun onBiometricAuthenticated(vault: VaultModel) { - presenter.saveVault(vault.toVault()) - } - - override fun onBiometricAuthenticationFailed(vault: VaultModel) { - showError(getString(R.string.error_biometric_auth_aborted)) - biometricAuthSettingsFragment().addOrUpdateVault(vault) - } - - override fun onBiometricKeyInvalidated(vault: VaultModel) { - presenter.onBiometricAuthKeyInvalidated(vault) - } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SharedFilesActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SharedFilesActivity.kt index d83c0f75..84f3fb8e 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SharedFilesActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SharedFilesActivity.kt @@ -5,8 +5,6 @@ import android.content.Intent import android.content.Intent.ACTION_SEND import android.content.Intent.ACTION_SEND_MULTIPLE import android.net.Uri -import android.os.Build -import androidx.annotation.RequiresApi import androidx.fragment.app.Fragment import org.cryptomator.generator.Activity import org.cryptomator.presentation.R @@ -16,13 +14,10 @@ import org.cryptomator.presentation.model.SharedFileModel import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.presenter.SharedFilesPresenter import org.cryptomator.presentation.ui.activity.view.SharedFilesView -import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog -import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog import org.cryptomator.presentation.ui.dialog.NotEnoughVaultsDialog import org.cryptomator.presentation.ui.dialog.ReplaceDialog import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog import org.cryptomator.presentation.ui.fragment.SharedFilesFragment -import org.cryptomator.presentation.util.BiometricAuthentication import java.lang.String.format import java.util.ArrayList import javax.inject.Inject @@ -32,8 +27,6 @@ import timber.log.Timber @Activity class SharedFilesActivity : BaseActivity(), // SharedFilesView, // - EnterPasswordDialog.Callback, // - BiometricAuthentication.Callback, // ReplaceDialog.Callback, // NotEnoughVaultsDialog.Callback, // UploadCloudFileDialog.Callback { @@ -126,7 +119,7 @@ class SharedFilesActivity : BaseActivity(), // } } - override fun createFragment(): Fragment? = SharedFilesFragment() + override fun createFragment(): Fragment = SharedFilesFragment() public override fun onMenuItemSelected(itemId: Int): Boolean = when (itemId) { android.R.id.home -> { @@ -150,16 +143,6 @@ class SharedFilesActivity : BaseActivity(), // private fun sharedFilesFragment(): SharedFilesFragment = getCurrentFragment(R.id.fragmentContainer) as SharedFilesFragment - @RequiresApi(api = Build.VERSION_CODES.M) - override fun showEnterPasswordDialog(vault: VaultModel) { - if (vaultWithBiometricAuthEnabled(vault)) { - BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.DECRYPT, presenter.useConfirmationInFaceUnlockBiometricAuthentication()) - .startListening(sharedFilesFragment(), vault) - } else { - showDialog(EnterPasswordDialog.newInstance(vault)) - } - } - override fun showReplaceDialog(existingFiles: List, size: Int) { ReplaceDialog.withContext(this).show(existingFiles, size) } @@ -168,22 +151,10 @@ class SharedFilesActivity : BaseActivity(), // sharedFilesFragment().showChosenLocation(folder) } - override fun showBiometricAuthKeyInvalidatedDialog() { - showDialog(BiometricAuthKeyInvalidatedDialog.newInstance()) - } - override fun showUploadDialog(uploadingFiles: Int) { showDialog(UploadCloudFileDialog.newInstance(uploadingFiles)) } - override fun onUnlockClick(vaultModel: VaultModel, password: String) { - presenter.onUnlockPressed(vaultModel, password) - } - - override fun onUnlockCanceled() { - presenter.onUnlockCanceled() - } - override fun onReplacePositiveClicked() { presenter.onReplaceExistingFilesPressed() } @@ -209,20 +180,6 @@ class SharedFilesActivity : BaseActivity(), // finish() } - override fun onBiometricAuthenticated(vault: VaultModel) { - presenter.onUnlockPressed(vault, vault.password) - } - - override fun onBiometricAuthenticationFailed(vault: VaultModel) { - showDialog(EnterPasswordDialog.newInstance(vault)) - } - - override fun onBiometricKeyInvalidated(vault: VaultModel) { - presenter.onBiometricAuthKeyInvalidated() - } - - private fun vaultWithBiometricAuthEnabled(vault: VaultModel): Boolean = vault.password != null - override fun onUploadCanceled() { presenter.onUploadCanceled() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt new file mode 100644 index 00000000..9ef06bef --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt @@ -0,0 +1,109 @@ +package org.cryptomator.presentation.ui.activity + +import android.os.Build +import androidx.annotation.RequiresApi +import org.cryptomator.domain.Vault +import org.cryptomator.generator.Activity +import org.cryptomator.generator.InjectIntent +import org.cryptomator.presentation.R +import org.cryptomator.presentation.intent.UnlockVaultIntent +import org.cryptomator.presentation.model.VaultModel +import org.cryptomator.presentation.presenter.UnlockVaultPresenter +import org.cryptomator.presentation.ui.activity.view.UnlockVaultView +import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog +import org.cryptomator.presentation.ui.dialog.ChangePasswordDialog +import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog +import org.cryptomator.presentation.ui.fragment.UnlockVaultFragment +import org.cryptomator.presentation.util.BiometricAuthentication +import javax.inject.Inject + +@Activity(layout = R.layout.activity_unlock_vault) +class UnlockVaultActivity : BaseActivity(), // + UnlockVaultView, // + BiometricAuthentication.Callback, + ChangePasswordDialog.Callback { + + @Inject + lateinit var presenter: UnlockVaultPresenter + + @InjectIntent + lateinit var unlockVaultIntent: UnlockVaultIntent + + + private lateinit var biometricAuthentication: BiometricAuthentication + + override fun finish() { + super.finish() + overridePendingTransition(0, 0) + } + + override fun showEnterPasswordDialog(vault: VaultModel) { + showDialog(EnterPasswordDialog.newInstance(vault)) + } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun showBiometricDialog(vault: VaultModel) { + biometricAuthentication = BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.DECRYPT, presenter.useConfirmationInFaceUnlockBiometricAuthentication()) + biometricAuthentication.startListening(unlockVaultFragment(), vault) + } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun getEncryptedPasswordWithBiometricAuthentication(vaultModel: VaultModel) { + biometricAuthentication = BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.ENCRYPT, presenter.useConfirmationInFaceUnlockBiometricAuthentication()) + biometricAuthentication.startListening(unlockVaultFragment(), vaultModel) + } + + override fun showBiometricAuthKeyInvalidatedDialog() { + showDialog(BiometricAuthKeyInvalidatedDialog.newInstance()) + } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun cancelBasicAuthIfRunning() { + biometricAuthentication.stopListening() + } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean { + return biometricAuthentication.stoppedBiometricAuthDuringCloudAuthentication() + } + + override fun onUnlockClick(vaultModel: VaultModel, password: String) { + presenter.onUnlockClick(vaultModel, password) + } + + override fun onUnlockCanceled() { + presenter.onUnlockCanceled() + } + + override fun onBiometricAuthenticated(vault: VaultModel) { + presenter.onBiometricAuthenticationSucceeded(vault) + } + + override fun onBiometricAuthenticationFailed(vault: VaultModel) { + val vaultWithoutPassword = Vault.aCopyOf(vault.toVault()).withSavedPassword(null).build() + when(unlockVaultIntent.vaultAction()) { + UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> presenter.saveVaultAfterChangePasswordButFailedBiometricAuth(vaultWithoutPassword) + else -> { + if (!presenter.startedUsingPrepareUnlock()) { + presenter.startPrepareUnlockUseCase(vaultWithoutPassword) + } + showEnterPasswordDialog(VaultModel(vaultWithoutPassword)) + } + } + } + + override fun onBiometricKeyInvalidated(vault: VaultModel) { + presenter.onBiometricKeyInvalidated() + } + + private fun unlockVaultFragment(): UnlockVaultFragment = // + getCurrentFragment(R.id.fragmentContainer) as UnlockVaultFragment + + override fun showChangePasswordDialog(vaultModel: VaultModel) { + showDialog(ChangePasswordDialog.newInstance(vaultModel)) + } + + override fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String) { + presenter.onChangePasswordClick(vaultModel, oldPassword, newPassword) + } +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt index c78e153b..bc9dcbf8 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt @@ -2,9 +2,7 @@ package org.cryptomator.presentation.ui.activity import android.content.Intent import android.net.Uri -import android.os.Build import android.view.View -import androidx.annotation.RequiresApi import androidx.fragment.app.Fragment import org.cryptomator.domain.Vault import org.cryptomator.generator.Activity @@ -25,9 +23,6 @@ import org.cryptomator.presentation.ui.bottomsheet.SettingsVaultBottomSheet import org.cryptomator.presentation.ui.callback.VaultListCallback import org.cryptomator.presentation.ui.dialog.AskForLockScreenDialog import org.cryptomator.presentation.ui.dialog.BetaConfirmationDialog -import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog -import org.cryptomator.presentation.ui.dialog.ChangePasswordDialog -import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog import org.cryptomator.presentation.ui.dialog.UpdateAppAvailableDialog import org.cryptomator.presentation.ui.dialog.UpdateAppDialog import org.cryptomator.presentation.ui.dialog.VaultDeleteConfirmationDialog @@ -35,7 +30,6 @@ import org.cryptomator.presentation.ui.dialog.VaultNotFoundDialog import org.cryptomator.presentation.ui.dialog.VaultRenameDialog import org.cryptomator.presentation.ui.fragment.VaultListFragment import org.cryptomator.presentation.ui.layout.ObscuredAwareCoordinatorLayout.Listener -import org.cryptomator.presentation.util.BiometricAuthentication import java.util.Locale import javax.inject.Inject import kotlinx.android.synthetic.main.activity_layout_obscure_aware.activityRootView @@ -45,10 +39,8 @@ import kotlinx.android.synthetic.main.toolbar_layout.toolbar class VaultListActivity : BaseActivity(), // VaultListView, // VaultListCallback, // - BiometricAuthentication.Callback, // AskForLockScreenDialog.Callback, // - ChangePasswordDialog.Callback, // - VaultNotFoundDialog.Callback, + VaultNotFoundDialog.Callback, // UpdateAppAvailableDialog.Callback, // UpdateAppDialog.Callback, // BetaConfirmationDialog.Callback { @@ -59,8 +51,6 @@ class VaultListActivity : BaseActivity(), // @InjectIntent lateinit var vaultListIntent: VaultListIntent - private var biometricAuthentication: BiometricAuthentication? = null - override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) vaultListPresenter.onWindowFocusChanged(hasFocus) @@ -125,40 +115,6 @@ class VaultListActivity : BaseActivity(), // showDialog(VaultRenameDialog.newInstance(vaultModel)) } - override fun showEnterPasswordDialog(vault: VaultModel) { - showDialog(EnterPasswordDialog.newInstance(vault)) - } - - @RequiresApi(api = Build.VERSION_CODES.M) - override fun showBiometricDialog(vault: VaultModel) { - biometricAuthentication = BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.DECRYPT, vaultListPresenter.useConfirmationInFaceUnlockBiometricAuthentication()) - biometricAuthentication?.startListening(vaultListFragment(), vault) - } - - override fun showChangePasswordDialog(vaultModel: VaultModel) { - showDialog(ChangePasswordDialog.newInstance(vaultModel)) - } - - @RequiresApi(api = Build.VERSION_CODES.M) - override fun getEncryptedPasswordWithBiometricAuthentication(vaultModel: VaultModel) { - biometricAuthentication = BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.ENCRYPT, vaultListPresenter.useConfirmationInFaceUnlockBiometricAuthentication()) - biometricAuthentication?.startListening(vaultListFragment(), vaultModel) - } - - override fun showBiometricAuthKeyInvalidatedDialog() { - showDialog(BiometricAuthKeyInvalidatedDialog.newInstance()) - } - - @RequiresApi(api = Build.VERSION_CODES.M) - override fun cancelBasicAuthIfRunning() { - biometricAuthentication?.stopListening() - } - - @RequiresApi(api = Build.VERSION_CODES.M) - override fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean { - return biometricAuthentication?.stoppedBiometricAuthDuringCloudAuthentication() == true - } - override fun rowMoved(fromPosition: Int, toPosition: Int) { vaultListFragment().rowMoved(fromPosition, toPosition) } @@ -209,14 +165,6 @@ class VaultListActivity : BaseActivity(), // vaultListPresenter.onCreateVault() } - override fun onUnlockClick(vaultModel: VaultModel, password: String) { - vaultListPresenter.onUnlockClick(vaultModel, password) - } - - override fun onUnlockCanceled() { - vaultListPresenter.onUnlockCanceled() - } - override fun onDeleteVaultClick(vaultModel: VaultModel) { VaultDeleteConfirmationDialog.newInstance(vaultModel) // .show(supportFragmentManager, "VaultDeleteConfirmationDialog") @@ -249,10 +197,6 @@ class VaultListActivity : BaseActivity(), // private fun vaultListFragment(): VaultListFragment = // getCurrentFragment(R.id.fragmentContainer) as VaultListFragment - override fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String) { - vaultListPresenter.onChangePasswordClicked(vaultModel, oldPassword, newPassword) - } - override fun onDeleteMissingVaultClicked(vault: Vault) { vaultListPresenter.onDeleteMissingVaultClicked(vault) } @@ -279,21 +223,4 @@ class VaultListActivity : BaseActivity(), // override fun onAskForBetaConfirmationFinished() { sharedPreferencesHandler.setBetaScreenDialogAlreadyShown() } - - override fun onBiometricAuthenticated(vault: VaultModel) { - vaultListPresenter.onBiometricAuthenticationSucceeded(vault) - } - - override fun onBiometricAuthenticationFailed(vault: VaultModel) { - val vaultWithoutPassword = Vault.aCopyOf(vault.toVault()).withSavedPassword(null).build() - if (!vaultListPresenter.startedUsingPrepareUnlock()) { - vaultListPresenter.startPrepareUnlockUseCase(vaultWithoutPassword) - } - showEnterPasswordDialog(VaultModel(vaultWithoutPassword)) - } - - override fun onBiometricKeyInvalidated(vault: VaultModel) { - vaultListPresenter.onBiometricKeyInvalidated() - } - } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/AutoUploadChooseVaultView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/AutoUploadChooseVaultView.kt index 862222bc..424b7d85 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/AutoUploadChooseVaultView.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/AutoUploadChooseVaultView.kt @@ -8,7 +8,5 @@ interface AutoUploadChooseVaultView : View { fun displayDialogUnableToUploadFiles() fun displayVaults(vaults: List) fun showChosenLocation(location: CloudFolderModel) - fun showEnterPasswordDialog(vaultModel: VaultModel) - fun showBiometricAuthKeyInvalidatedDialog() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BiometricAuthSettingsView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BiometricAuthSettingsView.kt index ae16db86..35dc75d0 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BiometricAuthSettingsView.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BiometricAuthSettingsView.kt @@ -6,9 +6,6 @@ interface BiometricAuthSettingsView : View { fun renderVaultList(vaultModelCollection: List) fun clearVaultList() - fun showBiometricAuthenticationDialog(vaultModel: VaultModel) - fun showEnterPasswordDialog(vaultModel: VaultModel) fun showSetupBiometricAuthDialog() - fun showBiometricAuthKeyInvalidatedDialog() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SharedFilesView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SharedFilesView.kt index 2be3cd55..f93948b3 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SharedFilesView.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SharedFilesView.kt @@ -11,10 +11,8 @@ interface SharedFilesView : View { fun displayVaults(vaults: List) fun displayFilesToUpload(sharedFiles: List) fun displayDialogUnableToUploadFiles() - fun showEnterPasswordDialog(vault: VaultModel) fun showReplaceDialog(existingFiles: List, size: Int) fun showChosenLocation(folder: CloudFolderModel) - fun showBiometricAuthKeyInvalidatedDialog() fun showUploadDialog(uploadingFiles: Int) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/UnlockVaultView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/UnlockVaultView.kt new file mode 100644 index 00000000..a7b962a1 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/UnlockVaultView.kt @@ -0,0 +1,16 @@ +package org.cryptomator.presentation.ui.activity.view + +import org.cryptomator.presentation.model.VaultModel +import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog + +interface UnlockVaultView : View, EnterPasswordDialog.Callback { + + fun showEnterPasswordDialog(vault: VaultModel) + fun showBiometricDialog(vault: VaultModel) + fun getEncryptedPasswordWithBiometricAuthentication(vaultModel: VaultModel) + fun showBiometricAuthKeyInvalidatedDialog() + fun cancelBasicAuthIfRunning() + fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean + fun showChangePasswordDialog(vaultModel: VaultModel) + +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/VaultListView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/VaultListView.kt index 4551f245..e9950de5 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/VaultListView.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/VaultListView.kt @@ -12,17 +12,10 @@ interface VaultListView : View { fun addOrUpdateVault(vault: VaultModel) fun renameVault(vaultModel: VaultModel) fun navigateToVaultContent(vault: VaultModel, decryptedRoot: CloudFolderModel) - fun showEnterPasswordDialog(vault: VaultModel) - fun showBiometricDialog(vault: VaultModel) - fun showChangePasswordDialog(vaultModel: VaultModel) - fun getEncryptedPasswordWithBiometricAuthentication(vaultModel: VaultModel) fun showVaultSettingsDialog(vaultModel: VaultModel) fun showAddVaultBottomSheet() fun showRenameDialog(vaultModel: VaultModel) - fun showBiometricAuthKeyInvalidatedDialog() fun isVaultLocked(vaultModel: VaultModel): Boolean - fun cancelBasicAuthIfRunning() - fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean fun rowMoved(fromPosition: Int, toPosition: Int) fun vaultMoved(vaults: List) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/callback/VaultListCallback.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/callback/VaultListCallback.kt index 4a478f59..604ebd4c 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/callback/VaultListCallback.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/callback/VaultListCallback.kt @@ -2,8 +2,11 @@ package org.cryptomator.presentation.ui.callback import org.cryptomator.presentation.ui.bottomsheet.AddVaultBottomSheet import org.cryptomator.presentation.ui.bottomsheet.SettingsVaultBottomSheet -import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog import org.cryptomator.presentation.ui.dialog.VaultDeleteConfirmationDialog import org.cryptomator.presentation.ui.dialog.VaultRenameDialog -interface VaultListCallback : AddVaultBottomSheet.Callback, EnterPasswordDialog.Callback, SettingsVaultBottomSheet.Callback, VaultDeleteConfirmationDialog.Callback, VaultRenameDialog.Callback +// FIXME delete this file and add this interfaces to VaultListView.kt +interface VaultListCallback : AddVaultBottomSheet.Callback, // + SettingsVaultBottomSheet.Callback, // + VaultDeleteConfirmationDialog.Callback, // + VaultRenameDialog.Callback diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/UnlockVaultFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/UnlockVaultFragment.kt new file mode 100644 index 00000000..006e07c9 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/UnlockVaultFragment.kt @@ -0,0 +1,17 @@ +package org.cryptomator.presentation.ui.fragment + +import org.cryptomator.generator.Fragment +import org.cryptomator.presentation.R +import org.cryptomator.presentation.presenter.UnlockVaultPresenter +import javax.inject.Inject + +@Fragment(R.layout.fragment_unlock_vault) +class UnlockVaultFragment : BaseFragment() { + + @Inject + lateinit var presenter: UnlockVaultPresenter + + override fun setupView() { + presenter.setup() + } +} diff --git a/presentation/src/main/res/layout/activity_unlock_vault.xml b/presentation/src/main/res/layout/activity_unlock_vault.xml new file mode 100644 index 00000000..fe197fdd --- /dev/null +++ b/presentation/src/main/res/layout/activity_unlock_vault.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/presentation/src/main/res/layout/fragment_unlock_vault.xml b/presentation/src/main/res/layout/fragment_unlock_vault.xml new file mode 100644 index 00000000..b9a2f34b --- /dev/null +++ b/presentation/src/main/res/layout/fragment_unlock_vault.xml @@ -0,0 +1,7 @@ + + + + diff --git a/presentation/src/main/res/values/styles.xml b/presentation/src/main/res/values/styles.xml index a09be1b3..be5e68ea 100644 --- a/presentation/src/main/res/values/styles.xml +++ b/presentation/src/main/res/values/styles.xml @@ -29,6 +29,10 @@ @color/colorPrimary + + diff --git a/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java b/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java index 63eb468a..699008f6 100644 --- a/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java +++ b/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java @@ -13,17 +13,13 @@ import org.cryptomator.domain.usecases.DoUpdateUseCase; import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase; import org.cryptomator.domain.usecases.ResultHandler; import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase; -import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase; import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase; import org.cryptomator.domain.usecases.vault.GetVaultListUseCase; import org.cryptomator.domain.usecases.vault.LockVaultUseCase; import org.cryptomator.domain.usecases.vault.MoveVaultPositionUseCase; -import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase; -import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase; import org.cryptomator.domain.usecases.vault.RenameVaultUseCase; import org.cryptomator.domain.usecases.vault.SaveVaultUseCase; import org.cryptomator.domain.usecases.vault.UnlockToken; -import org.cryptomator.domain.usecases.vault.UnlockVaultUseCase; import org.cryptomator.presentation.exception.ExceptionHandlers; import org.cryptomator.presentation.model.VaultModel; import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper; @@ -98,17 +94,12 @@ public class VaultListPresenterTest { private LockVaultUseCase lockVaultUseCase = Mockito.mock(LockVaultUseCase.class); private LockVaultUseCase.Launcher lockVaultUseCaseLauncher = Mockito.mock(LockVaultUseCase.Launcher.class); private GetDecryptedCloudForVaultUseCase getDecryptedCloudForVaultUseCase = Mockito.mock(GetDecryptedCloudForVaultUseCase.class); - private PrepareUnlockUseCase prepareUnlockUseCase = Mockito.mock(PrepareUnlockUseCase.class); - private PrepareUnlockUseCase.Launcher prepareUnlockUseCaseLauncher = Mockito.mock(PrepareUnlockUseCase.Launcher.class); private UnlockToken unlockToken = Mockito.mock(UnlockToken.class); - private UnlockVaultUseCase unlockVaultUseCase = Mockito.mock(UnlockVaultUseCase.class); private GetRootFolderUseCase getRootFolderUseCase = Mockito.mock(GetRootFolderUseCase.class); private AddExistingVaultWorkflow addExistingVaultWorkflow = Mockito.mock(AddExistingVaultWorkflow.class); private CreateNewVaultWorkflow createNewVaultWorkflow = Mockito.mock(CreateNewVaultWorkflow.class); private SaveVaultUseCase saveVaultUseCase = Mockito.mock(SaveVaultUseCase.class); private MoveVaultPositionUseCase moveVaultPositionUseCase = Mockito.mock(MoveVaultPositionUseCase.class); - private ChangePasswordUseCase changePasswordUseCase = Mockito.mock(ChangePasswordUseCase.class); - private RemoveStoredVaultPasswordsUseCase removeStoredVaultPasswordsUseCase = Mockito.mock(RemoveStoredVaultPasswordsUseCase.class); private DoLicenseCheckUseCase doLicenceCheckUsecase = Mockito.mock(DoLicenseCheckUseCase.class); private DoUpdateCheckUseCase updateCheckUseCase = Mockito.mock(DoUpdateCheckUseCase.class); private DoUpdateUseCase updateUseCase = Mockito.mock(DoUpdateUseCase.class); @@ -126,15 +117,11 @@ public class VaultListPresenterTest { renameVaultUseCase, // lockVaultUseCase, // getDecryptedCloudForVaultUseCase, // - prepareUnlockUseCase, // - unlockVaultUseCase, // getRootFolderUseCase, // addExistingVaultWorkflow, // createNewVaultWorkflow, // saveVaultUseCase, // moveVaultPositionUseCase, // - changePasswordUseCase, // - removeStoredVaultPasswordsUseCase, // doLicenceCheckUsecase, // updateCheckUseCase, // updateUseCase, // @@ -241,43 +228,4 @@ public class VaultListPresenterTest { Mockito.any()); } - @Test - public void testOnUnlockCanceled() { - inTest.onUnlockCanceled(); - - verify(prepareUnlockUseCase).unsubscribe(); - verify(unlockVaultUseCase).cancel(); - } - - @Test - public void testOnVaultLockedClicked() { - ArgumentCaptor> captor = ArgumentCaptor.forClass(ResultHandler.class); - - when(lockVaultUseCase.withVault(AN_UNLOCKED_VAULT_MODEL.toVault())).thenReturn(lockVaultUseCaseLauncher); - - inTest.onVaultLockClicked(AN_UNLOCKED_VAULT_MODEL); - - verify(lockVaultUseCaseLauncher).run(captor.capture()); - captor.getValue().onSuccess(AN_UNLOCKED_VAULT_MODEL.toVault()); - verify(vaultListView).addOrUpdateVault(AN_UNLOCKED_VAULT_MODEL); - } - - @Test - public void onVaultClickedWithCloudAndLocked() { - ArgumentCaptor> captor = ArgumentCaptor.forClass(ResultHandler.class); - - when(prepareUnlockUseCase.withVault(ANOTHER_VAULT_MODEL_WITH_CLOUD.toVault())) // - .thenReturn(prepareUnlockUseCaseLauncher); - when(unlockToken.getVault()) // - .thenReturn(ANOTHER_VAULT_MODEL_WITH_CLOUD.toVault()); - - inTest.onVaultClicked(ANOTHER_VAULT_MODEL_WITH_CLOUD); - - verify(prepareUnlockUseCaseLauncher).run(captor.capture()); - captor.getValue().onSuccess(unlockToken); - - verify(vaultListView).addOrUpdateVault(ANOTHER_VAULT_MODEL_WITH_CLOUD); - verify(vaultListView).showEnterPasswordDialog(ANOTHER_VAULT_MODEL_WITH_CLOUD); - } - } From 6df05fd95bebdf18aeee59eea28a4362382db8ca Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 7 Apr 2021 15:40:57 +0200 Subject: [PATCH 002/135] Simplify exists check in CloudImpls:write Switching if (exists(file) && !replace) to if (!replace && exists(file)) --- .../java/org/cryptomator/data/cloud/dropbox/DropboxImpl.java | 2 +- .../org/cryptomator/data/cloud/local/file/LocalStorageImpl.java | 2 +- .../storageaccessframework/LocalStorageAccessFrameworkImpl.java | 2 +- .../java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.java | 2 +- .../main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.java | 2 +- .../org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.java b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.java index f886857f..ed9234e1 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/dropbox/DropboxImpl.java @@ -195,7 +195,7 @@ class DropboxImpl { } public DropboxFile write(DropboxFile file, DataSource data, final ProgressAware progressAware, boolean replace, long size) throws AuthenticationException, DbxException, IOException, CloudNodeAlreadyExistsException { - if (exists(file) && !replace) { + if (!replace && exists(file)) { throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); } diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.java b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.java index b893e5cd..ba973ebb 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/local/file/LocalStorageImpl.java @@ -140,7 +140,7 @@ class LocalStorageImpl { } public LocalFile write(final CloudFile file, DataSource data, final ProgressAware progressAware, boolean replace, final long size) throws IOException, BackendException { - if (exists(file) && !replace) { + if (!replace && exists(file)) { throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); } diff --git a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.java b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.java index 44f7cde6..1143a51e 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/local/storageaccessframework/LocalStorageAccessFrameworkImpl.java @@ -427,7 +427,7 @@ class LocalStorageAccessFrameworkImpl { progressAware.onProgress(Progress.started(UploadState.upload(file))); Optional fileUri = existingFileUri(file); - if (fileUri.isPresent() && !replace) { + if (!replace && fileUri.isPresent()) { throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); } diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.java index c104da55..f6c7ef62 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.java @@ -242,7 +242,7 @@ class OnedriveImpl { } public OnedriveFile write(final OnedriveFile file, DataSource data, final ProgressAware progressAware, boolean replace, final long size) throws BackendException { - if (exists(file) && !replace) { + if (!replace && exists(file)) { throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); } diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.java index 4d4a3dcc..9a6d49a7 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.java @@ -138,7 +138,7 @@ class WebDavImpl { public WebDavFile write(final WebDavFile uploadFile, DataSource data, final ProgressAware progressAware, boolean replace, final long size) // throws BackendException, IOException { - if (exists(uploadFile) && !replace) { + if (!replace && exists(uploadFile)) { throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); } diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.java b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.java index 1eedb5ed..a13487f8 100644 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.java +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.java @@ -255,7 +255,7 @@ class GoogleDriveImpl { public GoogleDriveFile write(final GoogleDriveFile file, DataSource data, final ProgressAware progressAware, boolean replace, final long size) // throws IOException, BackendException { - if (exists(file) && !replace) { + if (!replace && exists(file)) { throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false"); } From 3fef796546826d4b121354b57adc67e7f503fb50 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 7 Apr 2021 16:46:24 +0200 Subject: [PATCH 003/135] Add vault format 8 support --- buildsystem/dependencies.gradle | 4 +- .../InterceptingCloudContentRepository.java | 75 +---- .../crypto/CryptoCloudContentRepository.java | 7 +- .../data/cloud/crypto/CryptoCloudFactory.java | 203 ++---------- .../cloud/crypto/CryptoCloudProvider.java | 27 ++ .../data/cloud/crypto/CryptoConstants.java | 18 +- .../cloud/crypto/CryptoImplDecorator.java | 4 +- .../cloud/crypto/CryptoImplVaultFormat7.java | 11 +- .../cloud/crypto/CryptoImplVaultFormat8.java | 16 + .../crypto/CryptoImplVaultFormatPre7.java | 9 +- .../crypto/MasterkeyCryptoCloudProvider.java | 312 ++++++++++++++++++ .../data/cloud/crypto/VaultCipherCombo.java | 34 ++ .../data/cloud/crypto/VaultConfig.kt | 148 +++++++++ .../data/db/entities/VaultEntity.java | 4 +- .../data/repository/CloudRepositoryImpl.java | 26 +- .../data/repository/RepositoryModule.java | 10 - .../domain/UnverifiedVaultConfig.kt | 6 + .../java/org/cryptomator/domain/Vault.java | 29 +- ...UnsupportedMasterkeyLocationException.java | 19 ++ .../vaultconfig/VaultConfigLoadException.java | 16 + .../vaultconfig/VaultKeyInvalidException.java | 6 + .../VaultVersionMismatchException.java | 11 + .../domain/repository/CloudRepository.java | 14 +- .../domain/usecases/DoLicenseCheck.java | 6 +- .../domain/usecases/vault/ChangePassword.java | 11 +- .../usecases/vault/CheckVaultPassword.java | 8 +- .../vault/GetUnverifiedVaultConfig.java | 36 ++ .../domain/usecases/vault/PrepareUnlock.java | 8 +- ...lt.java => UnlockVaultUsingMasterkey.java} | 12 +- ...ava => UnlockVaultUsingMasterkeyTest.java} | 19 +- .../presentation/model/VaultModel.kt | 6 +- .../presenter/UnlockVaultPresenter.kt | 70 ++-- .../ui/activity/UnlockVaultActivity.kt | 18 +- .../ui/activity/view/UnlockVaultView.kt | 3 +- .../ui/dialog/ChangePasswordDialog.kt | 9 +- .../workflow/AddExistingVaultWorkflow.java | 4 +- 36 files changed, 887 insertions(+), 332 deletions(-) create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudProvider.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/VaultCipherCombo.java create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt create mode 100644 domain/src/main/java/org/cryptomator/domain/UnverifiedVaultConfig.kt create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/UnsupportedMasterkeyLocationException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultConfigLoadException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultKeyInvalidException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultVersionMismatchException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/vault/GetUnverifiedVaultConfig.java rename domain/src/main/java/org/cryptomator/domain/usecases/vault/{UnlockVault.java => UnlockVaultUsingMasterkey.java} (64%) rename domain/src/test/java/org/cryptomator/domain/usecases/vault/{UnlockVaultTest.java => UnlockVaultUsingMasterkeyTest.java} (56%) diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 09756da3..56bad523 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -6,7 +6,7 @@ allprojects { ext { androidBuildToolsVersion = "29.0.2" - androidMinSdkVersion = 23 + androidMinSdkVersion = 24 androidTargetSdkVersion = 29 androidCompileSdkVersion = 29 @@ -49,7 +49,7 @@ ext { // cloud provider libs // do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x - cryptolibVersion = '1.3.0' + cryptolibVersion = '2.0.0-beta6' dropboxVersion = '3.2.0' diff --git a/data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.java index b5a03ded..734bd80c 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.java +++ b/data/src/main/java/org/cryptomator/data/cloud/InterceptingCloudContentRepository.java @@ -30,10 +30,7 @@ public abstract class InterceptingCloudContentRepository size) throws BackendException { try { return delegate.file(parent, name, size); - } catch (BackendException e) { - throwWrappedIfRequired(e); - throw e; - } catch (RuntimeException e) { + } catch (BackendException | RuntimeException e) { throwWrappedIfRequired(e); throw e; } @@ -82,10 +70,7 @@ public abstract class InterceptingCloudContentRepository list(DirType folder) throws BackendException { try { return delegate.list(folder); - } catch (BackendException e) { - throwWrappedIfRequired(e); - throw e; - } catch (RuntimeException e) { + } catch (BackendException | RuntimeException e) { throwWrappedIfRequired(e); throw e; } @@ -121,10 +100,7 @@ public abstract class InterceptingCloudContentRepository progressAware, boolean replace, long size) throws BackendException { try { return delegate.write(file, data, progressAware, replace, size); - } catch (BackendException e) { - throwWrappedIfRequired(e); - throw e; - } catch (RuntimeException e) { + } catch (BackendException | RuntimeException e) { throwWrappedIfRequired(e); throw e; } @@ -173,10 +140,7 @@ public abstract class InterceptingCloudContentRepository encryptedTmpFile, OutputStream data, ProgressAware progressAware) throws BackendException { try { delegate.read(file, encryptedTmpFile, data, progressAware); - } catch (BackendException e) { - throwWrappedIfRequired(e); - throw e; - } catch (RuntimeException e) { + } catch (BackendException | RuntimeException e) { throwWrappedIfRequired(e); throw e; } @@ -186,10 +150,7 @@ public abstract class InterceptingCloudContentRepository unverifiedVaultConfig(Vault vault) throws BackendException { CloudFolder vaultLocation = cloudContentRepository.resolve(vault.getCloud(), vault.getPath()); - return createUnlockToken(vault, vaultLocation); + String jwt = new String(readConfigFileData(vaultLocation), UTF_8); + return Optional.of(VaultConfig.decode(jwt)); } - private UnlockTokenImpl createUnlockToken(Vault vault, CloudFolder location) throws BackendException { - byte[] keyFileData = readKeyFileData(location); - UnlockTokenImpl unlockToken = new UnlockTokenImpl(vault, keyFileData); - assertVaultVersionIsSupported(unlockToken.getKeyFile().getVersion()); - return unlockToken; + private byte[] readConfigFileData(CloudFolder location) throws BackendException { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME); + cloudContentRepository.read(vaultFile, Optional.empty(), data, NO_OP_PROGRESS_AWARE); + return data.toByteArray(); } - private Cryptor cryptorFor(KeyFile keyFile, CharSequence password) { - return cryptorProvider.createFromKeyFile(keyFile, normalizePassword(password, keyFile.getVersion()), keyFile.getVersion()); + public Vault unlock(Vault vault, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException { + return cryptoCloudProvider(unverifiedVaultConfig).unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag); } - private CloudFolder vaultLocation(Vault vault) throws BackendException { - return cloudContentRepository.resolve(vault.getCloud(), vault.getPath()); + public Vault unlock(UnlockToken token, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException { + return cryptoCloudProvider(unverifiedVaultConfig).unlock(token, unverifiedVaultConfig, password, cancelledFlag); } - public boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException { - try { - // create a cryptor, which checks the password, then destroy it immediately - cryptorFor(createUnlockToken(vault).getKeyFile(), password).destroy(); - return true; - } catch (InvalidPassphraseException e) { - return false; - } + public UnlockToken createUnlockToken(Vault vault, Optional unverifiedVaultConfig) throws BackendException { + return cryptoCloudProvider(unverifiedVaultConfig).createUnlockToken(vault, unverifiedVaultConfig); + } + + public boolean isVaultPasswordValid(Vault vault, Optional unverifiedVaultConfig, CharSequence password) throws BackendException { + return cryptoCloudProvider(unverifiedVaultConfig).isVaultPasswordValid(vault, unverifiedVaultConfig, password); } public void lock(Vault vault) { cryptoCloudContentRepositoryFactory.deregisterCryptor(vault); } - private void assertVaultVersionIsSupported(int version) { - if (version < MIN_VAULT_VERSION) { - throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION); - } else if (version > MAX_VAULT_VERSION) { - throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION); - } + public void changePassword(Vault vault, Optional unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException { + cryptoCloudProvider(unverifiedVaultConfig).changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword); } - private void writeKeyFile(CloudFolder location, KeyFile keyFile) throws BackendException { - byte[] data = keyFile.serialize(); - cloudContentRepository.write(masterkeyFile(location), ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, false, data.length); - } - - private byte[] readKeyFileData(CloudFolder location) throws BackendException { - ByteArrayOutputStream data = new ByteArrayOutputStream(); - cloudContentRepository.read(masterkeyFile(location), Optional.empty(), data, NO_OP_PROGRESS_AWARE); - return data.toByteArray(); - } - - private CloudFile masterkeyFile(CloudFolder location) throws BackendException { - return cloudContentRepository.file(location, MASTERKEY_FILE_NAME); - } - - private CloudFile masterkeyBackupFile(CloudFolder location, byte[] data) throws BackendException { - String fileName = MASTERKEY_FILE_NAME + BackupFileIdSuffixGenerator.generate(data) + MASTERKEY_BACKUP_FILE_EXT; - return cloudContentRepository.file(location, fileName); - } - - public void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException { - CloudFolder vaultLocation = vaultLocation(vault); - ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream(); - cloudContentRepository.read(masterkeyFile(vaultLocation), Optional.empty(), dataOutputStream, NO_OP_PROGRESS_AWARE); - - byte[] data = dataOutputStream.toByteArray(); - int vaultVersion = KeyFile.parse(data).getVersion(); - - createBackupMasterKeyFile(data, vaultLocation); - createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, vaultLocation); - } - - private void createBackupMasterKeyFile(byte[] data, CloudFolder vaultLocation) throws BackendException { - cloudContentRepository.write( // - masterkeyBackupFile(vaultLocation, data), // - ByteArrayDataSource.from(data), // - NO_OP_PROGRESS_AWARE, // - true, // - data.length); - } - - private void createNewMasterKeyFile(byte[] data, int vaultVersion, String oldPassword, String newPassword, CloudFolder vaultLocation) throws BackendException { - byte[] newMasterKeyFile = Cryptors.changePassphrase(cryptorProvider, // - data, // - normalizePassword(oldPassword, vaultVersion), // - normalizePassword(newPassword, vaultVersion)); - cloudContentRepository.write(masterkeyFile(vaultLocation), // - ByteArrayDataSource.from(newMasterKeyFile), // - NO_OP_PROGRESS_AWARE, // - true, // - newMasterKeyFile.length); - } - - private CharSequence normalizePassword(CharSequence password, int vaultVersion) { - if (vaultVersion >= VERSION_WITH_NORMALIZED_PASSWORDS) { - return normalize(password, Normalizer.Form.NFC); + private CryptoCloudProvider cryptoCloudProvider(Optional unverifiedVaultConfigOptional) { + if (unverifiedVaultConfigOptional.isPresent()) { + switch (unverifiedVaultConfigOptional.get().getKeyId().getScheme()) { + case MASTERKEY_SCHEME: { + return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory); + } + default: throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme())); + } } else { - return password; + return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory); } } - - private static class UnlockTokenImpl implements UnlockToken { - - private final Vault vault; - private final byte[] keyFileData; - - private UnlockTokenImpl(Vault vault, byte[] keyFileData) { - this.vault = vault; - this.keyFileData = keyFileData; - } - - @Override - public Vault getVault() { - return vault; - } - - public KeyFile getKeyFile() { - return KeyFile.parse(keyFileData); - } - } - } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudProvider.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudProvider.java new file mode 100644 index 00000000..a2c2d133 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudProvider.java @@ -0,0 +1,27 @@ +package org.cryptomator.data.cloud.crypto; + +import org.cryptomator.domain.CloudFolder; +import org.cryptomator.domain.UnverifiedVaultConfig; +import org.cryptomator.domain.Vault; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.usecases.cloud.Flag; +import org.cryptomator.domain.usecases.vault.UnlockToken; +import org.cryptomator.util.Optional; + +public interface CryptoCloudProvider { + + void create(CloudFolder location, CharSequence password) throws BackendException; + + Vault unlock(Vault vault, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException; + + UnlockToken createUnlockToken(Vault vault, Optional unverifiedVaultConfig) throws BackendException; + + Vault unlock(UnlockToken token, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException; + + boolean isVaultPasswordValid(Vault vault, Optional unverifiedVaultConfig, CharSequence password) throws BackendException; + + void lock(Vault vault); + + void changePassword(Vault vault, Optional unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException; + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.java index 45bfabed..38987362 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.java @@ -1,14 +1,26 @@ package org.cryptomator.data.cloud.crypto; -class CryptoConstants { +public class CryptoConstants { + + public static final String MASTERKEY_SCHEME = "masterkeyfile"; + + static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator"; static final String ROOT_DIR_ID = ""; static final String DATA_DIR_NAME = "d"; - static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator"; + static final String VAULT_FILE_NAME = "vault.cryptomator"; static final String MASTERKEY_BACKUP_FILE_EXT = ".bkup"; - static final int MAX_VAULT_VERSION = 7; + static final int DEFAULT_MASTERKEY_FILE_VERSION = 999; + static final int MAX_VAULT_VERSION = 8; + static final int MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG = 7; static final int VERSION_WITH_NORMALIZED_PASSWORDS = 6; static final int MIN_VAULT_VERSION = 5; + static final int DEFAULT_MAX_FILE_NAME = 220; + + static final byte[] PEPPER = new byte[0]; + + static final VaultCipherCombo DEFAULT_CIPHER_COMBO = VaultCipherCombo.SIV_CTRMAC; + } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.java index 4bcbe5d6..43c279b4 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.java @@ -54,18 +54,20 @@ abstract class CryptoImplDecorator { final CloudContentRepository cloudContentRepository; final Context context; final DirIdCache dirIdCache; + final int maxFileNameLength; private final Supplier cryptor; private final CloudFolder storageLocation; private RootCryptoFolder root; - CryptoImplDecorator(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) { + CryptoImplDecorator(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int maxFileNameLength) { this.context = context; this.cryptor = cryptor; this.cloudContentRepository = cloudContentRepository; this.storageLocation = storageLocation; this.dirIdCache = dirIdCache; + this.maxFileNameLength = maxFileNameLength; } abstract CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException; diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java index 2cdc7467..2b84fdb1 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java @@ -51,9 +51,8 @@ import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE import static org.cryptomator.domain.usecases.cloud.Progress.progress; import static org.cryptomator.util.Encodings.UTF_8; -final class CryptoImplVaultFormat7 extends CryptoImplDecorator { +class CryptoImplVaultFormat7 extends CryptoImplDecorator { - private static final int SHORT_NAMES_MAX_LENGTH = 220; private static final String CLOUD_NODE_EXT = ".c9r"; private static final String LONG_NODE_FILE_EXT = ".c9s"; private static final String CLOUD_FOLDER_DIR_FILE_PRE = "dir"; @@ -65,7 +64,11 @@ final class CryptoImplVaultFormat7 extends CryptoImplDecorator { private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); CryptoImplVaultFormat7(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) { - super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache); + super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, CryptoConstants.DEFAULT_MAX_FILE_NAME); + } + + CryptoImplVaultFormat7(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int maxFileNameLength) { + super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, maxFileNameLength); } @Override @@ -82,7 +85,7 @@ final class CryptoImplVaultFormat7 extends CryptoImplDecorator { .fileNameCryptor() // .encryptFilename(BASE64, name, dirIdInfo(cryptoFolder).getId().getBytes(UTF_8)) + CLOUD_NODE_EXT; - if (ciphertextName.length() > SHORT_NAMES_MAX_LENGTH) { + if (ciphertextName.length() > maxFileNameLength) { ciphertextName = deflate(cryptoFolder, ciphertextName); } return ciphertextName; diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java new file mode 100644 index 00000000..356a4fb0 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java @@ -0,0 +1,16 @@ +package org.cryptomator.data.cloud.crypto; + +import android.content.Context; + +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.domain.CloudFolder; +import org.cryptomator.domain.repository.CloudContentRepository; +import org.cryptomator.util.Supplier; + +public class CryptoImplVaultFormat8 extends CryptoImplVaultFormat7 { + + CryptoImplVaultFormat8(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int maxFileNameLength) { + super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, maxFileNameLength); + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java index 4a635c3b..dcde522a 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java @@ -36,17 +36,16 @@ import static org.cryptomator.util.Encodings.UTF_8; final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator { - private static final int SHORT_NAMES_MAX_LENGTH = 129; + static final int MAX_FILE_NAME_LENGTH = 129; private static final String DIR_PREFIX = "0"; private static final String SYMLINK_PREFIX = "1S"; private static final String LONG_NAME_FILE_EXT = ".lng"; private static final String METADATA_DIR_NAME = "m"; - private static final BaseNCodec BASE32 = new Base32(); private static final Pattern BASE32_ENCRYPTED_NAME_PATTERN = Pattern.compile("^(0|1S)?(([A-Z2-7]{8})*[A-Z2-7=]{8})$"); CryptoImplVaultFormatPre7(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) { - super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache); + super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, MAX_FILE_NAME_LENGTH); } @Override @@ -75,7 +74,7 @@ final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator { private String encryptName(CryptoFolder cryptoParent, String name, String prefix) throws BackendException { String ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(name, dirIdInfo(cryptoParent).getId().getBytes(UTF_8)); - if (ciphertextName.length() > SHORT_NAMES_MAX_LENGTH) { + if (ciphertextName.length() > maxFileNameLength) { ciphertextName = deflate(ciphertextName); } return ciphertextName; @@ -140,7 +139,7 @@ final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator { if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) { try { ciphertextName = inflate(ciphertextName); - if (ciphertextName.length() <= SHORT_NAMES_MAX_LENGTH) { + if (ciphertextName.length() <= maxFileNameLength) { cloudFile = inflatePermanently(cloudFile, ciphertextName); } } catch (NoSuchCloudFileException e) { diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java new file mode 100644 index 00000000..d1720dd7 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java @@ -0,0 +1,312 @@ +package org.cryptomator.data.cloud.crypto; + +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.domain.CloudFile; +import org.cryptomator.domain.CloudFolder; +import org.cryptomator.domain.UnverifiedVaultConfig; +import org.cryptomator.domain.Vault; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.exception.CancellationException; +import org.cryptomator.domain.exception.FatalBackendException; +import org.cryptomator.domain.repository.CloudContentRepository; +import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource; +import org.cryptomator.domain.usecases.cloud.Flag; +import org.cryptomator.domain.usecases.vault.UnlockToken; +import org.cryptomator.util.Optional; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.security.SecureRandom; +import java.text.Normalizer; + +import static java.text.Normalizer.normalize; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_CIPHER_COMBO; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_MASTERKEY_FILE_VERSION; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_MAX_FILE_NAME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_BACKUP_FILE_EXT; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_FILE_NAME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MIN_VAULT_VERSION; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.PEPPER; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.ROOT_DIR_ID; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.VERSION_WITH_NORMALIZED_PASSWORDS; +import static org.cryptomator.data.cloud.crypto.VaultCipherCombo.SIV_CTRMAC; +import static org.cryptomator.domain.Vault.aCopyOf; +import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; +import static org.cryptomator.util.Encodings.UTF_8; + +public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { + + private final CloudContentRepository cloudContentRepository; + private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory; + + public MasterkeyCryptoCloudProvider(CloudContentRepository cloudContentRepository, // + CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) { + this.cloudContentRepository = cloudContentRepository; + this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory; + } + + @Override + public void create(CloudFolder location, CharSequence password) throws BackendException { + // 1. write masterkey: + Masterkey masterkey = Masterkey.generate(new SecureRandom()); + try (ByteArrayOutputStream data = new ByteArrayOutputStream()) { + new MasterkeyFileAccess(PEPPER, new SecureRandom()).persist(masterkey, data, password, DEFAULT_MASTERKEY_FILE_VERSION); + cloudContentRepository.write(legacyMasterkeyFile(location), ByteArrayDataSource.from(data.toByteArray()), NO_OP_PROGRESS_AWARE, false, data.size()); + } catch (IOException e) { + throw new FatalBackendException("Failed to write masterkey", e); + } + + // 2. initialize vault: + VaultConfig vaultConfig = new VaultConfig.VaultConfigBuilder() // + .vaultFormat(MAX_VAULT_VERSION) // + .cipherCombo(DEFAULT_CIPHER_COMBO) // + .keyId(URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME))) // + .maxFilenameLength(DEFAULT_MAX_FILE_NAME) // + .build(); + + byte[] encodedVaultConfig = vaultConfig.toToken(masterkey.getEncoded()).getBytes(UTF_8); + CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME); + cloudContentRepository.write(vaultFile, ByteArrayDataSource.from(encodedVaultConfig), NO_OP_PROGRESS_AWARE, false, encodedVaultConfig.length); + + // 3. create root folder: + createRootFolder(location, cryptorFor(masterkey, vaultConfig.getCipherCombo())); + } + + private void createRootFolder(CloudFolder location, Cryptor cryptor) throws BackendException { + CloudFolder dFolder = cloudContentRepository.folder(location, DATA_DIR_NAME); + dFolder = cloudContentRepository.create(dFolder); + String rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(ROOT_DIR_ID); + CloudFolder lvl1Folder = cloudContentRepository.folder(dFolder, rootDirHash.substring(0, 2)); + lvl1Folder = cloudContentRepository.create(lvl1Folder); + CloudFolder lvl2Folder = cloudContentRepository.folder(lvl1Folder, rootDirHash.substring(2)); + cloudContentRepository.create(lvl2Folder); + } + + @Override + public Vault unlock(Vault vault, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException { + return unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag); + } + + @Override + public Vault unlock(UnlockToken token, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException { + UnlockTokenImpl impl = (UnlockTokenImpl) token; + try { + Masterkey masterkey = impl.getKeyFile(password); + + int vaultFormat; + int maxFileNameLength; + Cryptor cryptor; + + if (unverifiedVaultConfig.isPresent()) { + VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get()); + vaultFormat = vaultConfig.getVaultFormat(); + assertVaultVersionIsSupported(vaultConfig.getVaultFormat()); + maxFileNameLength = vaultConfig.getMaxFilenameLength(); + cryptor = cryptorFor(masterkey, vaultConfig.getCipherCombo()); + } else { + vaultFormat = MasterkeyFileAccess.readAllegedVaultVersion(impl.keyFileData); + assertLegacyVaultVersionIsSupported(vaultFormat); + maxFileNameLength = vaultFormat > 6 ? CryptoConstants.DEFAULT_MAX_FILE_NAME : CryptoImplVaultFormatPre7.MAX_FILE_NAME_LENGTH; + cryptor = cryptorFor(masterkey, SIV_CTRMAC); + } + + + if (cancelledFlag.get()) { + throw new CancellationException(); + } + + Vault vault = aCopyOf(token.getVault()) // + .withUnlocked(true) // + .withFormat(vaultFormat) // + .withMaxFileNameLength(maxFileNameLength) + .build(); + + cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor); + + return vault; + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public UnlockTokenImpl createUnlockToken(Vault vault, Optional unverifiedVaultConfig) throws BackendException { + CloudFolder vaultLocation = vaultLocation(vault); + if (unverifiedVaultConfig.isPresent()) { + return createUnlockToken(vault, masterkeyFile(vaultLocation, unverifiedVaultConfig.get())); + } else { + return createUnlockToken(vault, legacyMasterkeyFile(vaultLocation)); + } + } + + private CloudFile masterkeyFile(CloudFolder vaultLocation, UnverifiedVaultConfig unverifiedVaultConfig) throws BackendException { + String path = unverifiedVaultConfig.getKeyId().getSchemeSpecificPart(); + // TODO / FIXME sanitize path and throw specific exception + //throw new UnsupportedMasterkeyLocationException(unverifiedVaultConfig); + return cloudContentRepository.file(vaultLocation, path); + } + + private CloudFile legacyMasterkeyFile(CloudFolder location) throws BackendException { + return cloudContentRepository.file(location, MASTERKEY_FILE_NAME); + } + + private UnlockTokenImpl createUnlockToken(Vault vault, CloudFile location) throws BackendException { + byte[] keyFileData = readKeyFileData(location); + UnlockTokenImpl unlockToken = new UnlockTokenImpl(vault, keyFileData); + return unlockToken; + } + + private byte[] readKeyFileData(CloudFile masterkeyFile) throws BackendException { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + cloudContentRepository.read(masterkeyFile, Optional.empty(), data, NO_OP_PROGRESS_AWARE); + return data.toByteArray(); + } + + private Cryptor cryptorFor(Masterkey keyFile, VaultCipherCombo vaultCipherCombo) { + return vaultCipherCombo.getCryptorProvider(new SecureRandom()).withKey(keyFile); + } + + @Override + public boolean isVaultPasswordValid(Vault vault, Optional unverifiedVaultConfig, CharSequence password) throws BackendException { + try { + // create a cryptor, which checks the password, then destroy it immediately + Masterkey masterkey = createUnlockToken(vault, unverifiedVaultConfig).getKeyFile(password); + VaultCipherCombo vaultCipherCombo; + if(unverifiedVaultConfig.isPresent()) { + VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get()); + assertVaultVersionIsSupported(vaultConfig.getVaultFormat()); + vaultCipherCombo = vaultConfig.getCipherCombo(); + } else { + int vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(masterkey.getEncoded()); + assertLegacyVaultVersionIsSupported(vaultVersion); + vaultCipherCombo = SIV_CTRMAC; + } + cryptorFor(masterkey, vaultCipherCombo).destroy(); + return true; + } catch (InvalidPassphraseException e) { + return false; + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public void lock(Vault vault) { + cryptoCloudContentRepositoryFactory.deregisterCryptor(vault); + } + + private void assertVaultVersionIsSupported(int version) { + if (version < MIN_VAULT_VERSION) { + throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION); + } else if (version > MAX_VAULT_VERSION) { + throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION); + } + } + + private void assertLegacyVaultVersionIsSupported(int version) { + if (version < MIN_VAULT_VERSION) { + throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION); + } else if (version > MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG) { + throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG); + } + } + + @Override + public void changePassword(Vault vault, Optional unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException { + CloudFolder vaultLocation = vaultLocation(vault); + ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream(); + + CloudFile masterkeyFile; + if (unverifiedVaultConfig.isPresent()) { + masterkeyFile = masterkeyFile(vaultLocation, unverifiedVaultConfig.get()); + } else { + masterkeyFile = legacyMasterkeyFile(vaultLocation); + } + + cloudContentRepository.read(masterkeyFile, Optional.empty(), dataOutputStream, NO_OP_PROGRESS_AWARE); + byte[] data = dataOutputStream.toByteArray(); + + int vaultVersion; + if (unverifiedVaultConfig.isPresent()) { + vaultVersion = unverifiedVaultConfig.get().getVaultFormat(); + assertVaultVersionIsSupported(vaultVersion); + } else { + try { + vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(data); + assertLegacyVaultVersionIsSupported(vaultVersion); + } catch (IOException e) { + throw new FatalBackendException("Failed to read legacy vault version", e); + } + } + + createBackupMasterKeyFile(data, masterkeyFile); + createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, masterkeyFile); + } + + private CloudFolder vaultLocation(Vault vault) throws BackendException { + return cloudContentRepository.resolve(vault.getCloud(), vault.getPath()); + } + + private void createBackupMasterKeyFile(byte[] data, CloudFile masterkeyFile) throws BackendException { + cloudContentRepository.write(masterkeyBackupFile(masterkeyFile, data), ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, true, data.length); + } + + private CloudFile masterkeyBackupFile(CloudFile masterkeyFile, byte[] data) throws BackendException { + String fileName = masterkeyFile.getName() + BackupFileIdSuffixGenerator.generate(data) + MASTERKEY_BACKUP_FILE_EXT; + return cloudContentRepository.file(masterkeyFile.getParent(), fileName); + } + + private void createNewMasterKeyFile(byte[] data, int vaultVersion, String oldPassword, String newPassword, CloudFile masterkeyFile) throws BackendException { + try { + byte[] newMasterKeyFile = new MasterkeyFileAccess(PEPPER, new SecureRandom()) // + .changePassphrase(data, normalizePassword(oldPassword, vaultVersion), normalizePassword(newPassword, vaultVersion)); + cloudContentRepository.write(masterkeyFile, // + ByteArrayDataSource.from(newMasterKeyFile), // + NO_OP_PROGRESS_AWARE, // + true, // + newMasterKeyFile.length); + } catch (IOException e) { + throw new FatalBackendException("Failed to read legacy vault version", e); + } + } + + private CharSequence normalizePassword(CharSequence password, int vaultVersion) { + if (vaultVersion >= VERSION_WITH_NORMALIZED_PASSWORDS) { + return normalize(password, Normalizer.Form.NFC); + } else { + return password; + } + } + + private static class UnlockTokenImpl implements UnlockToken { + + private final Vault vault; + private final byte[] keyFileData; + + private UnlockTokenImpl(Vault vault, byte[] keyFileData) { + this.vault = vault; + this.keyFileData = keyFileData; + } + + @Override + public Vault getVault() { + return vault; + } + + public Masterkey getKeyFile(CharSequence password) throws IOException { + return new MasterkeyFileAccess(PEPPER, new SecureRandom()).load(new ByteArrayInputStream(keyFileData), password); + } + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultCipherCombo.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultCipherCombo.java new file mode 100644 index 00000000..07096eba --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultCipherCombo.java @@ -0,0 +1,34 @@ +package org.cryptomator.data.cloud.crypto; + +import org.cryptomator.cryptolib.Cryptors; +import org.cryptomator.cryptolib.api.CryptorProvider; + +import java.security.SecureRandom; +import java.util.function.Function; + +/** + * A combination of different ciphers and/or cipher modes in a Cryptomator vault. + */ +public enum VaultCipherCombo { + /** + * AES-SIV for file name encryption + * AES-CTR + HMAC for content encryption + */ + SIV_CTRMAC(Cryptors::version1), + + /** + * AES-SIV for file name encryption + * AES-GCM for content encryption + */ + SIV_GCM(Cryptors::version2); + + private final Function cryptorProvider; + + VaultCipherCombo(Function cryptorProvider) { + this.cryptorProvider = cryptorProvider; + } + + public CryptorProvider getCryptorProvider(SecureRandom csprng) { + return cryptorProvider.apply(csprng); + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt new file mode 100644 index 00000000..fe1bcb79 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt @@ -0,0 +1,148 @@ +package org.cryptomator.data.cloud.crypto + +import org.cryptomator.domain.UnverifiedVaultConfig +import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException +import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException +import org.cryptomator.domain.exception.vaultconfig.VaultVersionMismatchException +import java.net.URI +import java.security.Key +import java.util.UUID +import io.jsonwebtoken.Claims +import io.jsonwebtoken.JwsHeader +import io.jsonwebtoken.JwtException +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SigningKeyResolverAdapter +import io.jsonwebtoken.security.Keys +import kotlin.properties.Delegates + +class VaultConfig private constructor(builder: VaultConfigBuilder) { + + val keyId: URI + val id: String + val vaultFormat: Int + val cipherCombo: VaultCipherCombo + val maxFilenameLength: Int + + fun toToken(rawKey: ByteArray): String { + return Jwts.builder() + .setHeaderParam(JSON_KEY_ID, keyId.toASCIIString()) // + .setId(id) // + .claim(JSON_KEY_VAULTFORMAT, vaultFormat) // + .claim(JSON_KEY_CIPHERCONFIG, cipherCombo.name) // + .claim(JSON_KEY_MAXFILENAMELEN, maxFilenameLength) // + .signWith(Keys.hmacShaKeyFor(rawKey)) // + .compact() + } + + class VaultConfigBuilder { + internal var id: String = UUID.randomUUID().toString() + internal var vaultFormat = CryptoConstants.MAX_VAULT_VERSION; + internal var cipherCombo = VaultCipherCombo.SIV_CTRMAC + internal var maxFilenameLength = CryptoConstants.DEFAULT_MAX_FILE_NAME; + lateinit var keyId: URI + + fun keyId(keyId: URI): VaultConfigBuilder { + this.keyId = keyId + return this + } + + fun cipherCombo(cipherCombo: VaultCipherCombo): VaultConfigBuilder { + this.cipherCombo = cipherCombo + return this + } + + fun maxFilenameLength(maxFilenameLength: Int): VaultConfigBuilder { + this.maxFilenameLength = maxFilenameLength + return this + } + + fun id(id: String): VaultConfigBuilder { + this.id = id + return this + } + + fun vaultFormat(vaultFormat: Int): VaultConfigBuilder { + this.vaultFormat = vaultFormat + return this + } + + fun build(): VaultConfig { + return VaultConfig(this) + } + } + + companion object { + private const val JSON_KEY_VAULTFORMAT = "format" + private const val JSON_KEY_CIPHERCONFIG = "cipherCombo" + private const val JSON_KEY_MAXFILENAMELEN = "maxFilenameLen" + private const val JSON_KEY_ID = "kid" + + @JvmStatic + @Throws(VaultConfigLoadException::class) + fun decode(token: String): UnverifiedVaultConfig { + val unverifiedSigningKeyResolver = UnverifiedSigningKeyResolver() + + // At this point we can't verify the signature because we don't have the masterkey yet. + try { + Jwts.parserBuilder().setSigningKeyResolver(unverifiedSigningKeyResolver).build().parse(token) + } catch (e: IllegalArgumentException) { + return UnverifiedVaultConfig(token, unverifiedSigningKeyResolver.keyId, unverifiedSigningKeyResolver.vaultFormat) + } + throw VaultConfigLoadException("Failed to load vaultconfig") + } + + @JvmStatic + @Throws(VaultKeyInvalidException::class, VaultVersionMismatchException::class, VaultConfigLoadException::class) + fun verify(rawKey: ByteArray, unverifiedVaultConfig: UnverifiedVaultConfig): VaultConfig { + return try { + val parser = Jwts // + .parserBuilder() // + .setSigningKey(rawKey) // + .require(JSON_KEY_VAULTFORMAT, unverifiedVaultConfig.vaultFormat) // + .build() // + .parseClaimsJws(unverifiedVaultConfig.jwt) + + val vaultConfigBuilder = createVaultConfig() // + .keyId(unverifiedVaultConfig.keyId) + .id(parser.header[JSON_KEY_ID] as String) // + .cipherCombo(VaultCipherCombo.valueOf(parser.body.get(JSON_KEY_CIPHERCONFIG, String::class.java))) // + .vaultFormat(unverifiedVaultConfig.vaultFormat) // + .maxFilenameLength(parser.body[JSON_KEY_MAXFILENAMELEN] as Int) + + VaultConfig(vaultConfigBuilder) + /*} catch (SignatureVerificationException e) { + throw new VaultKeyInvalidException(); + } catch (InvalidClaimException e) { + throw new VaultVersionMismatchException("Vault config not for version " + expectedVaultFormat); + } catch (JWTVerificationException e) { + throw new VaultConfigLoadException("Failed to verify vault config: " + unverifiedConfig.getToken()); + */ + } catch (e: JwtException) { + throw VaultConfigLoadException("Failed to verify vault config", e) + } + } + + fun createVaultConfig(): VaultConfigBuilder { + return VaultConfigBuilder() + } + } + + private class UnverifiedSigningKeyResolver : SigningKeyResolverAdapter() { + lateinit var keyId: URI + var vaultFormat: Int by Delegates.notNull() + + override fun resolveSigningKey(jwsHeader: JwsHeader<*>, claims: Claims): Key? { + keyId = URI.create(jwsHeader.keyId) + vaultFormat = claims[JSON_KEY_VAULTFORMAT] as Int + return null + } + } + + init { + id = builder.id + keyId = builder.keyId + vaultFormat = builder.vaultFormat + cipherCombo = builder.cipherCombo + maxFilenameLength = builder.maxFilenameLength + } +} diff --git a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java index af12e1ef..b8d683fc 100644 --- a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java +++ b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java @@ -182,9 +182,7 @@ public class VaultEntity extends DatabaseEntity { this.position = position; } - /** - * called by internal mechanisms, do not call yourself. - */ + /** called by internal mechanisms, do not call yourself. */ @Generated(hash = 674742652) public void __setDaoSession(DaoSession daoSession) { this.daoSession = daoSession; diff --git a/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java b/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java index f3dfbf8f..5ddafba6 100644 --- a/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java @@ -7,11 +7,13 @@ import org.cryptomator.data.db.mappers.CloudEntityMapper; import org.cryptomator.domain.Cloud; import org.cryptomator.domain.CloudFolder; import org.cryptomator.domain.CloudType; +import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.domain.usecases.cloud.Flag; import org.cryptomator.domain.usecases.vault.UnlockToken; +import org.cryptomator.util.Optional; import java.util.ArrayList; import java.util.List; @@ -92,26 +94,30 @@ class CloudRepositoryImpl implements CloudRepository { return cryptoCloudFactory.decryptedViewOf(vault); } + public Optional unverifiedVaultConfig(Vault vault) throws BackendException { + return cryptoCloudFactory.unverifiedVaultConfig(vault); + } + @Override - public Cloud unlock(Vault vault, CharSequence password, Flag cancelledFlag) throws BackendException { - Vault vaultWithVersion = cryptoCloudFactory.unlock(vault, password, cancelledFlag); + public Cloud unlock(Vault vault, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException { + Vault vaultWithVersion = cryptoCloudFactory.unlock(vault, unverifiedVaultConfig, password, cancelledFlag); return decryptedViewOf(vaultWithVersion); } @Override - public Cloud unlock(UnlockToken token, CharSequence password, Flag cancelledFlag) throws BackendException { - Vault vaultWithVersion = cryptoCloudFactory.unlock(token, password, cancelledFlag); + public Cloud unlock(UnlockToken token, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException { + Vault vaultWithVersion = cryptoCloudFactory.unlock(token, unverifiedVaultConfig, password, cancelledFlag); return decryptedViewOf(vaultWithVersion); } @Override - public UnlockToken prepareUnlock(Vault vault) throws BackendException { - return cryptoCloudFactory.createUnlockToken(vault); + public UnlockToken prepareUnlock(Vault vault, Optional unverifiedVaultConfig) throws BackendException { + return cryptoCloudFactory.createUnlockToken(vault, unverifiedVaultConfig); } @Override - public boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException { - return cryptoCloudFactory.isVaultPasswordValid(vault, password); + public boolean isVaultPasswordValid(Vault vault, Optional unverifiedVaultConfig, CharSequence password) throws BackendException { + return cryptoCloudFactory.isVaultPasswordValid(vault, unverifiedVaultConfig, password); } @Override @@ -121,8 +127,8 @@ class CloudRepositoryImpl implements CloudRepository { } @Override - public void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException { - cryptoCloudFactory.changePassword(vault, oldPassword, newPassword); + public void changePassword(Vault vault, Optional unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException { + cryptoCloudFactory.changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword); } } diff --git a/data/src/main/java/org/cryptomator/data/repository/RepositoryModule.java b/data/src/main/java/org/cryptomator/data/repository/RepositoryModule.java index d45e28d4..7bd1fff9 100644 --- a/data/src/main/java/org/cryptomator/data/repository/RepositoryModule.java +++ b/data/src/main/java/org/cryptomator/data/repository/RepositoryModule.java @@ -1,14 +1,10 @@ package org.cryptomator.data.repository; -import org.cryptomator.cryptolib.Cryptors; -import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.domain.repository.CloudContentRepository; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.domain.repository.UpdateCheckRepository; import org.cryptomator.domain.repository.VaultRepository; -import java.security.SecureRandom; - import javax.inject.Singleton; import dagger.Module; @@ -17,12 +13,6 @@ import dagger.Provides; @Module public class RepositoryModule { - @Singleton - @Provides - public CryptorProvider provideCryptorProvider() { - return Cryptors.version1(new SecureRandom()); - } - @Singleton @Provides public CloudRepository provideCloudRepository(CloudRepositoryImpl cloudRepository) { diff --git a/domain/src/main/java/org/cryptomator/domain/UnverifiedVaultConfig.kt b/domain/src/main/java/org/cryptomator/domain/UnverifiedVaultConfig.kt new file mode 100644 index 00000000..2bb8b14d --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/UnverifiedVaultConfig.kt @@ -0,0 +1,6 @@ +package org.cryptomator.domain + +import java.io.Serializable +import java.net.URI + +class UnverifiedVaultConfig(val jwt: String, val keyId: URI, val vaultFormat: Int) : Serializable diff --git a/domain/src/main/java/org/cryptomator/domain/Vault.java b/domain/src/main/java/org/cryptomator/domain/Vault.java index 4f70be7b..4b84eecb 100644 --- a/domain/src/main/java/org/cryptomator/domain/Vault.java +++ b/domain/src/main/java/org/cryptomator/domain/Vault.java @@ -12,7 +12,8 @@ public class Vault implements Serializable { private final CloudType cloudType; private final boolean unlocked; private final String password; - private final int version; + private final int format; + private final int maxFileNameLength; private final int position; private Vault(Builder builder) { @@ -23,7 +24,8 @@ public class Vault implements Serializable { this.unlocked = builder.unlocked; this.cloudType = builder.cloudType; this.password = builder.password; - this.version = builder.version; + this.format = builder.format; + this.maxFileNameLength = builder.maxFileNameLength; this.position = builder.position; } @@ -40,7 +42,8 @@ public class Vault implements Serializable { .withPath(vault.getPath()) // .withUnlocked(vault.isUnlocked()) // .withSavedPassword(vault.getPassword()) // - .withVersion(vault.getVersion()) // + .withFormat(vault.getFormat()) // + .withMaxFileNameLength(vault.getMaxFileNameLength()) // .withPosition(vault.getPosition()); } @@ -72,8 +75,12 @@ public class Vault implements Serializable { return password; } - public int getVersion() { - return version; + public int getFormat() { + return format; + } + + public int getMaxFileNameLength() { + return maxFileNameLength; } public int getPosition() { @@ -109,7 +116,8 @@ public class Vault implements Serializable { private CloudType cloudType; private boolean unlocked; private String password; - private int version = -1; + private int format = -1; + private int maxFileNameLength = -1; private int position = -1; private Builder() { @@ -176,8 +184,13 @@ public class Vault implements Serializable { return this; } - public Builder withVersion(int version) { - this.version = version; + public Builder withFormat(int version) { + this.format = version; + return this; + } + + public Builder withMaxFileNameLength(int maxFileNameLength) { + this.maxFileNameLength = maxFileNameLength; return this; } diff --git a/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/UnsupportedMasterkeyLocationException.java b/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/UnsupportedMasterkeyLocationException.java new file mode 100644 index 00000000..12c8b0d0 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/UnsupportedMasterkeyLocationException.java @@ -0,0 +1,19 @@ +package org.cryptomator.domain.exception.vaultconfig; + +import org.cryptomator.domain.UnverifiedVaultConfig; +import org.cryptomator.domain.exception.BackendException; + +import io.jsonwebtoken.JwtException; + +public class UnsupportedMasterkeyLocationException extends BackendException { + + UnverifiedVaultConfig unverifiedVaultConfig; + + public UnsupportedMasterkeyLocationException(UnverifiedVaultConfig unverifiedVaultConfig) { + this.unverifiedVaultConfig = unverifiedVaultConfig; + } + + public UnsupportedMasterkeyLocationException(String message, JwtException e) { + super(message, e); + } +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultConfigLoadException.java b/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultConfigLoadException.java new file mode 100644 index 00000000..149a3c42 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultConfigLoadException.java @@ -0,0 +1,16 @@ +package org.cryptomator.domain.exception.vaultconfig; + +import org.cryptomator.domain.exception.BackendException; + +import io.jsonwebtoken.JwtException; + +public class VaultConfigLoadException extends BackendException { + + public VaultConfigLoadException(String message) { + super(message); + } + + public VaultConfigLoadException(String message, JwtException e) { + super(message, e); + } +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultKeyInvalidException.java b/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultKeyInvalidException.java new file mode 100644 index 00000000..a5be4f7e --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultKeyInvalidException.java @@ -0,0 +1,6 @@ +package org.cryptomator.domain.exception.vaultconfig; + +import org.cryptomator.domain.exception.BackendException; + +public class VaultKeyInvalidException extends BackendException { +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultVersionMismatchException.java b/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultVersionMismatchException.java new file mode 100644 index 00000000..35e29eb7 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultVersionMismatchException.java @@ -0,0 +1,11 @@ +package org.cryptomator.domain.exception.vaultconfig; + +import org.cryptomator.domain.exception.BackendException; + +public class VaultVersionMismatchException extends BackendException { + + public VaultVersionMismatchException(String message) { + super(message); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/repository/CloudRepository.java b/domain/src/main/java/org/cryptomator/domain/repository/CloudRepository.java index 841d6473..55e71c98 100644 --- a/domain/src/main/java/org/cryptomator/domain/repository/CloudRepository.java +++ b/domain/src/main/java/org/cryptomator/domain/repository/CloudRepository.java @@ -3,10 +3,12 @@ package org.cryptomator.domain.repository; import org.cryptomator.domain.Cloud; import org.cryptomator.domain.CloudFolder; import org.cryptomator.domain.CloudType; +import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.usecases.cloud.Flag; import org.cryptomator.domain.usecases.vault.UnlockToken; +import org.cryptomator.util.Optional; import java.util.List; @@ -24,16 +26,18 @@ public interface CloudRepository { Cloud decryptedViewOf(Vault vault) throws BackendException; - boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException; + boolean isVaultPasswordValid(Vault vault, Optional unverifiedVaultConfig, CharSequence password) throws BackendException; void lock(Vault vault) throws BackendException; - void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException; + void changePassword(Vault vault, Optional unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException; - UnlockToken prepareUnlock(Vault vault) throws BackendException; + Optional unverifiedVaultConfig(Vault vault) throws BackendException; - Cloud unlock(UnlockToken token, CharSequence password, Flag cancelledFlag) throws BackendException; + UnlockToken prepareUnlock(Vault vault, Optional vaultFile) throws BackendException; - Cloud unlock(Vault vault, CharSequence password, Flag cancelledFlag) throws BackendException; + Cloud unlock(UnlockToken token, Optional vaultFile, CharSequence password, Flag cancelledFlag) throws BackendException; + + Cloud unlock(Vault vault, Optional vaultFile, CharSequence password, Flag cancelledFlag) throws BackendException; } diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/DoLicenseCheck.java b/domain/src/main/java/org/cryptomator/domain/usecases/DoLicenseCheck.java index c2dcdc74..0d71c7ba 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/DoLicenseCheck.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/DoLicenseCheck.java @@ -37,8 +37,10 @@ public class DoLicenseCheck { try { final Claims claims = Jwts // - .parserBuilder().setSigningKey(getPublicKey()) // - .build().parseClaimsJws(license) // + .parserBuilder() // + .setSigningKey(getPublicKey()) // + .build() // + .parseClaimsJws(license) // .getBody(); return claims::getSubject; diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/ChangePassword.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/ChangePassword.java index 42d77176..51d663a3 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/ChangePassword.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/ChangePassword.java @@ -1,6 +1,7 @@ package org.cryptomator.domain.usecases.vault; import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.NoSuchCloudFileException; @@ -8,6 +9,7 @@ import org.cryptomator.domain.exception.NoSuchVaultException; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; +import org.cryptomator.util.Optional; import static org.cryptomator.util.ExceptionUtil.contains; @@ -16,23 +18,26 @@ class ChangePassword { private final CloudRepository cloudRepository; private final Vault vault; + private final Optional unverifiedVaultConfig; private final String oldPassword; - private final String newPassword; + private final String newPassword;; public ChangePassword(CloudRepository cloudRepository, // @Parameter Vault vault, // + @Parameter Optional unverifiedVaultConfig, // @Parameter String oldPassword, // @Parameter String newPassword) { this.cloudRepository = cloudRepository; this.vault = vault; + this.unverifiedVaultConfig = unverifiedVaultConfig; this.oldPassword = oldPassword; this.newPassword = newPassword; } public void execute() throws BackendException { try { - if (cloudRepository.isVaultPasswordValid(vault, oldPassword)) { - cloudRepository.changePassword(vault, oldPassword, newPassword); + if (cloudRepository.isVaultPasswordValid(vault, unverifiedVaultConfig, oldPassword)) { + cloudRepository.changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword); } else { throw new InvalidPassphraseException(); } diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/CheckVaultPassword.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/CheckVaultPassword.java index 006f843e..cc201583 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/CheckVaultPassword.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/CheckVaultPassword.java @@ -1,10 +1,12 @@ package org.cryptomator.domain.usecases.vault; +import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; +import org.cryptomator.util.Optional; @UseCase class CheckVaultPassword { @@ -12,15 +14,17 @@ class CheckVaultPassword { private final CloudRepository cloudRepository; private final Vault vault; private final String password; + private final Optional unverifiedVaultConfig; - public CheckVaultPassword(CloudRepository cloudRepository, @Parameter Vault vault, @Parameter String password) { + public CheckVaultPassword(CloudRepository cloudRepository, @Parameter Vault vault, @Parameter String password, @Parameter Optional unverifiedVaultConfig) { this.cloudRepository = cloudRepository; this.vault = vault; this.password = password; + this.unverifiedVaultConfig = unverifiedVaultConfig; } public Boolean execute() throws BackendException { - return cloudRepository.isVaultPasswordValid(vault, password); + return cloudRepository.isVaultPasswordValid(vault, unverifiedVaultConfig, password); } } diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/GetUnverifiedVaultConfig.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/GetUnverifiedVaultConfig.java new file mode 100644 index 00000000..4922008e --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/GetUnverifiedVaultConfig.java @@ -0,0 +1,36 @@ +package org.cryptomator.domain.usecases.vault; + +import org.cryptomator.domain.UnverifiedVaultConfig; +import org.cryptomator.domain.Vault; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.exception.NoSuchCloudFileException; +import org.cryptomator.domain.repository.CloudRepository; +import org.cryptomator.generator.Parameter; +import org.cryptomator.generator.UseCase; +import org.cryptomator.util.Optional; + +import static org.cryptomator.util.ExceptionUtil.contains; + +@UseCase +public class GetUnverifiedVaultConfig { + + private final CloudRepository cloudRepository; + private final Vault vault; + + public GetUnverifiedVaultConfig(CloudRepository cloudRepository, @Parameter Vault vault) { + this.cloudRepository = cloudRepository; + this.vault = vault; + } + + public Optional execute() throws BackendException { + try { + return cloudRepository.unverifiedVaultConfig(vault); + } catch (BackendException e) { + if (contains(e, NoSuchCloudFileException.class)) { + return Optional.empty(); + } + throw e; + } + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/PrepareUnlock.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/PrepareUnlock.java index 99f029f5..931d312d 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/PrepareUnlock.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/PrepareUnlock.java @@ -1,5 +1,6 @@ package org.cryptomator.domain.usecases.vault; +import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.NoSuchCloudFileException; @@ -7,6 +8,7 @@ import org.cryptomator.domain.exception.NoSuchVaultException; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; +import org.cryptomator.util.Optional; import static org.cryptomator.util.ExceptionUtil.contains; @@ -15,15 +17,17 @@ class PrepareUnlock { private final CloudRepository cloudRepository; private final Vault vault; + private final Optional unverifiedVaultConfig; - public PrepareUnlock(CloudRepository cloudRepository, @Parameter Vault vault) { + public PrepareUnlock(CloudRepository cloudRepository, @Parameter Vault vault, @Parameter Optional unverifiedVaultConfig) { this.cloudRepository = cloudRepository; this.vault = vault; + this.unverifiedVaultConfig = unverifiedVaultConfig; } public UnlockToken execute() throws BackendException { try { - return cloudRepository.prepareUnlock(vault); + return cloudRepository.prepareUnlock(vault, unverifiedVaultConfig); } catch (BackendException e) { if (contains(e, NoSuchCloudFileException.class)) { throw new NoSuchVaultException(vault, e); diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockVault.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkey.java similarity index 64% rename from domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockVault.java rename to domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkey.java index ce7616ee..c8e3e904 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockVault.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkey.java @@ -1,17 +1,20 @@ package org.cryptomator.domain.usecases.vault; import org.cryptomator.domain.Cloud; +import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.CloudRepository; import org.cryptomator.domain.usecases.cloud.Flag; import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; +import org.cryptomator.util.Optional; @UseCase -class UnlockVault { +class UnlockVaultUsingMasterkey { private final CloudRepository cloudRepository; private final VaultOrUnlockToken vaultOrUnlockToken; + private Optional unverifiedVaultConfig; private final String password; private volatile boolean cancelled; @@ -22,9 +25,10 @@ class UnlockVault { } }; - public UnlockVault(CloudRepository cloudRepository, @Parameter VaultOrUnlockToken vaultOrUnlockToken, @Parameter String password) { + public UnlockVaultUsingMasterkey(CloudRepository cloudRepository, @Parameter VaultOrUnlockToken vaultOrUnlockToken, @Parameter Optional unverifiedVaultConfig, @Parameter String password) { this.cloudRepository = cloudRepository; this.vaultOrUnlockToken = vaultOrUnlockToken; + this.unverifiedVaultConfig = unverifiedVaultConfig; this.password = password; } @@ -34,9 +38,9 @@ class UnlockVault { public Cloud execute() throws BackendException { if (vaultOrUnlockToken.getVault().isPresent()) { - return cloudRepository.unlock(vaultOrUnlockToken.getVault().get(), password, cancelledFlag); + return cloudRepository.unlock(vaultOrUnlockToken.getVault().get(), unverifiedVaultConfig, password, cancelledFlag); } else { - return cloudRepository.unlock(vaultOrUnlockToken.getUnlockToken().get(), password, cancelledFlag); + return cloudRepository.unlock(vaultOrUnlockToken.getUnlockToken().get(), unverifiedVaultConfig, password, cancelledFlag); } } diff --git a/domain/src/test/java/org/cryptomator/domain/usecases/vault/UnlockVaultTest.java b/domain/src/test/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkeyTest.java similarity index 56% rename from domain/src/test/java/org/cryptomator/domain/usecases/vault/UnlockVaultTest.java rename to domain/src/test/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkeyTest.java index a8849f4e..c3cce335 100644 --- a/domain/src/test/java/org/cryptomator/domain/usecases/vault/UnlockVaultTest.java +++ b/domain/src/test/java/org/cryptomator/domain/usecases/vault/UnlockVaultUsingMasterkeyTest.java @@ -1,15 +1,17 @@ package org.cryptomator.domain.usecases.vault; +import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.CloudRepository; +import org.cryptomator.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import static org.mockito.Mockito.verify; -public class UnlockVaultTest { +public class UnlockVaultUsingMasterkeyTest { private static final String A_STRING = "89dfhsjdhfjsd"; @@ -19,30 +21,33 @@ public class UnlockVaultTest { private CloudRepository cloudRepository; - private UnlockVault inTest; + private UnlockVaultUsingMasterkey inTest; + + private Optional unverifiedVaultConfig; @BeforeEach public void setup() { unlockToken = Mockito.mock(UnlockToken.class); vault = Mockito.mock(Vault.class); cloudRepository = Mockito.mock(CloudRepository.class); - inTest = Mockito.mock(UnlockVault.class); + unverifiedVaultConfig = Mockito.mock(Optional.class); + inTest = Mockito.mock(UnlockVaultUsingMasterkey.class); } @Test public void testExecuteDelegatesToUnlockWhenInvokedWithVault() throws BackendException { - inTest = new UnlockVault(cloudRepository, VaultOrUnlockToken.from(vault), A_STRING); + inTest = new UnlockVaultUsingMasterkey(cloudRepository, VaultOrUnlockToken.from(vault), unverifiedVaultConfig, A_STRING); inTest.execute(); - verify(cloudRepository).unlock(Mockito.eq(vault), Mockito.eq(A_STRING), Mockito.any()); + verify(cloudRepository).unlock(Mockito.eq(vault), Mockito.any(), Mockito.eq(A_STRING), Mockito.any()); } @Test public void testExecuteDelegatesToUnlockWhenInvokedWithUnlockToken() throws BackendException { - inTest = new UnlockVault(cloudRepository, VaultOrUnlockToken.from(unlockToken), A_STRING); + inTest = new UnlockVaultUsingMasterkey(cloudRepository, VaultOrUnlockToken.from(unlockToken), unverifiedVaultConfig, A_STRING); inTest.execute(); - verify(cloudRepository).unlock(Mockito.eq(unlockToken), Mockito.eq(A_STRING), Mockito.any()); + verify(cloudRepository).unlock(Mockito.eq(unlockToken), Mockito.any(), Mockito.eq(A_STRING), Mockito.any()); } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt index ed8e6e96..a7d95415 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt @@ -15,8 +15,10 @@ class VaultModel(private val vault: Vault) : Serializable { get() = !vault.isUnlocked val position: Int get() = vault.position - val version: Int - get() = vault.version + val format: Int + get() = vault.format + val maxFileNameLength: Int + get() = vault.maxFileNameLength fun toVault(): Vault { return vault diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt index 61aad8d4..27967560 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -2,18 +2,21 @@ package org.cryptomator.presentation.presenter import android.os.Handler import androidx.biometric.BiometricManager +import org.cryptomator.data.cloud.crypto.CryptoConstants import org.cryptomator.domain.Cloud +import org.cryptomator.domain.UnverifiedVaultConfig import org.cryptomator.domain.Vault import org.cryptomator.domain.di.PerView import org.cryptomator.domain.exception.NetworkConnectionException import org.cryptomator.domain.exception.authentication.AuthenticationException import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase +import org.cryptomator.domain.usecases.vault.GetUnverifiedVaultConfigUseCase import org.cryptomator.domain.usecases.vault.LockVaultUseCase import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase import org.cryptomator.domain.usecases.vault.SaveVaultUseCase import org.cryptomator.domain.usecases.vault.UnlockToken -import org.cryptomator.domain.usecases.vault.UnlockVaultUseCase +import org.cryptomator.domain.usecases.vault.UnlockVaultUsingMasterkeyUseCase import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken import org.cryptomator.generator.Callback import org.cryptomator.generator.InjectIntent @@ -27,6 +30,7 @@ import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.ui.activity.view.UnlockVaultView import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler +import org.cryptomator.util.Optional import org.cryptomator.util.SharedPreferencesHandler import java.io.Serializable import javax.inject.Inject @@ -35,8 +39,9 @@ import timber.log.Timber @PerView class UnlockVaultPresenter @Inject constructor( private val changePasswordUseCase: ChangePasswordUseCase, + private val getUnverifiedVaultConfigUseCase: GetUnverifiedVaultConfigUseCase, private val lockVaultUseCase: LockVaultUseCase, - private val unlockVaultUseCase: UnlockVaultUseCase, + private val unlockVaultUsingMasterkeyUseCase: UnlockVaultUsingMasterkeyUseCase, private val prepareUnlockUseCase: PrepareUnlockUseCase, private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, private val saveVaultUseCase: SaveVaultUseCase, @@ -63,12 +68,33 @@ class UnlockVaultPresenter @Inject constructor( } fun setup() { - when (intent.vaultAction()) { - UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD -> view?.getEncryptedPasswordWithBiometricAuthentication(intent.vaultModel()) - UnlockVaultIntent.VaultAction.UNLOCK, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> unlockVault(intent.vaultModel()) - UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(intent.vaultModel()) - else -> TODO("Not yet implemented") + if(intent.vaultAction() == UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD) { + view?.getEncryptedPasswordWithBiometricAuthentication(intent.vaultModel()) + return } + + getUnverifiedVaultConfigUseCase + .withVault(intent.vaultModel().toVault()) + .run(object : DefaultResultHandler>() { + override fun onSuccess(unverifiedVaultConfig: Optional) { + if (unverifiedVaultConfig.isAbsent || unverifiedVaultConfig.get().keyId.scheme == CryptoConstants.MASTERKEY_SCHEME) { + when (intent.vaultAction()) { + UnlockVaultIntent.VaultAction.UNLOCK, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> { + startedUsingPrepareUnlock = sharedPreferencesHandler.backgroundUnlockPreparation() + pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig = unverifiedVaultConfig.orElse(null) + unlockVault(intent.vaultModel()) + } + UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(intent.vaultModel(), unverifiedVaultConfig.orElse(null)) + else -> TODO("Not yet implemented") + } + } + } + + override fun onError(e: Throwable) { + super.onError(e) + finishWithResult(null) + } + }) } private fun unlockVault(vaultModel: VaultModel) { @@ -109,18 +135,18 @@ class UnlockVaultPresenter @Inject constructor( fun onUnlockCanceled() { prepareUnlockUseCase.unsubscribe() - unlockVaultUseCase.cancel() + unlockVaultUsingMasterkeyUseCase.cancel() finish() } fun startPrepareUnlockUseCase(vault: Vault) { - pendingUnlock = null prepareUnlockUseCase // .withVault(vault) // + .andUnverifiedVaultConfig(Optional.ofNullable(pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig)) .run(object : DefaultResultHandler() { override fun onSuccess(unlockToken: UnlockToken) { if (!startedUsingPrepareUnlock && vault.password != null) { - doUnlock(unlockToken, vault.password) + doUnlock(unlockToken, vault.password, pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig) } else { unlockTokenObtained(unlockToken) } @@ -170,7 +196,7 @@ class UnlockVaultPresenter @Inject constructor( .run(object : DefaultResultHandler() { override fun onSuccess(unlockToken: UnlockToken) { if (!startedUsingPrepareUnlock && vault.password != null) { - doUnlock(unlockToken, vault.password) + doUnlock(unlockToken, vault.password, pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig) } else { unlockTokenObtained(unlockToken) } @@ -195,9 +221,10 @@ class UnlockVaultPresenter @Inject constructor( pendingUnlockFor(vault.toVault())?.setPassword(password, this) } - private fun doUnlock(token: UnlockToken, password: String) { - unlockVaultUseCase // + private fun doUnlock(token: UnlockToken, password: String, unverifiedVaultConfig: UnverifiedVaultConfig?) { + unlockVaultUsingMasterkeyUseCase // .withVaultOrUnlockToken(VaultOrUnlockToken.from(token)) // + .andUnverifiedVaultConfig(Optional.ofNullable(unverifiedVaultConfig)) // .andPassword(password) // .run(object : DefaultResultHandler() { override fun onSuccess(cloud: Cloud) { @@ -214,7 +241,7 @@ class UnlockVaultPresenter @Inject constructor( private fun handleUnlockVaultSuccess(vault: Vault, cloud: Cloud, password: String) { lockVaultUseCase.withVault(vault).run(object : DefaultResultHandler() { - override fun onFinished() { + override fun onSuccess(vault: Vault) { finishWithResultAndExtra(cloud, PASSWORD, password) } }) @@ -265,9 +292,10 @@ class UnlockVaultPresenter @Inject constructor( } } - fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String) { + fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String) { view?.showProgress(ProgressModel(ProgressStateModel.CHANGING_PASSWORD)) changePasswordUseCase.withVault(vaultModel.toVault()) // + .andUnverifiedVaultConfig(Optional.ofNullable(unverifiedVaultConfig)) // .andOldPassword(oldPassword) // .andNewPassword(newPassword) // .run(object : DefaultResultHandler() { @@ -287,7 +315,7 @@ class UnlockVaultPresenter @Inject constructor( override fun onError(e: Throwable) { if (!authenticationExceptionHandler.handleAuthenticationException( // this@UnlockVaultPresenter, e, // - ActivityResultCallbacks.changePasswordAfterAuthentication(vaultModel.toVault(), oldPassword, newPassword))) { + ActivityResultCallbacks.changePasswordAfterAuthentication(vaultModel.toVault(), unverifiedVaultConfig, oldPassword, newPassword))) { showError(e) } } @@ -295,10 +323,10 @@ class UnlockVaultPresenter @Inject constructor( } @Callback - fun changePasswordAfterAuthentication(result: ActivityResult, vault: Vault, oldPassword: String, newPassword: String) { + fun changePasswordAfterAuthentication(result: ActivityResult, vault: Vault, unverifiedVaultConfig: UnverifiedVaultConfig, oldPassword: String, newPassword: String) { val cloud = result.getSingleResult(CloudModel::class.java).toCloud() val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build() - onChangePasswordClick(VaultModel(vaultWithUpdatedCloud), oldPassword, newPassword) + onChangePasswordClick(VaultModel(vaultWithUpdatedCloud), unverifiedVaultConfig, oldPassword, newPassword) } fun saveVaultAfterChangePasswordButFailedBiometricAuth(vault: Vault) { @@ -317,6 +345,8 @@ class UnlockVaultPresenter @Inject constructor( private var unlockToken: UnlockToken? = null private var password: String? = null + var unverifiedVaultConfig: UnverifiedVaultConfig? = null + fun setUnlockToken(unlockToken: UnlockToken?, presenter: UnlockVaultPresenter) { this.unlockToken = unlockToken continueIfComplete(presenter) @@ -328,7 +358,7 @@ class UnlockVaultPresenter @Inject constructor( } open fun continueIfComplete(presenter: UnlockVaultPresenter) { - unlockToken?.let { token -> password?.let { password -> presenter.doUnlock(token, password) } } + unlockToken?.let { token -> password?.let { password -> presenter.doUnlock(token, password, unverifiedVaultConfig) } } } fun belongsTo(vault: Vault): Boolean { @@ -351,7 +381,7 @@ class UnlockVaultPresenter @Inject constructor( } init { - unsubscribeOnDestroy(changePasswordUseCase, lockVaultUseCase, unlockVaultUseCase, prepareUnlockUseCase, removeStoredVaultPasswordsUseCase, saveVaultUseCase) + unsubscribeOnDestroy(changePasswordUseCase, getUnverifiedVaultConfigUseCase, lockVaultUseCase, unlockVaultUsingMasterkeyUseCase, prepareUnlockUseCase, removeStoredVaultPasswordsUseCase, saveVaultUseCase) } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt index 9ef06bef..23a4b4a6 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt @@ -2,6 +2,7 @@ package org.cryptomator.presentation.ui.activity import android.os.Build import androidx.annotation.RequiresApi +import org.cryptomator.domain.UnverifiedVaultConfig import org.cryptomator.domain.Vault import org.cryptomator.generator.Activity import org.cryptomator.generator.InjectIntent @@ -13,6 +14,7 @@ import org.cryptomator.presentation.ui.activity.view.UnlockVaultView import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog import org.cryptomator.presentation.ui.dialog.ChangePasswordDialog import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog +import org.cryptomator.presentation.ui.dialog.VaultNotFoundDialog import org.cryptomator.presentation.ui.fragment.UnlockVaultFragment import org.cryptomator.presentation.util.BiometricAuthentication import javax.inject.Inject @@ -21,7 +23,8 @@ import javax.inject.Inject class UnlockVaultActivity : BaseActivity(), // UnlockVaultView, // BiometricAuthentication.Callback, - ChangePasswordDialog.Callback { + ChangePasswordDialog.Callback, + VaultNotFoundDialog.Callback { @Inject lateinit var presenter: UnlockVaultPresenter @@ -99,11 +102,16 @@ class UnlockVaultActivity : BaseActivity(), // private fun unlockVaultFragment(): UnlockVaultFragment = // getCurrentFragment(R.id.fragmentContainer) as UnlockVaultFragment - override fun showChangePasswordDialog(vaultModel: VaultModel) { - showDialog(ChangePasswordDialog.newInstance(vaultModel)) + override fun showChangePasswordDialog(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?) { + showDialog(ChangePasswordDialog.newInstance(vaultModel, unverifiedVaultConfig)) } - override fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String) { - presenter.onChangePasswordClick(vaultModel, oldPassword, newPassword) + override fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String) { + presenter.onChangePasswordClick(vaultModel, unverifiedVaultConfig, oldPassword, newPassword) } + + override fun onDeleteMissingVaultClicked(vault: Vault) { + TODO("Not yet implemented") + } + } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/UnlockVaultView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/UnlockVaultView.kt index a7b962a1..ae777241 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/UnlockVaultView.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/UnlockVaultView.kt @@ -1,5 +1,6 @@ package org.cryptomator.presentation.ui.activity.view +import org.cryptomator.domain.UnverifiedVaultConfig import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog @@ -11,6 +12,6 @@ interface UnlockVaultView : View, EnterPasswordDialog.Callback { fun showBiometricAuthKeyInvalidatedDialog() fun cancelBasicAuthIfRunning() fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean - fun showChangePasswordDialog(vaultModel: VaultModel) + fun showChangePasswordDialog(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/ChangePasswordDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/ChangePasswordDialog.kt index 8a7b8614..20e2297f 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/ChangePasswordDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/ChangePasswordDialog.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.View import android.widget.Button import androidx.appcompat.app.AlertDialog +import org.cryptomator.domain.UnverifiedVaultConfig import org.cryptomator.generator.Dialog import org.cryptomator.presentation.R import org.cryptomator.presentation.model.VaultModel @@ -23,7 +24,7 @@ class ChangePasswordDialog : BaseProgressErrorDialog result) { + void cryptomatorFileChosen(SerializableResult result) { CloudFileModel masterkeyFile = result.getResult(); state().masterkeyFile = masterkeyFile.toCloudNode(); presenter().getView().showProgress(ProgressModel.GENERIC); From 18d7c9c21885297e7b8f1e7707bae2d5e61c2b0b Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 7 Apr 2021 17:16:24 +0200 Subject: [PATCH 004/135] Rename to Impl since no other ciphers left --- .../org/cryptomator/util/crypto/BiometricAuthCryptor.java | 2 +- .../util/crypto/{CipherFromApi23.java => CipherImpl.java} | 4 ++-- .../org/cryptomator/util/crypto/CryptoOperationsFactory.java | 2 +- ...yptoOperationsFromApi23.java => CryptoOperationsImpl.java} | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename util/src/main/java/org/cryptomator/util/crypto/{CipherFromApi23.java => CipherImpl.java} (95%) rename util/src/main/java/org/cryptomator/util/crypto/{CryptoOperationsFromApi23.java => CryptoOperationsImpl.java} (94%) diff --git a/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java b/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java index a6dfd69c..79a593fd 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java +++ b/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java @@ -52,7 +52,7 @@ public class BiometricAuthCryptor { } public String decrypt(javax.crypto.Cipher cipher, String password) throws IllegalBlockSizeException, BadPaddingException { - byte[] ciphered = cipher.doFinal(CipherFromApi23.getBytes(password.getBytes(ISO_8859_1))); + byte[] ciphered = cipher.doFinal(CipherImpl.getBytes(password.getBytes(ISO_8859_1))); return new String(ciphered, UTF_8); } } diff --git a/util/src/main/java/org/cryptomator/util/crypto/CipherFromApi23.java b/util/src/main/java/org/cryptomator/util/crypto/CipherImpl.java similarity index 95% rename from util/src/main/java/org/cryptomator/util/crypto/CipherFromApi23.java rename to util/src/main/java/org/cryptomator/util/crypto/CipherImpl.java index 1164ac07..2ba69fc6 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CipherFromApi23.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CipherImpl.java @@ -8,14 +8,14 @@ import javax.crypto.spec.IvParameterSpec; import static java.lang.System.arraycopy; -class CipherFromApi23 implements Cipher { +class CipherImpl implements Cipher { private static final int IV_LENGTH = 16; private final javax.crypto.Cipher cipher; private final SecretKey key; - CipherFromApi23(javax.crypto.Cipher cipher, SecretKey key) { + CipherImpl(javax.crypto.Cipher cipher, SecretKey key) { this.cipher = cipher; this.key = key; } diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFactory.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFactory.java index d9de5749..e06507b9 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFactory.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFactory.java @@ -16,7 +16,7 @@ class CryptoOperationsFactory { } private static CryptoOperations createCryptoOperations() { - return new CryptoOperationsFromApi23(); + return new CryptoOperationsImpl(); } } diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFromApi23.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsImpl.java similarity index 94% rename from util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFromApi23.java rename to util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsImpl.java index 20973b50..14c7e810 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFromApi23.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsImpl.java @@ -10,7 +10,7 @@ import java.security.UnrecoverableKeyException; import javax.crypto.SecretKey; -class CryptoOperationsFromApi23 implements CryptoOperations { +class CryptoOperationsImpl implements CryptoOperations { @Override public Cipher cryptor(KeyStore keyStore, String alias) throws UnrecoverableStorageKeyException { @@ -19,7 +19,7 @@ class CryptoOperationsFromApi23 implements CryptoOperations { final javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" // + KeyProperties.BLOCK_MODE_CBC + "/" // + KeyProperties.ENCRYPTION_PADDING_PKCS7); - return new CipherFromApi23(cipher, key); + return new CipherImpl(cipher, key); } catch (UnrecoverableKeyException e) { throw new UnrecoverableStorageKeyException(e); } catch (Exception e) { From 4a20fbf7aba978e2b1103410891b730ad1036e35 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 7 Apr 2021 17:59:19 +0200 Subject: [PATCH 005/135] Delete vault when not found and user confirmed --- .../presenter/UnlockVaultPresenter.kt | 24 +++++++++++++++++-- .../ui/activity/UnlockVaultActivity.kt | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt index 27967560..4bb68516 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -10,6 +10,7 @@ import org.cryptomator.domain.di.PerView import org.cryptomator.domain.exception.NetworkConnectionException import org.cryptomator.domain.exception.authentication.AuthenticationException import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase +import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase import org.cryptomator.domain.usecases.vault.GetUnverifiedVaultConfigUseCase import org.cryptomator.domain.usecases.vault.LockVaultUseCase import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase @@ -39,6 +40,7 @@ import timber.log.Timber @PerView class UnlockVaultPresenter @Inject constructor( private val changePasswordUseCase: ChangePasswordUseCase, + private val deleteVaultUseCase: DeleteVaultUseCase, private val getUnverifiedVaultConfigUseCase: GetUnverifiedVaultConfigUseCase, private val lockVaultUseCase: LockVaultUseCase, private val unlockVaultUsingMasterkeyUseCase: UnlockVaultUsingMasterkeyUseCase, @@ -68,7 +70,7 @@ class UnlockVaultPresenter @Inject constructor( } fun setup() { - if(intent.vaultAction() == UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD) { + if (intent.vaultAction() == UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD) { view?.getEncryptedPasswordWithBiometricAuthentication(intent.vaultModel()) return } @@ -340,6 +342,16 @@ class UnlockVaultPresenter @Inject constructor( }) } + fun onDeleteMissingVaultClicked(vault: Vault) { + deleteVaultUseCase // + .withVault(vault) // + .run(object : DefaultResultHandler() { + override fun onSuccess(vaultId: Long) { + finishWithResult(null) + } + }) + } + private open class PendingUnlock(private val vault: Vault?) : Serializable { private var unlockToken: UnlockToken? = null @@ -381,7 +393,15 @@ class UnlockVaultPresenter @Inject constructor( } init { - unsubscribeOnDestroy(changePasswordUseCase, getUnverifiedVaultConfigUseCase, lockVaultUseCase, unlockVaultUsingMasterkeyUseCase, prepareUnlockUseCase, removeStoredVaultPasswordsUseCase, saveVaultUseCase) + unsubscribeOnDestroy( // + changePasswordUseCase, // + deleteVaultUseCase, // + getUnverifiedVaultConfigUseCase, // + lockVaultUseCase, // + unlockVaultUsingMasterkeyUseCase, // + prepareUnlockUseCase, // + removeStoredVaultPasswordsUseCase, // + saveVaultUseCase) } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt index 23a4b4a6..f9795fd6 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt @@ -111,7 +111,7 @@ class UnlockVaultActivity : BaseActivity(), // } override fun onDeleteMissingVaultClicked(vault: Vault) { - TODO("Not yet implemented") + presenter.onDeleteMissingVaultClicked(vault) } } From e34597a71659fbd44c1bc591102c56bfab9e7c74 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 7 Apr 2021 18:20:36 +0200 Subject: [PATCH 006/135] Improve error handling while verifying VaultConfig --- .../data/cloud/crypto/VaultConfig.kt | 23 +++++++++++-------- .../vaultconfig/VaultConfigLoadException.java | 5 ++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt index fe1bcb79..69bd4039 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt @@ -8,11 +8,14 @@ import java.net.URI import java.security.Key import java.util.UUID import io.jsonwebtoken.Claims +import io.jsonwebtoken.IncorrectClaimException import io.jsonwebtoken.JwsHeader import io.jsonwebtoken.JwtException import io.jsonwebtoken.Jwts +import io.jsonwebtoken.MissingClaimException import io.jsonwebtoken.SigningKeyResolverAdapter import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.SignatureException import kotlin.properties.Delegates class VaultConfig private constructor(builder: VaultConfigBuilder) { @@ -35,6 +38,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { } class VaultConfigBuilder { + internal var id: String = UUID.randomUUID().toString() internal var vaultFormat = CryptoConstants.MAX_VAULT_VERSION; internal var cipherCombo = VaultCipherCombo.SIV_CTRMAC @@ -72,6 +76,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { } companion object { + private const val JSON_KEY_VAULTFORMAT = "format" private const val JSON_KEY_CIPHERCONFIG = "cipherCombo" private const val JSON_KEY_MAXFILENAMELEN = "maxFilenameLen" @@ -110,24 +115,24 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { .maxFilenameLength(parser.body[JSON_KEY_MAXFILENAMELEN] as Int) VaultConfig(vaultConfigBuilder) - /*} catch (SignatureVerificationException e) { - throw new VaultKeyInvalidException(); - } catch (InvalidClaimException e) { - throw new VaultVersionMismatchException("Vault config not for version " + expectedVaultFormat); - } catch (JWTVerificationException e) { - throw new VaultConfigLoadException("Failed to verify vault config: " + unverifiedConfig.getToken()); - */ - } catch (e: JwtException) { - throw VaultConfigLoadException("Failed to verify vault config", e) + } catch (e: Exception) { + when (e) { + is MissingClaimException, is IncorrectClaimException -> throw VaultVersionMismatchException("Vault config not for version " + unverifiedVaultConfig.vaultFormat) + is SignatureException -> throw VaultKeyInvalidException() + is JwtException -> throw VaultConfigLoadException("Failed to verify vault config", e) + else -> throw VaultConfigLoadException(e) + } } } + @JvmStatic fun createVaultConfig(): VaultConfigBuilder { return VaultConfigBuilder() } } private class UnverifiedSigningKeyResolver : SigningKeyResolverAdapter() { + lateinit var keyId: URI var vaultFormat: Int by Delegates.notNull() diff --git a/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultConfigLoadException.java b/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultConfigLoadException.java index 149a3c42..4fe159f2 100644 --- a/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultConfigLoadException.java +++ b/domain/src/main/java/org/cryptomator/domain/exception/vaultconfig/VaultConfigLoadException.java @@ -13,4 +13,9 @@ public class VaultConfigLoadException extends BackendException { public VaultConfigLoadException(String message, JwtException e) { super(message, e); } + + public VaultConfigLoadException(Exception e) { + super(e); + } + } From 617c0386bf144d54537994811e776e4541c4cc87 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Thu, 8 Apr 2021 15:36:58 +0200 Subject: [PATCH 007/135] Select vault / masterkey file while adding a vault --- .../presentation/intent/ChooseCloudNodeSettings.java | 9 ++++++++- .../presentation/ui/activity/BrowseFilesActivity.kt | 4 ---- .../presentation/workflow/AddExistingVaultWorkflow.java | 3 ++- presentation/src/main/res/values/strings.xml | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/intent/ChooseCloudNodeSettings.java b/presentation/src/main/java/org/cryptomator/presentation/intent/ChooseCloudNodeSettings.java index bfeb8104..f4699431 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/intent/ChooseCloudNodeSettings.java +++ b/presentation/src/main/java/org/cryptomator/presentation/intent/ChooseCloudNodeSettings.java @@ -160,12 +160,19 @@ public class ChooseCloudNodeSettings implements Serializable { return this; } - public Builder selectingFilesWithNameOnly(String name) { + public Builder selectingFileWithNameOnly(String name) { this.selectionMode = FILES_ONLY; this.namePattern = Pattern.compile(Pattern.quote(name)); return this; } + public Builder selectingFilesWithNameOnly(List names) { + this.selectionMode = FILES_ONLY; + String pattern = names.stream().map(Pattern::quote).reduce(Pattern.quote(""), (p1, p2) -> p1 + "|" + p2); + this.namePattern = Pattern.compile(pattern); + return this; + } + public Builder selectingFoldersNotContaining(List names) { this.selectionMode = FOLDERS_ONLY; this.excludeFolderContainingNames = names; diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt index 210cb3c6..e828c74c 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt @@ -1,7 +1,6 @@ package org.cryptomator.presentation.ui.activity import android.content.Intent -import android.os.Build import android.view.Menu import android.view.View import androidx.appcompat.widget.SearchView @@ -229,9 +228,6 @@ class BrowseFilesActivity : BaseActivity(), // override fun onPrepareOptionsMenu(menu: Menu): Boolean { if (isNavigationMode(SELECT_ITEMS)) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - menu.findItem(R.id.action_export_items).isVisible = false - } menu.findItem(R.id.action_delete_items).isEnabled = enableGeneralSelectionActions menu.findItem(R.id.action_move_items).isEnabled = enableGeneralSelectionActions menu.findItem(R.id.action_export_items).isEnabled = enableGeneralSelectionActions diff --git a/presentation/src/main/java/org/cryptomator/presentation/workflow/AddExistingVaultWorkflow.java b/presentation/src/main/java/org/cryptomator/presentation/workflow/AddExistingVaultWorkflow.java index 2837cbb0..1d5041ff 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/workflow/AddExistingVaultWorkflow.java +++ b/presentation/src/main/java/org/cryptomator/presentation/workflow/AddExistingVaultWorkflow.java @@ -19,6 +19,7 @@ import org.cryptomator.presentation.model.mappers.CloudModelMapper; import org.cryptomator.presentation.presenter.VaultListPresenter; import java.io.Serializable; +import java.util.Arrays; import java.util.List; import javax.inject.Inject; @@ -89,7 +90,7 @@ public class AddExistingVaultWorkflow extends WorkflowVault @string/screen_vault_list_action_add_existing_vault - Select masterkey file + Select vault or masterkey file @string/screen_vault_list_action_create_new_vault Place here Vault name: %1$s From 9b989561df2e6a89dd8f3321fc39df66d32b7054 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Thu, 8 Apr 2021 20:02:44 +0200 Subject: [PATCH 008/135] Update dependencies --- buildsystem/dependencies.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 56bad523..b7f05dee 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -26,7 +26,7 @@ ext { rxAndroidVersion = '2.1.1' rxBindingVersion = '2.2.0' - daggerVersion = '2.33' + daggerVersion = '2.34' gsonVersion = '2.8.6' @@ -69,7 +69,7 @@ ext { jUnitVersion = '5.7.1' jUnit4Version = '4.13.1' assertJVersion = '1.7.1' - mockitoVersion = '3.8.0' + mockitoVersion = '3.9.0' mockitoInlineVersion = '3.8.0' hamcrestVersion = '1.3' dexmakerVersion = '1.0' From 9c0067b7e28a3eaa03190269d61fa494c9fff95a Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Thu, 8 Apr 2021 20:26:55 +0200 Subject: [PATCH 009/135] Enhance error handling while verifying vault Display error messages when failed to read or verify the vault config --- .../presentation/exception/ExceptionHandlers.kt | 6 ++++++ .../presentation/presenter/UnlockVaultPresenter.kt | 14 ++++++++++++++ .../presentation/presenter/VaultListPresenter.kt | 10 +--------- .../ui/activity/UnlockVaultActivity.kt | 4 ++++ .../presentation/ui/activity/VaultListActivity.kt | 7 ------- .../presentation/ui/dialog/VaultNotFoundDialog.kt | 5 ++++- presentation/src/main/res/values/strings.xml | 4 +++- 7 files changed, 32 insertions(+), 18 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt b/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt index b50bfc4a..6ccbedbe 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt @@ -16,6 +16,9 @@ import org.cryptomator.domain.exception.license.LicenseNotValidException import org.cryptomator.domain.exception.license.NoLicenseAvailableException import org.cryptomator.domain.exception.update.GeneralUpdateErrorException import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException +import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException +import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException +import org.cryptomator.domain.exception.vaultconfig.VaultVersionMismatchException import org.cryptomator.presentation.R import org.cryptomator.presentation.ui.activity.view.View import java.util.ArrayList @@ -45,6 +48,9 @@ class ExceptionHandlers @Inject constructor(private val context: Context, defaul staticHandler(NoLicenseAvailableException::class.java, R.string.dialog_enter_license_no_content) staticHandler(GeneralUpdateErrorException::class.java, R.string.error_general_update) staticHandler(SSLHandshakePreAndroid5UpdateCheckException::class.java, R.string.error_general_update) + staticHandler(VaultVersionMismatchException::class.java, R.string.error_vault_version_mismatch) + staticHandler(VaultKeyInvalidException::class.java, R.string.error_vault_key_invalid) + staticHandler(VaultConfigLoadException::class.java, R.string.error_vault_config_loading) exceptionHandlers.add(MissingCryptorExceptionHandler()) exceptionHandlers.add(CancellationExceptionHandler()) exceptionHandlers.add(NoSuchVaultExceptionHandler()) diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt index 4bb68516..974acdb2 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -29,6 +29,7 @@ import org.cryptomator.presentation.model.ProgressModel import org.cryptomator.presentation.model.ProgressStateModel import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.ui.activity.view.UnlockVaultView +import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler import org.cryptomator.util.Optional @@ -238,6 +239,14 @@ class UnlockVaultPresenter @Inject constructor( else -> TODO("Not yet implemented") } } + + override fun onError(e: Throwable) { + super.onError(e) + // finish in case of biometric auth, otherwise show error in dialog + if(view?.isShowingDialog(EnterPasswordDialog::class) == false) { + finishWithResult(null) + } + } }) } @@ -266,6 +275,7 @@ class UnlockVaultPresenter @Inject constructor( override fun onError(e: Throwable) { Timber.tag("VaultListPresenter").e(e, "Error while removing vault passwords") + finishWithResult(null) } }) } @@ -352,6 +362,10 @@ class UnlockVaultPresenter @Inject constructor( }) } + fun onCancelMissingVaultClicked(vault: Vault) { + finishWithResult(null) + } + private open class PendingUnlock(private val vault: Vault?) : Serializable { private var unlockToken: UnlockToken? = null diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt index c512c674..f773853c 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -185,12 +185,8 @@ class VaultListPresenter @Inject constructor( // } fun deleteVault(vaultModel: VaultModel) { - deleteVault(vaultModel.toVault()) - } - - private fun deleteVault(vault: Vault) { deleteVaultUseCase // - .withVault(vault) // + .withVault(vaultModel.toVault()) // .run(object : DefaultResultHandler() { override fun onSuccess(vaultId: Long) { view?.deleteVaultFromAdapter(vaultId) @@ -433,10 +429,6 @@ class VaultListPresenter @Inject constructor( // } } - fun onDeleteMissingVaultClicked(vault: Vault) { - deleteVault(vault) - } - fun onFilteredTouchEventForSecurity() { view?.showDialog(AppIsObscuredInfoDialog.newInstance()) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt index f9795fd6..5fe46dfe 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt @@ -114,4 +114,8 @@ class UnlockVaultActivity : BaseActivity(), // presenter.onDeleteMissingVaultClicked(vault) } + override fun onCancelMissingVaultClicked(vault: Vault) { + presenter.onCancelMissingVaultClicked(vault) + } + } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt index bc9dcbf8..ddce44e1 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt @@ -4,7 +4,6 @@ import android.content.Intent import android.net.Uri import android.view.View import androidx.fragment.app.Fragment -import org.cryptomator.domain.Vault import org.cryptomator.generator.Activity import org.cryptomator.generator.InjectIntent import org.cryptomator.presentation.CryptomatorApp @@ -26,7 +25,6 @@ import org.cryptomator.presentation.ui.dialog.BetaConfirmationDialog import org.cryptomator.presentation.ui.dialog.UpdateAppAvailableDialog import org.cryptomator.presentation.ui.dialog.UpdateAppDialog import org.cryptomator.presentation.ui.dialog.VaultDeleteConfirmationDialog -import org.cryptomator.presentation.ui.dialog.VaultNotFoundDialog import org.cryptomator.presentation.ui.dialog.VaultRenameDialog import org.cryptomator.presentation.ui.fragment.VaultListFragment import org.cryptomator.presentation.ui.layout.ObscuredAwareCoordinatorLayout.Listener @@ -40,7 +38,6 @@ class VaultListActivity : BaseActivity(), // VaultListView, // VaultListCallback, // AskForLockScreenDialog.Callback, // - VaultNotFoundDialog.Callback, // UpdateAppAvailableDialog.Callback, // UpdateAppDialog.Callback, // BetaConfirmationDialog.Callback { @@ -197,10 +194,6 @@ class VaultListActivity : BaseActivity(), // private fun vaultListFragment(): VaultListFragment = // getCurrentFragment(R.id.fragmentContainer) as VaultListFragment - override fun onDeleteMissingVaultClicked(vault: Vault) { - vaultListPresenter.onDeleteMissingVaultClicked(vault) - } - override fun onUpdateAppDialogLoaded() { showProgress(ProgressModel.GENERIC) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/VaultNotFoundDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/VaultNotFoundDialog.kt index 5b918e64..ec110d07 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/VaultNotFoundDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/VaultNotFoundDialog.kt @@ -14,6 +14,8 @@ class VaultNotFoundDialog private constructor(private val context: Context) { interface Callback { fun onDeleteMissingVaultClicked(vault: Vault) + fun onCancelMissingVaultClicked(vault: Vault) + } fun show(vault: Vault) { @@ -21,7 +23,8 @@ class VaultNotFoundDialog private constructor(private val context: Context) { .setTitle(String.format(ResourceHelper.getString(R.string.dialog_vault_not_found_title), vault.name)) // .setMessage(ResourceHelper.getString(R.string.dialog_vault_not_found_message)) // .setPositiveButton(ResourceHelper.getString(R.string.dialog_vault_not_found_positive_button_text)) { _: DialogInterface, _: Int -> callback.onDeleteMissingVaultClicked(vault) } // - .setNegativeButton(ResourceHelper.getString(R.string.dialog_button_cancel)) { dialog: DialogInterface, _: Int -> dialog.dismiss() } // + .setNegativeButton(ResourceHelper.getString(R.string.dialog_button_cancel)) { _: DialogInterface, _: Int -> callback.onCancelMissingVaultClicked(vault) } // + .setOnCancelListener { callback.onCancelMissingVaultClicked(vault) } .create().show() } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index a8d0920f..37098863 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -32,7 +32,9 @@ Failed to decrypt WebDAV password, please re add in settings Play Services not installed Biometric authentication aborted - + Version specified in vault.cryptomator is different to masterkey.cryptomator + vault.cryptomator does not match with this masterkey.cryptomator + General error while loading the vault config From cf5bf19c98027265c85729838af25aaf5b76812f Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Thu, 8 Apr 2021 21:57:16 +0200 Subject: [PATCH 010/135] Provide SecureRandom as parameter for better testing and code dedup --- .../data/cloud/crypto/CryptoCloudFactory.java | 6 ++++-- .../cloud/crypto/MasterkeyCryptoCloudProvider.java | 13 ++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java index a1729c24..4caa9d2d 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudFactory.java @@ -12,6 +12,7 @@ import org.cryptomator.domain.usecases.vault.UnlockToken; import org.cryptomator.util.Optional; import java.io.ByteArrayOutputStream; +import java.security.SecureRandom; import javax.inject.Inject; import javax.inject.Singleton; @@ -27,6 +28,7 @@ public class CryptoCloudFactory { private final CloudContentRepository cloudContentRepository; private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory; + private final SecureRandom secureRandom = new SecureRandom(); @Inject public CryptoCloudFactory(CloudContentRepository cloudContentRepository, // @@ -84,12 +86,12 @@ public class CryptoCloudFactory { if (unverifiedVaultConfigOptional.isPresent()) { switch (unverifiedVaultConfigOptional.get().getKeyId().getScheme()) { case MASTERKEY_SCHEME: { - return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory); + return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom); } default: throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme())); } } else { - return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory); + return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom); } } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java index d1720dd7..e7b7739f 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java @@ -49,19 +49,22 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { private final CloudContentRepository cloudContentRepository; private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory; + private final SecureRandom secureRandom; public MasterkeyCryptoCloudProvider(CloudContentRepository cloudContentRepository, // - CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) { + CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, + SecureRandom secureRandom) { this.cloudContentRepository = cloudContentRepository; this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory; + this.secureRandom = secureRandom; } @Override public void create(CloudFolder location, CharSequence password) throws BackendException { // 1. write masterkey: - Masterkey masterkey = Masterkey.generate(new SecureRandom()); + Masterkey masterkey = Masterkey.generate(secureRandom); try (ByteArrayOutputStream data = new ByteArrayOutputStream()) { - new MasterkeyFileAccess(PEPPER, new SecureRandom()).persist(masterkey, data, password, DEFAULT_MASTERKEY_FILE_VERSION); + new MasterkeyFileAccess(PEPPER, secureRandom).persist(masterkey, data, password, DEFAULT_MASTERKEY_FILE_VERSION); cloudContentRepository.write(legacyMasterkeyFile(location), ByteArrayDataSource.from(data.toByteArray()), NO_OP_PROGRESS_AWARE, false, data.size()); } catch (IOException e) { throw new FatalBackendException("Failed to write masterkey", e); @@ -174,7 +177,7 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { } private Cryptor cryptorFor(Masterkey keyFile, VaultCipherCombo vaultCipherCombo) { - return vaultCipherCombo.getCryptorProvider(new SecureRandom()).withKey(keyFile); + return vaultCipherCombo.getCryptorProvider(secureRandom).withKey(keyFile); } @Override @@ -269,7 +272,7 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { private void createNewMasterKeyFile(byte[] data, int vaultVersion, String oldPassword, String newPassword, CloudFile masterkeyFile) throws BackendException { try { - byte[] newMasterKeyFile = new MasterkeyFileAccess(PEPPER, new SecureRandom()) // + byte[] newMasterKeyFile = new MasterkeyFileAccess(PEPPER, secureRandom) // .changePassphrase(data, normalizePassword(oldPassword, vaultVersion), normalizePassword(newPassword, vaultVersion)); cloudContentRepository.write(masterkeyFile, // ByteArrayDataSource.from(newMasterKeyFile), // From 23350a8cd7f0ab85214f9a51a254ae7ccc9b49d2 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Thu, 8 Apr 2021 21:58:16 +0200 Subject: [PATCH 011/135] Write createVault test and started with lockVault but still WIP --- .../MasterkeyCryptoCloudProviderTest.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java new file mode 100644 index 00000000..d51e5326 --- /dev/null +++ b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java @@ -0,0 +1,127 @@ +package org.cryptomator.data.cloud.crypto; + +import android.content.Context; + +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.cryptomator.domain.Cloud; +import org.cryptomator.domain.Vault; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.repository.CloudContentRepository; +import org.cryptomator.domain.usecases.cloud.DataSource; +import org.cryptomator.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.stream.Collectors; + +import static org.cryptomator.cryptolib.api.Masterkey.SUBKEY_LEN_BYTES; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_FILE_NAME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.ROOT_DIR_ID; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME; +import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class MasterkeyCryptoCloudProviderTest { + + private Context context; + private Cloud cloud; + private CloudContentRepository cloudContentRepository; + private CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory; + private Vault vault; + + private Cryptor cryptor; + private FileNameCryptor fileNameCryptor; + private SecureRandom secureRandom; + + private MasterkeyCryptoCloudProvider inTest; + + @BeforeEach + void setUp() { + context = Mockito.mock(Context.class); + cloud = Mockito.mock(Cloud.class); + cloudContentRepository = Mockito.mock(CloudContentRepository.class); + cryptoCloudContentRepositoryFactory = Mockito.mock(CryptoCloudContentRepositoryFactory.class); + vault = Mockito.mock(Vault.class); + + cryptor = Mockito.mock(Cryptor.class); + fileNameCryptor = Mockito.mock(FileNameCryptor.class); + secureRandom = Mockito.mock(SecureRandom.class); + + Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); + + byte[] key = new byte[SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES]; + Mockito.doNothing().when(secureRandom).nextBytes(key); + + inTest = new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom); + } + + @Test + @DisplayName("create(\"/foo, foo\")") + public void testCreateVault() throws BackendException { + String masterkey = "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}"; + + TestFolder rootFolder = new RootTestFolder(cloud); + TestFolder foo = new TestFolder(rootFolder, "foo", "/foo"); + TestFile vaultFile = new TestFile(foo, VAULT_FILE_NAME, "/foo/" + VAULT_FILE_NAME, Optional.empty(), Optional.empty()); + TestFile masterKeyFile = new TestFile(foo, MASTERKEY_FILE_NAME, "/foo/" + MASTERKEY_FILE_NAME, Optional.empty(), Optional.empty()); + + Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile); + Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile); + + // 1. write masterkey + Mockito.when(cloudContentRepository.write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { + DataSource in = invocationOnMock.getArgument(1); + String masterKeyFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining()); + assertThat(masterKeyFileContent, is(masterkey)); + return invocationOnMock.getArgument(0); + }); + + + // 2. initialize vault: + Mockito.when(cloudContentRepository.write(Mockito.eq(vaultFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong())).thenReturn(vaultFile); + + // 3. create root folder: + String rootDirHash = "KG6TFDGKXGZEGWRZOGTDFDF4YEGAZO6Q"; + + TestFolder dFolder = new TestFolder(foo, "d", "/foo/" + DATA_DIR_NAME); + TestFolder lvl1Dir = new TestFolder(dFolder, rootDirHash.substring(0, 2), "/foo/" + DATA_DIR_NAME + "/" + rootDirHash.substring(0, 2)); + TestFolder lvl2Dir = new TestFolder(lvl1Dir, rootDirHash.substring(2), "/foo/" + DATA_DIR_NAME + "/" + rootDirHash.substring(0, 2) + "/" + rootDirHash.substring(2)); + + + Mockito.when(cloudContentRepository.folder(foo, DATA_DIR_NAME)).thenReturn(dFolder); + Mockito.when(cloudContentRepository.create(dFolder)).thenReturn(dFolder); + + Mockito.when(cryptor.fileNameCryptor().hashDirectoryId(ROOT_DIR_ID)).thenReturn(ROOT_DIR_ID); + + Mockito.when(cloudContentRepository.folder(dFolder, lvl1Dir.getName())).thenReturn(lvl1Dir); + Mockito.when(cloudContentRepository.create(lvl1Dir)).thenReturn(lvl1Dir); + + Mockito.when(cloudContentRepository.folder(lvl1Dir, lvl2Dir.getName())).thenReturn(lvl2Dir); + Mockito.when(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir); + + inTest.create(foo, "foo"); + + Mockito.verify(cloudContentRepository).write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong()); + Mockito.verify(cloudContentRepository).write(Mockito.eq(vaultFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong()); + Mockito.verify(cloudContentRepository).create(dFolder); + Mockito.verify(cloudContentRepository).create(lvl1Dir); + Mockito.verify(cloudContentRepository).create(lvl2Dir); + } + + @Test + @DisplayName("lock(\"foo\")") + public void testLockVault() throws BackendException { + // TODO implement me + cryptoCloudContentRepositoryFactory.deregisterCryptor(vault); + } + +} From f6c85547792ce5702ee34153975275547a972700 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 9 Apr 2021 09:22:41 +0200 Subject: [PATCH 012/135] Add VaultConfigBuilder as parameter in createVault To enhance testing --- .../data/cloud/crypto/MasterkeyCryptoCloudProvider.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java index e7b7739f..300d4372 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java @@ -61,6 +61,12 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { @Override public void create(CloudFolder location, CharSequence password) throws BackendException { + // Just for testing (id in VaultConfig is auto generated which makes sense while creating a vault but not for testing) + create(location, password, VaultConfig.createVaultConfig()); + } + + // Visible for testing + void create(CloudFolder location, CharSequence password, VaultConfig.VaultConfigBuilder vaultConfigBuilder) throws BackendException { // 1. write masterkey: Masterkey masterkey = Masterkey.generate(secureRandom); try (ByteArrayOutputStream data = new ByteArrayOutputStream()) { @@ -71,7 +77,7 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { } // 2. initialize vault: - VaultConfig vaultConfig = new VaultConfig.VaultConfigBuilder() // + VaultConfig vaultConfig = vaultConfigBuilder // .vaultFormat(MAX_VAULT_VERSION) // .cipherCombo(DEFAULT_CIPHER_COMBO) // .keyId(URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME))) // From a4762fb32ad9f7ac615044566f0919af1d1572fb Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 9 Apr 2021 09:24:36 +0200 Subject: [PATCH 013/135] Test content of written vault config as well While creating a vault, check also the content for the vault.cryptomator file. --- .../MasterkeyCryptoCloudProviderTest.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java index d51e5326..a04d7436 100644 --- a/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java +++ b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java @@ -37,6 +37,7 @@ class MasterkeyCryptoCloudProviderTest { private CloudContentRepository cloudContentRepository; private CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory; private Vault vault; + private VaultConfig.VaultConfigBuilder vaultConfigBuilder; private Cryptor cryptor; private FileNameCryptor fileNameCryptor; @@ -45,12 +46,13 @@ class MasterkeyCryptoCloudProviderTest { private MasterkeyCryptoCloudProvider inTest; @BeforeEach - void setUp() { + public void setUp() { context = Mockito.mock(Context.class); cloud = Mockito.mock(Cloud.class); cloudContentRepository = Mockito.mock(CloudContentRepository.class); cryptoCloudContentRepositoryFactory = Mockito.mock(CryptoCloudContentRepositoryFactory.class); vault = Mockito.mock(Vault.class); + vaultConfigBuilder = VaultConfig.createVaultConfig().id(""); cryptor = Mockito.mock(Cryptor.class); fileNameCryptor = Mockito.mock(FileNameCryptor.class); @@ -68,6 +70,7 @@ class MasterkeyCryptoCloudProviderTest { @DisplayName("create(\"/foo, foo\")") public void testCreateVault() throws BackendException { String masterkey = "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}"; + String vaultConfig = "eyJraWQiOiJtYXN0ZXJrZXlmaWxlOm1hc3RlcmtleS5jcnlwdG9tYXRvciIsImFsZyI6IkhTNTEyIn0.eyJtYXhGaWxlbmFtZUxlbiI6MjIwLCJmb3JtYXQiOjgsImNpcGhlckNvbWJvIjoiU0lWX0NUUk1BQyJ9.umiAcGObWuVISugrQu16hznDHIFM7moD1ukA1r5V1DRA0GjHQk1p6S9hkL0PaMD7xl04jSttMRalOYU1sg4wqQ"; TestFolder rootFolder = new RootTestFolder(cloud); TestFolder foo = new TestFolder(rootFolder, "foo", "/foo"); @@ -85,11 +88,15 @@ class MasterkeyCryptoCloudProviderTest { return invocationOnMock.getArgument(0); }); + // 2. initialize vault + Mockito.when(cloudContentRepository.write(Mockito.eq(vaultFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { + DataSource in = invocationOnMock.getArgument(1); + String vaultConfigFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining()); + assertThat(vaultConfigFileContent, is(vaultConfig)); + return invocationOnMock.getArgument(0); + }); - // 2. initialize vault: - Mockito.when(cloudContentRepository.write(Mockito.eq(vaultFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong())).thenReturn(vaultFile); - - // 3. create root folder: + // 3. create root folder String rootDirHash = "KG6TFDGKXGZEGWRZOGTDFDF4YEGAZO6Q"; TestFolder dFolder = new TestFolder(foo, "d", "/foo/" + DATA_DIR_NAME); @@ -108,7 +115,7 @@ class MasterkeyCryptoCloudProviderTest { Mockito.when(cloudContentRepository.folder(lvl1Dir, lvl2Dir.getName())).thenReturn(lvl2Dir); Mockito.when(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir); - inTest.create(foo, "foo"); + inTest.create(foo, "foo", vaultConfigBuilder); Mockito.verify(cloudContentRepository).write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong()); Mockito.verify(cloudContentRepository).write(Mockito.eq(vaultFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong()); From 65ad054ff3033930dc8270cd1e286385032d58f9 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 9 Apr 2021 14:56:40 +0200 Subject: [PATCH 014/135] Enhance visibility for testing --- .../crypto/MasterkeyCryptoCloudProvider.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java index 300d4372..daed730f 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java @@ -52,7 +52,7 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { private final SecureRandom secureRandom; public MasterkeyCryptoCloudProvider(CloudContentRepository cloudContentRepository, // - CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, + CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, // SecureRandom secureRandom) { this.cloudContentRepository = cloudContentRepository; this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory; @@ -138,7 +138,7 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { Vault vault = aCopyOf(token.getVault()) // .withUnlocked(true) // .withFormat(vaultFormat) // - .withMaxFileNameLength(maxFileNameLength) + .withMaxFileNameLength(maxFileNameLength) // .build(); cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor); @@ -182,7 +182,8 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { return data.toByteArray(); } - private Cryptor cryptorFor(Masterkey keyFile, VaultCipherCombo vaultCipherCombo) { + // Visible for testing + Cryptor cryptorFor(Masterkey keyFile, VaultCipherCombo vaultCipherCombo) { return vaultCipherCombo.getCryptorProvider(secureRandom).withKey(keyFile); } @@ -190,14 +191,15 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { public boolean isVaultPasswordValid(Vault vault, Optional unverifiedVaultConfig, CharSequence password) throws BackendException { try { // create a cryptor, which checks the password, then destroy it immediately - Masterkey masterkey = createUnlockToken(vault, unverifiedVaultConfig).getKeyFile(password); + UnlockTokenImpl unlockToken = createUnlockToken(vault, unverifiedVaultConfig); + Masterkey masterkey = unlockToken.getKeyFile(password); VaultCipherCombo vaultCipherCombo; - if(unverifiedVaultConfig.isPresent()) { + if (unverifiedVaultConfig.isPresent()) { VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get()); assertVaultVersionIsSupported(vaultConfig.getVaultFormat()); vaultCipherCombo = vaultConfig.getCipherCombo(); } else { - int vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(masterkey.getEncoded()); + int vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(unlockToken.keyFileData); assertLegacyVaultVersionIsSupported(vaultVersion); vaultCipherCombo = SIV_CTRMAC; } @@ -234,8 +236,6 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { @Override public void changePassword(Vault vault, Optional unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException { CloudFolder vaultLocation = vaultLocation(vault); - ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream(); - CloudFile masterkeyFile; if (unverifiedVaultConfig.isPresent()) { masterkeyFile = masterkeyFile(vaultLocation, unverifiedVaultConfig.get()); @@ -243,6 +243,7 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { masterkeyFile = legacyMasterkeyFile(vaultLocation); } + ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream(); cloudContentRepository.read(masterkeyFile, Optional.empty(), dataOutputStream, NO_OP_PROGRESS_AWARE); byte[] data = dataOutputStream.toByteArray(); @@ -298,12 +299,12 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { } } - private static class UnlockTokenImpl implements UnlockToken { + static class UnlockTokenImpl implements UnlockToken { private final Vault vault; private final byte[] keyFileData; - private UnlockTokenImpl(Vault vault, byte[] keyFileData) { + UnlockTokenImpl(Vault vault, byte[] keyFileData) { this.vault = vault; this.keyFileData = keyFileData; } From 4871172fb404e696f711ff16ec48d71cfb972ebc Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 9 Apr 2021 14:57:34 +0200 Subject: [PATCH 015/135] Finish tests of MasterkeyCryptoCloudProvider --- .../MasterkeyCryptoCloudProviderTest.java | 224 +++++++++++++++++- 1 file changed, 213 insertions(+), 11 deletions(-) diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java index a04d7436..d157a8cc 100644 --- a/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java +++ b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java @@ -4,45 +4,66 @@ import android.content.Context; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.data.util.CopyStream; import org.cryptomator.domain.Cloud; +import org.cryptomator.domain.CloudType; +import org.cryptomator.domain.UnverifiedVaultConfig; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.CloudContentRepository; import org.cryptomator.domain.usecases.cloud.DataSource; +import org.cryptomator.domain.usecases.vault.UnlockToken; import org.cryptomator.util.Optional; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.stream.Collectors; import static org.cryptomator.cryptolib.api.Masterkey.SUBKEY_LEN_BYTES; import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_MAX_FILE_NAME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_BACKUP_FILE_EXT; import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_FILE_NAME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG; import static org.cryptomator.data.cloud.crypto.CryptoConstants.ROOT_DIR_ID; import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME; +import static org.cryptomator.data.cloud.crypto.VaultCipherCombo.SIV_CTRMAC; import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; class MasterkeyCryptoCloudProviderTest { + private final String masterkeyV8 = "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}"; + private final String masterkeyV7 = "{ \"version\": 7, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"cn2sAK6l9p1/w9deJVUuW3h7br056mpv5srvALiYw+g=\"}"; + private final String vaultConfig = "eyJraWQiOiJtYXN0ZXJrZXlmaWxlOm1hc3RlcmtleS5jcnlwdG9tYXRvciIsImFsZyI6IkhTNTEyIn0.eyJtYXhGaWxlbmFtZUxlbiI6MjIwLCJmb3JtYXQiOjgsImNpcGhlckNvbWJvIjoiU0lWX0NUUk1BQyJ9.umiAcGObWuVISugrQu16hznDHIFM7moD1ukA1r5V1DRA0GjHQk1p6S9hkL0PaMD7xl04jSttMRalOYU1sg4wqQ"; + private Context context; private Cloud cloud; private CloudContentRepository cloudContentRepository; private CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory; private Vault vault; private VaultConfig.VaultConfigBuilder vaultConfigBuilder; - private Cryptor cryptor; private FileNameCryptor fileNameCryptor; private SecureRandom secureRandom; - private MasterkeyCryptoCloudProvider inTest; @BeforeEach @@ -50,7 +71,9 @@ class MasterkeyCryptoCloudProviderTest { context = Mockito.mock(Context.class); cloud = Mockito.mock(Cloud.class); cloudContentRepository = Mockito.mock(CloudContentRepository.class); + cryptoCloudContentRepositoryFactory = Mockito.mock(CryptoCloudContentRepositoryFactory.class); + vault = Mockito.mock(Vault.class); vaultConfigBuilder = VaultConfig.createVaultConfig().id(""); @@ -63,15 +86,12 @@ class MasterkeyCryptoCloudProviderTest { byte[] key = new byte[SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES]; Mockito.doNothing().when(secureRandom).nextBytes(key); - inTest = new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom); + inTest = Mockito.spy(new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom)); } @Test - @DisplayName("create(\"/foo, foo\")") + @DisplayName("create(\"/foo\", \"foo\")") public void testCreateVault() throws BackendException { - String masterkey = "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}"; - String vaultConfig = "eyJraWQiOiJtYXN0ZXJrZXlmaWxlOm1hc3RlcmtleS5jcnlwdG9tYXRvciIsImFsZyI6IkhTNTEyIn0.eyJtYXhGaWxlbmFtZUxlbiI6MjIwLCJmb3JtYXQiOjgsImNpcGhlckNvbWJvIjoiU0lWX0NUUk1BQyJ9.umiAcGObWuVISugrQu16hznDHIFM7moD1ukA1r5V1DRA0GjHQk1p6S9hkL0PaMD7xl04jSttMRalOYU1sg4wqQ"; - TestFolder rootFolder = new RootTestFolder(cloud); TestFolder foo = new TestFolder(rootFolder, "foo", "/foo"); TestFile vaultFile = new TestFile(foo, VAULT_FILE_NAME, "/foo/" + VAULT_FILE_NAME, Optional.empty(), Optional.empty()); @@ -84,7 +104,7 @@ class MasterkeyCryptoCloudProviderTest { Mockito.when(cloudContentRepository.write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> { DataSource in = invocationOnMock.getArgument(1); String masterKeyFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining()); - assertThat(masterKeyFileContent, is(masterkey)); + assertThat(masterKeyFileContent, is(masterkeyV8)); return invocationOnMock.getArgument(0); }); @@ -126,9 +146,191 @@ class MasterkeyCryptoCloudProviderTest { @Test @DisplayName("lock(\"foo\")") - public void testLockVault() throws BackendException { - // TODO implement me - cryptoCloudContentRepositoryFactory.deregisterCryptor(vault); + public void testLockVault() { + inTest.lock(vault); + Mockito.verify(cryptoCloudContentRepositoryFactory).deregisterCryptor(vault); + } + + @Test + @DisplayName("unlock(\"foo\")") + public void testUnlockVault() throws BackendException, IOException { + CloudType cloudType = Mockito.mock(CloudType.class); + + Mockito.when(cloud.type()).thenReturn(cloudType); + + Mockito.when(vault.getCloud()).thenReturn(cloud); + Mockito.when(vault.getCloudType()).thenReturn(cloudType); + Mockito.when(vault.getFormat()).thenReturn(8); + Mockito.when(vault.getId()).thenReturn(25L); + Mockito.when(vault.getName()).thenReturn("foo"); + Mockito.when(vault.getPath()).thenReturn("/foo"); + Mockito.when(vault.isUnlocked()).thenReturn(true); + + MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV7.getBytes(StandardCharsets.UTF_8)); + UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION); + + Vault result = inTest.unlock(unlockToken, Optional.of(unverifiedVaultConfig), "foo", () -> false); + + MatcherAssert.assertThat(result.isUnlocked(), is(true)); + MatcherAssert.assertThat(result.getFormat(), is(8)); + MatcherAssert.assertThat(result.getMaxFileNameLength(), is(DEFAULT_MAX_FILE_NAME)); + + Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile("foo"), SIV_CTRMAC); + Mockito.verify(cryptoCloudContentRepositoryFactory).registerCryptor(Mockito.any(Vault.class), Mockito.any(Cryptor.class)); + } + + @Test + @DisplayName("unlockLegacy(\"foo\")") + public void testUnlockLegacyVault() throws BackendException, IOException { + CloudType cloudType = Mockito.mock(CloudType.class); + + Mockito.when(cloud.type()).thenReturn(cloudType); + + Mockito.when(vault.getCloud()).thenReturn(cloud); + Mockito.when(vault.getCloudType()).thenReturn(cloudType); + Mockito.when(vault.getFormat()).thenReturn(7); + Mockito.when(vault.getId()).thenReturn(25L); + Mockito.when(vault.getName()).thenReturn("foo"); + Mockito.when(vault.getPath()).thenReturn("/foo"); + Mockito.when(vault.isUnlocked()).thenReturn(true); + + MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV7.getBytes(StandardCharsets.UTF_8)); + + Vault result = inTest.unlock(unlockToken, Optional.empty(), "foo", () -> false); + + MatcherAssert.assertThat(result.isUnlocked(), is(true)); + MatcherAssert.assertThat(result.getFormat(), is(MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG)); + MatcherAssert.assertThat(result.getMaxFileNameLength(), is(DEFAULT_MAX_FILE_NAME)); + + Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile("foo"), SIV_CTRMAC); + Mockito.verify(cryptoCloudContentRepositoryFactory).registerCryptor(Mockito.any(Vault.class), Mockito.any(Cryptor.class)); + } + + @Test + @DisplayName("unlockLegacyUsingNewVault(\"foo\")") + public void testUnlockLegacyVaultUsingVaultFormat8() { + UnlockToken unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV8.getBytes(StandardCharsets.UTF_8)); + Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> inTest.unlock(unlockToken, Optional.empty(), "foo", () -> false)); + } + + @DisplayName("changePassword(\"foo\")") + @ParameterizedTest(name = "Legacy vault format {0}") + @ValueSource(booleans = {true, false}) + public void tesChangePassword(boolean legacy) throws BackendException { + if (legacy) { + testChangePassword(masterkeyV7, Optional.empty()); + } else { + UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION); + testChangePassword(masterkeyV8, Optional.of(unverifiedVaultConfig)); + } + } + + private void testChangePassword(String masterkeyContent, Optional unverifiedVaultConfig) throws BackendException { + TestFolder rootFolder = new RootTestFolder(cloud); + TestFolder foo = new TestFolder(rootFolder, "foo", "/foo"); + TestFile vaultFile = new TestFile(foo, VAULT_FILE_NAME, "/foo/" + VAULT_FILE_NAME, Optional.empty(), Optional.empty()); + TestFile masterKeyFile = new TestFile(foo, MASTERKEY_FILE_NAME, "/foo/" + MASTERKEY_FILE_NAME, Optional.empty(), Optional.empty()); + + Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile); + Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile); + Mockito.when(cloudContentRepository.resolve(vault.getCloud(), vault.getPath())).thenReturn(foo); + + // 1. Read masterkey + Mockito.doAnswer(invocation -> { + OutputStream out = invocation.getArgument(2); + CopyStream.copyStreamToStream(new ByteArrayInputStream(masterkeyContent.getBytes()), out); + return null; + }).when(cloudContentRepository).read(Mockito.eq(masterKeyFile), Mockito.eq(Optional.empty()), Mockito.any(), Mockito.eq(NO_OP_PROGRESS_AWARE)); + + // 2. Create backup + String fileName = masterKeyFile.getName() + BackupFileIdSuffixGenerator.generate(masterkeyContent.getBytes()) + MASTERKEY_BACKUP_FILE_EXT; + TestFile masterKeyBackupFile = new TestFile(foo, fileName, "/foo/" + fileName, Optional.empty(), Optional.empty()); + Mockito.when(cloudContentRepository.file(foo, fileName)).thenReturn(masterKeyBackupFile); + + Mockito.when(cloudContentRepository.write(Mockito.eq(masterKeyBackupFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { + DataSource in = invocationOnMock.getArgument(1); + String masterKeyFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining()); + assertThat(masterKeyFileContent, is(masterkeyContent)); + return invocationOnMock.getArgument(0); + }); + + // 3. Create new Masterkey file + String changedMasterkey; + if (unverifiedVaultConfig.isPresent()) { + changedMasterkey = "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"hmacMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}"; + } else { + changedMasterkey = "{ \"version\": 7, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"hmacMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"versionMac\": \"cn2sAK6l9p1/w9deJVUuW3h7br056mpv5srvALiYw+g=\"}"; + } + Mockito.when(cloudContentRepository.write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> { + DataSource in = invocationOnMock.getArgument(1); + String masterKeyFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining()); + assertThat(masterKeyFileContent, is(changedMasterkey)); + return invocationOnMock.getArgument(0); + }); + + inTest.changePassword(vault, unverifiedVaultConfig, "foo", "bar"); + + Mockito.verify(cloudContentRepository).read(Mockito.eq(masterKeyFile), Mockito.eq(Optional.empty()), Mockito.any(), Mockito.eq(NO_OP_PROGRESS_AWARE)); + Mockito.verify(cloudContentRepository).write(Mockito.eq(masterKeyBackupFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong()); + Mockito.verify(cloudContentRepository).write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong()); + } + + @DisplayName("isVaultPasswordValid(\"foo\", \"foo\")") + @ParameterizedTest(name = "Legacy vault format {0}") + @ValueSource(booleans = {true, false}) + public void testVaultPasswordVault(boolean legacy) throws BackendException, IOException { + String password = "foo"; + if (legacy) { + assertThat(testVaultPasswordVault(masterkeyV7, Optional.empty(), password), is(true)); + + MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV7.getBytes(StandardCharsets.UTF_8)); + Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile(password), SIV_CTRMAC); + } else { + UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION); + assertThat(testVaultPasswordVault(masterkeyV8, Optional.of(unverifiedVaultConfig), password), is(true)); + + MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV8.getBytes(StandardCharsets.UTF_8)); + Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile(password), SIV_CTRMAC); + } + } + + @DisplayName("isVaultPasswordValid(\"foo\", \"bar\")") + @ParameterizedTest(name = "Legacy vault format {0}") + @ValueSource(booleans = {true, false}) + public void testVaultPasswordVaultInvalidPassword(boolean legacy) throws BackendException, IOException { + String password = "bar"; + if (legacy) { + assertThat(testVaultPasswordVault(masterkeyV7, Optional.empty(), password), is(false)); + } else { + UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION); + assertThat(testVaultPasswordVault(masterkeyV8, Optional.of(unverifiedVaultConfig), password), is(false)); + } + } + + + private boolean testVaultPasswordVault(String masterkeyContent, Optional unverifiedVaultConfig, String password) throws BackendException { + TestFolder rootFolder = new RootTestFolder(cloud); + TestFolder foo = new TestFolder(rootFolder, "foo", "/foo"); + TestFile vaultFile = new TestFile(foo, VAULT_FILE_NAME, "/foo/" + VAULT_FILE_NAME, Optional.empty(), Optional.empty()); + TestFile masterKeyFile = new TestFile(foo, MASTERKEY_FILE_NAME, "/foo/" + MASTERKEY_FILE_NAME, Optional.empty(), Optional.empty()); + + Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile); + Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile); + Mockito.when(cloudContentRepository.resolve(vault.getCloud(), vault.getPath())).thenReturn(foo); + + + Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile); + Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile); + Mockito.when(cloudContentRepository.resolve(vault.getCloud(), vault.getPath())).thenReturn(foo); + + // 1. Read masterkey + Mockito.doAnswer(invocation -> { + OutputStream out = invocation.getArgument(2); + CopyStream.copyStreamToStream(new ByteArrayInputStream(masterkeyContent.getBytes()), out); + return null; + }).when(cloudContentRepository).read(Mockito.eq(masterKeyFile), Mockito.eq(Optional.empty()), Mockito.any(), Mockito.eq(NO_OP_PROGRESS_AWARE)); + + return inTest.isVaultPasswordValid(vault, unverifiedVaultConfig, password); } } From ae4c57ac8ec956b08b7f6533f107ea478b59d80c Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Mon, 12 Apr 2021 16:00:22 +0200 Subject: [PATCH 016/135] Fix MasterkeyCryptoCloudProviderTest and enhance logging when test fails --- data/build.gradle | 21 +++++++++++++++++++++ domain/build.gradle | 9 +++++---- presentation/build.gradle | 19 +++++++++++++------ util/build.gradle | 13 +++++++++++++ 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/data/build.gradle b/data/build.gradle index 2187a71f..28005cbf 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -17,6 +17,8 @@ android { buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}" buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\"" + + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } compileOptions { @@ -98,6 +100,11 @@ dependencies { // dagger annotationProcessor dependencies.daggerCompiler implementation dependencies.dagger + + api dependencies.jsonWebTokenApi + implementation dependencies.jsonWebTokenImpl + implementation dependencies.jsonWebTokenJson + // cloud implementation dependencies.dropbox implementation dependencies.msgraph @@ -150,6 +157,7 @@ dependencies { testRuntimeOnly dependencies.junit4Engine testImplementation dependencies.mockito + testImplementation dependencies.mockitoInline testImplementation dependencies.hamcrest } @@ -160,3 +168,16 @@ configurations { static def getApiKey(key) { return System.getenv().getOrDefault(key, "") } + +tasks.withType(Test) { + testLogging { + events "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + + showStandardStreams = false + } +} diff --git a/domain/build.gradle b/domain/build.gradle index 711ecff4..a148564a 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -16,6 +16,8 @@ android { buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}" buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\"" + + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } compileOptions { @@ -50,10 +52,8 @@ dependencies { implementation dependencies.appcompat api dependencies.jsonWebTokenApi - runtimeOnly dependencies.jsonWebTokenImpl - runtimeOnly(dependencies.jsonWebTokenJson) { - exclude group: 'org.json', module: 'json' //provided by Android natively - } + implementation dependencies.jsonWebTokenImpl + implementation dependencies.jsonWebTokenJson // test testImplementation dependencies.junit @@ -65,6 +65,7 @@ dependencies { testRuntimeOnly dependencies.junit4Engine testImplementation dependencies.mockito + testImplementation dependencies.mockitoInline testImplementation dependencies.hamcrest } diff --git a/presentation/build.gradle b/presentation/build.gradle index 8a1c5f99..ef608fe8 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -169,12 +169,6 @@ dependencies { implementation dependencies.zxcvbn implementation dependencies.rxBinding - api dependencies.jsonWebTokenApi - runtimeOnly dependencies.jsonWebTokenImpl - runtimeOnly(dependencies.jsonWebTokenJson) { - exclude group: 'org.json', module: 'json' //provided by Android natively - } - // multidex implementation dependencies.multidex @@ -237,3 +231,16 @@ androidExtensions { static def getApiKey(key) { return System.getenv().getOrDefault(key, "") } + +tasks.withType(Test) { + testLogging { + events "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + + showStandardStreams = false + } +} diff --git a/util/build.gradle b/util/build.gradle index b0a47e2f..3f5ebfd5 100644 --- a/util/build.gradle +++ b/util/build.gradle @@ -73,3 +73,16 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" } + +tasks.withType(Test) { + testLogging { + events "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + + showStandardStreams = false + } +} From cfe1716f8fe5c1d0b391509ef64dad78de153fd6 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 13 Apr 2021 14:57:50 +0200 Subject: [PATCH 017/135] #198 enable desugaring to fix OneDrive in Android 6.0 on some devices --- buildsystem/dependencies.gradle | 5 +++++ data/build.gradle | 4 ++++ domain/build.gradle | 4 ++++ presentation/build.gradle | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index da0c6a0b..1764f584 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -20,6 +20,8 @@ ext { androidSupportAppcompatVersion = '1.2.0' androidSupportDesignVersion = '1.3.0' + coreDesugaringVersion = '1.0.9' + // app frameworks and utilities rxJavaVersion = '2.2.21' @@ -92,6 +94,8 @@ ext { jsonWebTokenApiVersion = '0.11.2' + + dependencies = [ android : "com.google.android:android:${androidVersion}", androidAnnotations : "androidx.annotation:annotation:${androidSupportAnnotationsVersion}", @@ -110,6 +114,7 @@ ext { dagger : "com.google.dagger:dagger:${daggerVersion}", daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}", design : "com.google.android.material:material:${androidSupportDesignVersion}", + coreDesugaring : "com.android.tools:desugar_jdk_libs:${coreDesugaringVersion}", dropbox : "com.dropbox.core:dropbox-core-sdk:${dropboxVersion}", espresso : "androidx.test.espresso:espresso-core:${espressoVersion}", googleApiClientAndroid: "com.google.api-client:google-api-client-android:${googleClientVersion}", diff --git a/data/build.gradle b/data/build.gradle index 2187a71f..9b209072 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -22,6 +22,8 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 + + coreLibraryDesugaringEnabled true } lintOptions { @@ -90,6 +92,8 @@ dependencies { implementation project(':msa-auth-for-android') implementation project(':pcloud-sdk-java') + coreLibraryDesugaring dependencies.coreDesugaring + // cryptomator implementation dependencies.cryptolib diff --git a/domain/build.gradle b/domain/build.gradle index 711ecff4..611594d8 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -21,6 +21,8 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 + + coreLibraryDesugaringEnabled true } lintOptions { @@ -39,6 +41,8 @@ dependencies { annotationProcessor project(':generator') annotationProcessor dependencies.daggerCompiler + coreLibraryDesugaring dependencies.coreDesugaring + compileOnly dependencies.javaxAnnotation api dependencies.timber diff --git a/presentation/build.gradle b/presentation/build.gradle index 8a1c5f99..739ee947 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -34,6 +34,8 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 + + coreLibraryDesugaringEnabled true } lintOptions { @@ -122,6 +124,8 @@ dependencies { implementation project(':data') implementation project(':pcloud-sdk-android') + coreLibraryDesugaring dependencies.coreDesugaring + // dagger kapt dependencies.daggerCompiler implementation dependencies.dagger From b304d6e828bb0f6b3365f8d3977f445deafe7221 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Mon, 19 Apr 2021 15:30:22 +0200 Subject: [PATCH 018/135] Rename crypto maxFileNameLength to shorteningThreshold --- .../crypto/CryptoCloudContentRepository.java | 2 +- .../data/cloud/crypto/CryptoImplDecorator.java | 6 +++--- .../cloud/crypto/CryptoImplVaultFormat7.java | 6 +++--- .../cloud/crypto/CryptoImplVaultFormat8.java | 4 ++-- .../cloud/crypto/CryptoImplVaultFormatPre7.java | 8 ++++---- .../crypto/MasterkeyCryptoCloudProvider.java | 10 +++++----- .../cryptomator/data/cloud/crypto/VaultConfig.kt | 16 ++++++++-------- .../crypto/MasterkeyCryptoCloudProviderTest.java | 6 +++--- .../main/java/org/cryptomator/domain/Vault.java | 16 ++++++++-------- .../cryptomator/presentation/model/VaultModel.kt | 4 ++-- 10 files changed, 39 insertions(+), 39 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.java index 78e53a7f..ac83afa7 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.java @@ -38,7 +38,7 @@ class CryptoCloudContentRepository implements CloudContentRepository cryptor; private final CloudFolder storageLocation; private RootCryptoFolder root; - CryptoImplDecorator(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int maxFileNameLength) { + CryptoImplDecorator(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int shorteningThreshold) { this.context = context; this.cryptor = cryptor; this.cloudContentRepository = cloudContentRepository; this.storageLocation = storageLocation; this.dirIdCache = dirIdCache; - this.maxFileNameLength = maxFileNameLength; + this.shorteningThreshold = shorteningThreshold; } abstract CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException; diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java index 2b84fdb1..085a1ba9 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.java @@ -67,8 +67,8 @@ class CryptoImplVaultFormat7 extends CryptoImplDecorator { super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, CryptoConstants.DEFAULT_MAX_FILE_NAME); } - CryptoImplVaultFormat7(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int maxFileNameLength) { - super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, maxFileNameLength); + CryptoImplVaultFormat7(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int shorteningThreshold) { + super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold); } @Override @@ -85,7 +85,7 @@ class CryptoImplVaultFormat7 extends CryptoImplDecorator { .fileNameCryptor() // .encryptFilename(BASE64, name, dirIdInfo(cryptoFolder).getId().getBytes(UTF_8)) + CLOUD_NODE_EXT; - if (ciphertextName.length() > maxFileNameLength) { + if (ciphertextName.length() > shorteningThreshold) { ciphertextName = deflate(cryptoFolder, ciphertextName); } return ciphertextName; diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java index 356a4fb0..7da228a6 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat8.java @@ -9,8 +9,8 @@ import org.cryptomator.util.Supplier; public class CryptoImplVaultFormat8 extends CryptoImplVaultFormat7 { - CryptoImplVaultFormat8(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int maxFileNameLength) { - super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, maxFileNameLength); + CryptoImplVaultFormat8(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int shorteningThreshold) { + super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold); } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java index dcde522a..45a2916e 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.java @@ -36,7 +36,7 @@ import static org.cryptomator.util.Encodings.UTF_8; final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator { - static final int MAX_FILE_NAME_LENGTH = 129; + static final int SHORTENING_THRESHOLD = 129; private static final String DIR_PREFIX = "0"; private static final String SYMLINK_PREFIX = "1S"; private static final String LONG_NAME_FILE_EXT = ".lng"; @@ -45,7 +45,7 @@ final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator { private static final Pattern BASE32_ENCRYPTED_NAME_PATTERN = Pattern.compile("^(0|1S)?(([A-Z2-7]{8})*[A-Z2-7=]{8})$"); CryptoImplVaultFormatPre7(Context context, Supplier cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) { - super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, MAX_FILE_NAME_LENGTH); + super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, SHORTENING_THRESHOLD); } @Override @@ -74,7 +74,7 @@ final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator { private String encryptName(CryptoFolder cryptoParent, String name, String prefix) throws BackendException { String ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(name, dirIdInfo(cryptoParent).getId().getBytes(UTF_8)); - if (ciphertextName.length() > maxFileNameLength) { + if (ciphertextName.length() > shorteningThreshold) { ciphertextName = deflate(ciphertextName); } return ciphertextName; @@ -139,7 +139,7 @@ final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator { if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) { try { ciphertextName = inflate(ciphertextName); - if (ciphertextName.length() <= maxFileNameLength) { + if (ciphertextName.length() <= shorteningThreshold) { cloudFile = inflatePermanently(cloudFile, ciphertextName); } } catch (NoSuchCloudFileException e) { diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java index daed730f..2ea3cc6d 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java @@ -81,7 +81,7 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { .vaultFormat(MAX_VAULT_VERSION) // .cipherCombo(DEFAULT_CIPHER_COMBO) // .keyId(URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME))) // - .maxFilenameLength(DEFAULT_MAX_FILE_NAME) // + .shorteningThreshold(DEFAULT_MAX_FILE_NAME) // .build(); byte[] encodedVaultConfig = vaultConfig.toToken(masterkey.getEncoded()).getBytes(UTF_8); @@ -114,19 +114,19 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { Masterkey masterkey = impl.getKeyFile(password); int vaultFormat; - int maxFileNameLength; + int shorteningThreshold; Cryptor cryptor; if (unverifiedVaultConfig.isPresent()) { VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get()); vaultFormat = vaultConfig.getVaultFormat(); assertVaultVersionIsSupported(vaultConfig.getVaultFormat()); - maxFileNameLength = vaultConfig.getMaxFilenameLength(); + shorteningThreshold = vaultConfig.getShorteningThreshold(); cryptor = cryptorFor(masterkey, vaultConfig.getCipherCombo()); } else { vaultFormat = MasterkeyFileAccess.readAllegedVaultVersion(impl.keyFileData); assertLegacyVaultVersionIsSupported(vaultFormat); - maxFileNameLength = vaultFormat > 6 ? CryptoConstants.DEFAULT_MAX_FILE_NAME : CryptoImplVaultFormatPre7.MAX_FILE_NAME_LENGTH; + shorteningThreshold = vaultFormat > 6 ? CryptoConstants.DEFAULT_MAX_FILE_NAME : CryptoImplVaultFormatPre7.SHORTENING_THRESHOLD; cryptor = cryptorFor(masterkey, SIV_CTRMAC); } @@ -138,7 +138,7 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { Vault vault = aCopyOf(token.getVault()) // .withUnlocked(true) // .withFormat(vaultFormat) // - .withMaxFileNameLength(maxFileNameLength) // + .withShorteningThreshold(shorteningThreshold) // .build(); cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor); diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt index 69bd4039..31f2b05c 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/VaultConfig.kt @@ -24,7 +24,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { val id: String val vaultFormat: Int val cipherCombo: VaultCipherCombo - val maxFilenameLength: Int + val shorteningThreshold: Int fun toToken(rawKey: ByteArray): String { return Jwts.builder() @@ -32,7 +32,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { .setId(id) // .claim(JSON_KEY_VAULTFORMAT, vaultFormat) // .claim(JSON_KEY_CIPHERCONFIG, cipherCombo.name) // - .claim(JSON_KEY_MAXFILENAMELEN, maxFilenameLength) // + .claim(JSON_KEY_SHORTENING_THRESHOLD, shorteningThreshold) // .signWith(Keys.hmacShaKeyFor(rawKey)) // .compact() } @@ -42,7 +42,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { internal var id: String = UUID.randomUUID().toString() internal var vaultFormat = CryptoConstants.MAX_VAULT_VERSION; internal var cipherCombo = VaultCipherCombo.SIV_CTRMAC - internal var maxFilenameLength = CryptoConstants.DEFAULT_MAX_FILE_NAME; + internal var shorteningThreshold = CryptoConstants.DEFAULT_MAX_FILE_NAME; lateinit var keyId: URI fun keyId(keyId: URI): VaultConfigBuilder { @@ -55,8 +55,8 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { return this } - fun maxFilenameLength(maxFilenameLength: Int): VaultConfigBuilder { - this.maxFilenameLength = maxFilenameLength + fun shorteningThreshold(shorteningThreshold: Int): VaultConfigBuilder { + this.shorteningThreshold = shorteningThreshold return this } @@ -79,7 +79,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { private const val JSON_KEY_VAULTFORMAT = "format" private const val JSON_KEY_CIPHERCONFIG = "cipherCombo" - private const val JSON_KEY_MAXFILENAMELEN = "maxFilenameLen" + private const val JSON_KEY_SHORTENING_THRESHOLD = "shorteningThreshold" private const val JSON_KEY_ID = "kid" @JvmStatic @@ -112,7 +112,7 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { .id(parser.header[JSON_KEY_ID] as String) // .cipherCombo(VaultCipherCombo.valueOf(parser.body.get(JSON_KEY_CIPHERCONFIG, String::class.java))) // .vaultFormat(unverifiedVaultConfig.vaultFormat) // - .maxFilenameLength(parser.body[JSON_KEY_MAXFILENAMELEN] as Int) + .shorteningThreshold(parser.body[JSON_KEY_SHORTENING_THRESHOLD] as Int) VaultConfig(vaultConfigBuilder) } catch (e: Exception) { @@ -148,6 +148,6 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { keyId = builder.keyId vaultFormat = builder.vaultFormat cipherCombo = builder.cipherCombo - maxFilenameLength = builder.maxFilenameLength + shorteningThreshold = builder.shorteningThreshold } } diff --git a/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java index d157a8cc..5d137015 100644 --- a/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java +++ b/data/src/test/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProviderTest.java @@ -53,7 +53,7 @@ class MasterkeyCryptoCloudProviderTest { private final String masterkeyV8 = "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}"; private final String masterkeyV7 = "{ \"version\": 7, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"cn2sAK6l9p1/w9deJVUuW3h7br056mpv5srvALiYw+g=\"}"; - private final String vaultConfig = "eyJraWQiOiJtYXN0ZXJrZXlmaWxlOm1hc3RlcmtleS5jcnlwdG9tYXRvciIsImFsZyI6IkhTNTEyIn0.eyJtYXhGaWxlbmFtZUxlbiI6MjIwLCJmb3JtYXQiOjgsImNpcGhlckNvbWJvIjoiU0lWX0NUUk1BQyJ9.umiAcGObWuVISugrQu16hznDHIFM7moD1ukA1r5V1DRA0GjHQk1p6S9hkL0PaMD7xl04jSttMRalOYU1sg4wqQ"; + private final String vaultConfig = "eyJraWQiOiJtYXN0ZXJrZXlmaWxlOm1hc3RlcmtleS5jcnlwdG9tYXRvciIsImFsZyI6IkhTNTEyIn0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.Evt5KXS_35pm53DynIwL3qvXWF56UkfqDZKv12n7SD288jzcdvvmtvu5sQhhqvxU6CPL4Q9v3yFQ_lvBynyrYA"; private Context context; private Cloud cloud; @@ -173,7 +173,7 @@ class MasterkeyCryptoCloudProviderTest { MatcherAssert.assertThat(result.isUnlocked(), is(true)); MatcherAssert.assertThat(result.getFormat(), is(8)); - MatcherAssert.assertThat(result.getMaxFileNameLength(), is(DEFAULT_MAX_FILE_NAME)); + MatcherAssert.assertThat(result.getShorteningThreshold(), is(DEFAULT_MAX_FILE_NAME)); Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile("foo"), SIV_CTRMAC); Mockito.verify(cryptoCloudContentRepositoryFactory).registerCryptor(Mockito.any(Vault.class), Mockito.any(Cryptor.class)); @@ -200,7 +200,7 @@ class MasterkeyCryptoCloudProviderTest { MatcherAssert.assertThat(result.isUnlocked(), is(true)); MatcherAssert.assertThat(result.getFormat(), is(MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG)); - MatcherAssert.assertThat(result.getMaxFileNameLength(), is(DEFAULT_MAX_FILE_NAME)); + MatcherAssert.assertThat(result.getShorteningThreshold(), is(DEFAULT_MAX_FILE_NAME)); Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile("foo"), SIV_CTRMAC); Mockito.verify(cryptoCloudContentRepositoryFactory).registerCryptor(Mockito.any(Vault.class), Mockito.any(Cryptor.class)); diff --git a/domain/src/main/java/org/cryptomator/domain/Vault.java b/domain/src/main/java/org/cryptomator/domain/Vault.java index 4b84eecb..b142ad8a 100644 --- a/domain/src/main/java/org/cryptomator/domain/Vault.java +++ b/domain/src/main/java/org/cryptomator/domain/Vault.java @@ -13,7 +13,7 @@ public class Vault implements Serializable { private final boolean unlocked; private final String password; private final int format; - private final int maxFileNameLength; + private final int shorteningThreshold; private final int position; private Vault(Builder builder) { @@ -25,7 +25,7 @@ public class Vault implements Serializable { this.cloudType = builder.cloudType; this.password = builder.password; this.format = builder.format; - this.maxFileNameLength = builder.maxFileNameLength; + this.shorteningThreshold = builder.shorteningThreshold; this.position = builder.position; } @@ -43,7 +43,7 @@ public class Vault implements Serializable { .withUnlocked(vault.isUnlocked()) // .withSavedPassword(vault.getPassword()) // .withFormat(vault.getFormat()) // - .withMaxFileNameLength(vault.getMaxFileNameLength()) // + .withShorteningThreshold(vault.getShorteningThreshold()) // .withPosition(vault.getPosition()); } @@ -79,8 +79,8 @@ public class Vault implements Serializable { return format; } - public int getMaxFileNameLength() { - return maxFileNameLength; + public int getShorteningThreshold() { + return shorteningThreshold; } public int getPosition() { @@ -117,7 +117,7 @@ public class Vault implements Serializable { private boolean unlocked; private String password; private int format = -1; - private int maxFileNameLength = -1; + private int shorteningThreshold = -1; private int position = -1; private Builder() { @@ -189,8 +189,8 @@ public class Vault implements Serializable { return this; } - public Builder withMaxFileNameLength(int maxFileNameLength) { - this.maxFileNameLength = maxFileNameLength; + public Builder withShorteningThreshold(int shorteningThreshold) { + this.shorteningThreshold = shorteningThreshold; return this; } diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt index a7d95415..177c1a52 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt @@ -17,8 +17,8 @@ class VaultModel(private val vault: Vault) : Serializable { get() = vault.position val format: Int get() = vault.format - val maxFileNameLength: Int - get() = vault.maxFileNameLength + val shorteningThreshold: Int + get() = vault.shorteningThreshold fun toVault(): Vault { return vault From 714223743e3c50d1ec603d591abbe18249a69b96 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 30 Apr 2021 16:26:21 +0200 Subject: [PATCH 019/135] Skip deployment to F-Droid when beta option set [ci skip] --- fastlane/Fastfile | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 71fd794e..6e06db4a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -177,25 +177,31 @@ platform :android do |options| } ) - FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "repo/Cryptomator.apk") + if options[:beta] + puts "Skipping deployment to F-Droid cause there isn't currently a beta channel" + else + puts "Updating F-Droid" - sh("cp -r metadata/android/ metadata/org.cryptomator/") - FileUtils.cp("metadata/org.cryptomator/en-US/changelogs/default.txt", "metadata/org.cryptomator/en-US/changelogs/#{version}.txt") - FileUtils.cp("metadata/org.cryptomator/de-DE/changelogs/default.txt", "metadata/org.cryptomator/de-DE/changelogs/#{version}.txt") - sh("fdroid update && fdroid rewritemeta") - sh("rm -r metadata/org.cryptomator/") + FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "repo/Cryptomator.apk") - aws_s3( - bucket: ENV['S3_BUCKET'], - endpoint: ENV['S3_ENDPOINT'], - region: ENV['S3_REGION'], - access_key: ENV['S3_ACCESS_KEY'], - secret_access_key: ENV['S3_SECRET_ACCESS_KEY'], - path: "android/fdroid", - folder: "fastlane/repo", - skip_html_upload: true, - apk: '' - ) + sh("cp -r metadata/android/ metadata/org.cryptomator/") + FileUtils.cp("metadata/org.cryptomator/en-US/changelogs/default.txt", "metadata/org.cryptomator/en-US/changelogs/#{version}.txt") + FileUtils.cp("metadata/org.cryptomator/de-DE/changelogs/default.txt", "metadata/org.cryptomator/de-DE/changelogs/#{version}.txt") + sh("fdroid update && fdroid rewritemeta") + sh("rm -r metadata/org.cryptomator/") + + aws_s3( + bucket: ENV['S3_BUCKET'], + endpoint: ENV['S3_ENDPOINT'], + region: ENV['S3_REGION'], + access_key: ENV['S3_ACCESS_KEY'], + secret_access_key: ENV['S3_SECRET_ACCESS_KEY'], + path: "android/fdroid", + folder: "fastlane/repo", + skip_html_upload: true, + apk: '' + ) + end FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_fdroid_signed.apk") end From 1eb8c079c6545cdaa12f2ce958a91aa3f26a4025 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Sat, 1 May 2021 14:32:14 +0200 Subject: [PATCH 020/135] Verify SHA256 of the APK in update process --- data/build.gradle | 2 +- .../cryptomator/data/db/DatabaseUpgrades.java | 6 +- .../org/cryptomator/data/db/Upgrade6To7.kt | 41 +++++++++++++ .../data/db/entities/UpdateCheckEntity.java | 16 +++++- .../repository/UpdateCheckRepositoryImpl.java | 57 +++++++++++++++---- .../update/GeneralUpdateErrorException.java | 4 ++ .../HashMismatchUpdateCheckException.java | 9 +++ .../domain/usecases/UpdateCheck.java | 2 + fastlane/Fastfile | 2 + .../exception/ExceptionHandlers.kt | 2 + presentation/src/main/res/values/strings.xml | 1 + 11 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/update/HashMismatchUpdateCheckException.java diff --git a/data/build.gradle b/data/build.gradle index 881c59b7..b59f7d48 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -74,7 +74,7 @@ android { } greendao { - schemaVersion 6 + schemaVersion 7 } configurations.all { diff --git a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java index 100755cd..59fef0f2 100644 --- a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java +++ b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java @@ -24,7 +24,8 @@ class DatabaseUpgrades { Upgrade2To3 upgrade2To3, // Upgrade3To4 upgrade3To4, // Upgrade4To5 upgrade4To5, // - Upgrade5To6 upgrade5To6) { + Upgrade5To6 upgrade5To6, // + Upgrade6To7 upgrade6To7) { availableUpgrades = defineUpgrades( // upgrade0To1, // @@ -32,7 +33,8 @@ class DatabaseUpgrades { upgrade2To3, // upgrade3To4, // upgrade4To5, // - upgrade5To6); + upgrade5To6, // + upgrade6To7); } private static Comparator reverseOrder() { diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt new file mode 100644 index 00000000..63c2e4a7 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt @@ -0,0 +1,41 @@ +package org.cryptomator.data.db + +import org.greenrobot.greendao.database.Database +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class Upgrade6To7 @Inject constructor() : DatabaseUpgrade(6, 7) { + + override fun internalApplyTo(db: Database, origin: Int) { + db.beginTransaction() + try { + changeUpdateEntityToSupportSha256Verification(db) + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private fun changeUpdateEntityToSupportSha256Verification(db: Database) { + Sql.alterTable("UPDATE_CHECK_ENTITY").renameTo("UPDATE_CHECK_ENTITY_OLD").executeOn(db) + + Sql.createTable("UPDATE_CHECK_ENTITY") // + .id() // + .optionalText("LICENSE_TOKEN") // + .optionalText("RELEASE_NOTE") // + .optionalText("VERSION") // + .optionalText("URL_TO_APK") // + .optionalText("APK_SHA256") // + .optionalText("URL_TO_RELEASE_NOTE") // + .executeOn(db) + + Sql.insertInto("UPDATE_CHECK_ENTITY") // + .select("_id", "LICENSE_TOKEN", "RELEASE_NOTE", "VERSION", "URL_TO_APK", "URL_TO_RELEASE_NOTE") // + .columns("_id", "LICENSE_TOKEN", "RELEASE_NOTE", "VERSION", "URL_TO_APK", "URL_TO_RELEASE_NOTE") // + .from("UPDATE_CHECK_ENTITY_OLD") // + .executeOn(db) + + Sql.dropTable("UPDATE_CHECK_ENTITY_OLD").executeOn(db) + } +} diff --git a/data/src/main/java/org/cryptomator/data/db/entities/UpdateCheckEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/UpdateCheckEntity.java index 10323409..bb27dfdc 100644 --- a/data/src/main/java/org/cryptomator/data/db/entities/UpdateCheckEntity.java +++ b/data/src/main/java/org/cryptomator/data/db/entities/UpdateCheckEntity.java @@ -18,18 +18,22 @@ public class UpdateCheckEntity extends DatabaseEntity { private String urlToApk; + private String apkSha256; + private String urlToReleaseNote; public UpdateCheckEntity() { } - @Generated(hash = 38676936) - public UpdateCheckEntity(Long id, String licenseToken, String releaseNote, String version, String urlToApk, String urlToReleaseNote) { + @Generated(hash = 67239496) + public UpdateCheckEntity(Long id, String licenseToken, String releaseNote, String version, String urlToApk, String apkSha256, + String urlToReleaseNote) { this.id = id; this.licenseToken = licenseToken; this.releaseNote = releaseNote; this.version = version; this.urlToApk = urlToApk; + this.apkSha256 = apkSha256; this.urlToReleaseNote = urlToReleaseNote; } @@ -81,4 +85,12 @@ public class UpdateCheckEntity extends DatabaseEntity { public void setUrlToReleaseNote(String urlToReleaseNote) { this.urlToReleaseNote = urlToReleaseNote; } + + public String getApkSha256() { + return this.apkSha256; + } + + public void setApkSha256(String apkSha256) { + this.apkSha256 = apkSha256; + } } diff --git a/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java b/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java index 2c053cfb..41229dfe 100644 --- a/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java @@ -1,22 +1,28 @@ package org.cryptomator.data.repository; +import android.content.Context; +import android.net.Uri; + import com.google.common.io.BaseEncoding; +import org.apache.commons.codec.binary.Hex; import org.cryptomator.data.db.Database; import org.cryptomator.data.db.entities.UpdateCheckEntity; import org.cryptomator.data.util.UserAgentInterceptor; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.FatalBackendException; import org.cryptomator.domain.exception.update.GeneralUpdateErrorException; -import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException; +import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException; import org.cryptomator.domain.repository.UpdateCheckRepository; import org.cryptomator.domain.usecases.UpdateCheck; import org.cryptomator.util.Optional; import java.io.File; import java.io.IOException; +import java.security.DigestInputStream; import java.security.Key; import java.security.KeyFactory; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.interfaces.ECPublicKey; import java.security.spec.InvalidKeySpecException; @@ -25,7 +31,6 @@ import java.security.spec.X509EncodedKeySpec; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; -import javax.net.ssl.SSLHandshakeException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -42,11 +47,13 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { private final Database database; private final OkHttpClient httpClient; + private final Context context; @Inject - UpdateCheckRepositoryImpl(Database database) { + UpdateCheckRepositoryImpl(Database database, Context context) { this.httpClient = httpClient(); this.database = database; + this.context = context; } private OkHttpClient httpClient() { @@ -65,13 +72,14 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { final UpdateCheckEntity entity = database.load(UpdateCheckEntity.class, 1L); - if (entity.getVersion() != null && entity.getVersion().equals(latestVersion.version)) { + if (entity.getVersion() != null && entity.getVersion().equals(latestVersion.version) && entity.getApkSha256() != null) { return Optional.of(new UpdateCheckImpl("", entity)); } UpdateCheck updateCheck = loadUpdateStatus(latestVersion); entity.setUrlToApk(updateCheck.getUrlApk()); entity.setVersion(updateCheck.getVersion()); + entity.setApkSha256(updateCheck.getApkSha256()); database.store(entity); @@ -107,7 +115,18 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { if (response.isSuccessful()) { final BufferedSink sink = Okio.buffer(Okio.sink(file)); sink.writeAll(response.body().source()); + sink.flush(); sink.close(); + + String apkSha256 = calculateSha256(file); + + if(!apkSha256.equals(entity.getApkSha256())) { + file.delete(); + throw new HashMismatchUpdateCheckException(String.format( // + "Sha of calculated hash (%s) doesn't match the specified one (%s)", // + apkSha256, // + entity.getApkSha256())); + } } else { throw new GeneralUpdateErrorException("Failed to load update file, status code is not correct: " + response.code()); } @@ -116,6 +135,20 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { } } + private String calculateSha256(File file) throws GeneralUpdateErrorException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try(DigestInputStream digestInputStream = new DigestInputStream(context.getContentResolver().openInputStream(Uri.fromFile(file)), digest)) { + byte[] buffer = new byte[8192]; + while(digestInputStream.read(buffer) > -1) { + } + } + return new String(Hex.encodeHex(digest.digest())); + } catch (Exception e) { + throw new GeneralUpdateErrorException(e); + } + } + private LatestVersion loadLatestVersion() throws BackendException { try { final Request request = new Request // @@ -123,12 +156,6 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { .url(HOSTNAME_LATEST_VERSION) // .build(); return toLatestVersion(httpClient.newCall(request).execute()); - } catch (SSLHandshakeException e) { - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { - throw new SSLHandshakePreAndroid5UpdateCheckException("Failed to update.", e); - } else { - throw new GeneralUpdateErrorException("Failed to update. General error occurred.", e); - } } catch (IOException e) { throw new GeneralUpdateErrorException("Failed to update. General error occurred.", e); } @@ -181,12 +208,14 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { private final String releaseNote; private final String version; private final String urlApk; + private final String apkSha256; private final String urlReleaseNote; private UpdateCheckImpl(String releaseNote, LatestVersion latestVersion) { this.releaseNote = releaseNote; this.version = latestVersion.version; this.urlApk = latestVersion.urlApk; + this.apkSha256 = latestVersion.apkSha256; this.urlReleaseNote = latestVersion.urlReleaseNote; } @@ -194,6 +223,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { this.releaseNote = releaseNote; this.version = updateCheckEntity.getVersion(); this.urlApk = updateCheckEntity.getUrlToApk(); + this.apkSha256 = updateCheckEntity.getApkSha256(); this.urlReleaseNote = updateCheckEntity.getUrlToReleaseNote(); } @@ -212,6 +242,11 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { return urlApk; } + @Override + public String getApkSha256() { + return apkSha256; + } + @Override public String getUrlReleaseNote() { return urlReleaseNote; @@ -222,6 +257,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { private final String version; private final String urlApk; + private final String apkSha256; private final String urlReleaseNote; LatestVersion(String json) throws GeneralUpdateErrorException { @@ -234,6 +270,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { version = jws.get("version", String.class); urlApk = jws.get("url", String.class); + apkSha256 = jws.get("apk_sha_256", String.class); urlReleaseNote = jws.get("release_notes", String.class); } catch (Exception e) { throw new GeneralUpdateErrorException("Failed to parse latest version", e); diff --git a/domain/src/main/java/org/cryptomator/domain/exception/update/GeneralUpdateErrorException.java b/domain/src/main/java/org/cryptomator/domain/exception/update/GeneralUpdateErrorException.java index dee9870a..5b6d14f6 100644 --- a/domain/src/main/java/org/cryptomator/domain/exception/update/GeneralUpdateErrorException.java +++ b/domain/src/main/java/org/cryptomator/domain/exception/update/GeneralUpdateErrorException.java @@ -11,4 +11,8 @@ public class GeneralUpdateErrorException extends BackendException { public GeneralUpdateErrorException(final String message, final Exception e) { super(message, e); } + + public GeneralUpdateErrorException(Exception e) { + super(e); + } } diff --git a/domain/src/main/java/org/cryptomator/domain/exception/update/HashMismatchUpdateCheckException.java b/domain/src/main/java/org/cryptomator/domain/exception/update/HashMismatchUpdateCheckException.java new file mode 100644 index 00000000..5168ab01 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/update/HashMismatchUpdateCheckException.java @@ -0,0 +1,9 @@ +package org.cryptomator.domain.exception.update; + +public class HashMismatchUpdateCheckException extends GeneralUpdateErrorException { + + public HashMismatchUpdateCheckException(final String message) { + super(message); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/UpdateCheck.java b/domain/src/main/java/org/cryptomator/domain/usecases/UpdateCheck.java index 494b33da..99bd3f1b 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/UpdateCheck.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/UpdateCheck.java @@ -8,5 +8,7 @@ public interface UpdateCheck { String getUrlApk(); + String getApkSha256(); + String getUrlReleaseNote(); } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6e06db4a..51ba7e52 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -102,11 +102,13 @@ platform :android do |options| server_host = ENV["APK_STORE_BASIC_URL"] base_url = "https://#{server_host}/android/" apk_url = "#{base_url}#{version}/Cryptomator-#{version}.apk" + apk_sha_256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_signed.apk" release_note_url = "#{base_url}#{version}/release-notes.html" claims = { "version": version, "url": apk_url, + "apk_sha_256": apk_sha_256, "release_notes": release_note_url } diff --git a/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt b/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt index 93c2365f..89949aff 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt @@ -16,6 +16,7 @@ import org.cryptomator.domain.exception.authentication.AuthenticationException import org.cryptomator.domain.exception.license.LicenseNotValidException import org.cryptomator.domain.exception.license.NoLicenseAvailableException import org.cryptomator.domain.exception.update.GeneralUpdateErrorException +import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException import org.cryptomator.presentation.R import org.cryptomator.presentation.ui.activity.view.View @@ -44,6 +45,7 @@ class ExceptionHandlers @Inject constructor(private val context: Context, defaul staticHandler(UnableToDecryptWebdavPasswordException::class.java, R.string.error_failed_to_decrypt_webdav_password) staticHandler(LicenseNotValidException::class.java, R.string.dialog_enter_license_not_valid_content) staticHandler(NoLicenseAvailableException::class.java, R.string.dialog_enter_license_no_content) + staticHandler(HashMismatchUpdateCheckException::class.java, R.string.error_hash_mismatch_update) staticHandler(GeneralUpdateErrorException::class.java, R.string.error_general_update) staticHandler(SSLHandshakePreAndroid5UpdateCheckException::class.java, R.string.error_general_update) staticHandler(NoSuchBucketException::class.java, R.string.error_no_such_bucket) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 25a5c224..94fbfbc4 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -28,6 +28,7 @@ File names can\'t contain special characters. Vault name can\'t contain special characters. Update check failed. General error occurred. + Update check failed. Calculated hash doesn\'t match the uploaded file Update check failed. No internet connection. Failed to decrypt WebDAV password, please re add in settings Play Services not installed From 3381a12da458fac45a91daae429b2187ed8962b0 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Sun, 2 May 2021 20:42:14 +0200 Subject: [PATCH 021/135] BiometricManager.canAuthenticate() is deprecated Switching to BiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) --- .../cryptomator/presentation/presenter/VaultListPresenter.kt | 4 +++- .../presentation/ui/activity/BiometricAuthSettingsActivity.kt | 4 +++- .../cryptomator/presentation/ui/fragment/SettingsFragment.kt | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt index fd0ec4f7..0b5d50a9 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -537,7 +537,9 @@ class VaultListPresenter @Inject constructor( // } private fun canUseBiometricOn(vault: VaultModel): Boolean { - return vault.password != null && BiometricManager.from(context()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS + return vault.password != null && BiometricManager // + .from(context()) // + .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS } fun onAddExistingVault() { diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BiometricAuthSettingsActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BiometricAuthSettingsActivity.kt index a4de1005..eccb9323 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BiometricAuthSettingsActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BiometricAuthSettingsActivity.kt @@ -36,7 +36,9 @@ class BiometricAuthSettingsActivity : BaseActivity(), // } override fun showSetupBiometricAuthDialog() { - val biometricAuthenticationAvailable = BiometricManager.from(context()).canAuthenticate() + val biometricAuthenticationAvailable = BiometricManager // + .from(context()) // + .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) if (biometricAuthenticationAvailable == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { showDialog(EnrollSystemBiometricDialog.newInstance()) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt index a8cd7886..074155b0 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt @@ -99,7 +99,9 @@ class SettingsFragment : PreferenceFragmentCompat() { private fun activity(): SettingsActivity = this.activity as SettingsActivity private fun isBiometricAuthenticationNotAvailableRemovePreference() { - val biometricAuthenticationAvailable = BiometricManager.from(requireContext()).canAuthenticate() + val biometricAuthenticationAvailable = BiometricManager // + .from(requireContext()) // + .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) if (biometricAuthenticationAvailable != BiometricManager.BIOMETRIC_SUCCESS && biometricAuthenticationAvailable != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { From df57e01fce9f5b029487bb5eb397a25842213229 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 5 May 2021 14:41:03 +0200 Subject: [PATCH 022/135] Throw specific Exception if Masterkey is not at the default location And fix displaying those errors in local storage cloud because the enter password dialog was only partial present when this exception is thrown which leads to a nullpointer. --- .../data/cloud/crypto/MasterkeyCryptoCloudProvider.java | 6 ++++-- .../cryptomator/presentation/exception/ExceptionHandlers.kt | 2 ++ .../presentation/presenter/UnlockVaultPresenter.kt | 2 +- presentation/src/main/res/values/strings.xml | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java index 2ea3cc6d..60806d53 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.java @@ -12,6 +12,7 @@ import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.CancellationException; import org.cryptomator.domain.exception.FatalBackendException; +import org.cryptomator.domain.exception.vaultconfig.UnsupportedMasterkeyLocationException; import org.cryptomator.domain.repository.CloudContentRepository; import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource; import org.cryptomator.domain.usecases.cloud.Flag; @@ -161,8 +162,9 @@ public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider { private CloudFile masterkeyFile(CloudFolder vaultLocation, UnverifiedVaultConfig unverifiedVaultConfig) throws BackendException { String path = unverifiedVaultConfig.getKeyId().getSchemeSpecificPart(); - // TODO / FIXME sanitize path and throw specific exception - //throw new UnsupportedMasterkeyLocationException(unverifiedVaultConfig); + if(!path.equals(MASTERKEY_FILE_NAME)) { + throw new UnsupportedMasterkeyLocationException(unverifiedVaultConfig); + } return cloudContentRepository.file(vaultLocation, path); } diff --git a/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt b/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt index e4769223..adb07a36 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt @@ -18,6 +18,7 @@ import org.cryptomator.domain.exception.license.NoLicenseAvailableException import org.cryptomator.domain.exception.update.GeneralUpdateErrorException import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException +import org.cryptomator.domain.exception.vaultconfig.UnsupportedMasterkeyLocationException import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException import org.cryptomator.domain.exception.vaultconfig.VaultVersionMismatchException @@ -54,6 +55,7 @@ class ExceptionHandlers @Inject constructor(private val context: Context, defaul staticHandler(VaultVersionMismatchException::class.java, R.string.error_vault_version_mismatch) staticHandler(VaultKeyInvalidException::class.java, R.string.error_vault_key_invalid) staticHandler(VaultConfigLoadException::class.java, R.string.error_vault_config_loading) + staticHandler(UnsupportedMasterkeyLocationException::class.java, R.string.error_masterkey_location_not_supported) staticHandler(NoSuchBucketException::class.java, R.string.error_no_such_bucket) exceptionHandlers.add(MissingCryptorExceptionHandler()) exceptionHandlers.add(CancellationExceptionHandler()) diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt index c9375dba..586e6347 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -107,8 +107,8 @@ class UnlockVaultPresenter @Inject constructor( } view?.showBiometricDialog(vaultModel) } else { - startPrepareUnlockUseCase(vaultModel.toVault()) view?.showEnterPasswordDialog(vaultModel) + startPrepareUnlockUseCase(vaultModel.toVault()) } } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 73341cfb..dae556a2 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ General error while loading the vault config Local file isn\'t present anymore after switching back to Cryptomator. Possible changes cannot be propagated back to the cloud. No such bucket + Custom Masterkey location not supported yet From c9185b142b0c4fba3a8746034864953afbbb92bd Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 5 May 2021 14:54:56 +0200 Subject: [PATCH 023/135] Update dependencies --- buildsystem/dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index ae934029..941213ac 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -49,7 +49,7 @@ ext { // cloud provider libs // do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x - cryptolibVersion = '2.0.0-beta6' + cryptolibVersion = '2.0.0-rc1' awsAndroidSdkS3 = '2.23.0' From 454bf9406e93bb360874c79cb3223f5081bd67c1 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 5 May 2021 15:06:41 +0200 Subject: [PATCH 024/135] Update dependencies --- buildsystem/dependencies.gradle | 8 ++-- .../ui/fragment/SettingsFragment.kt | 32 ++++++------- presentation/src/main/res/xml/preferences.xml | 48 +++++++++---------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 96f91c16..15085566 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -5,7 +5,7 @@ allprojects { } ext { - androidBuildToolsVersion = "29.0.2" + androidBuildToolsVersion = "29.0.3" androidMinSdkVersion = 23 androidTargetSdkVersion = 29 androidCompileSdkVersion = 29 @@ -26,7 +26,7 @@ ext { rxAndroidVersion = '2.1.1' rxBindingVersion = '2.2.0' - daggerVersion = '2.35' + daggerVersion = '2.35.1' gsonVersion = '2.8.6' @@ -82,10 +82,10 @@ ext { uiautomatorVersion = '2.2.0' androidxCoreVersion = '1.3.2' - androidxFragmentVersion = '1.3.2' + androidxFragmentVersion = '1.3.3' androidxViewpagerVersion = '1.0.0' androidxSwiperefreshVersion = '1.1.0' - androidxPreferenceVersion = '1.0.0' // 1.1.0 and 1.1.2 does have a bug with the text size + androidxPreferenceVersion = '1.1.1' androidxRecyclerViewVersion = '1.2.0' androidxDocumentfileVersion = '1.0.1' androidxBiometricVersion = '1.1.0' diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt index 074155b0..d86043a1 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt @@ -105,7 +105,7 @@ class SettingsFragment : PreferenceFragmentCompat() { if (biometricAuthenticationAvailable != BiometricManager.BIOMETRIC_SUCCESS && biometricAuthenticationAvailable != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { - val preference = findPreference(BIOMETRIC_AUTHENTICATION_ITEM_KEY) + val preference = findPreference(BIOMETRIC_AUTHENTICATION_ITEM_KEY) as Preference? val generalCategory = findPreference(getString(R.string.screen_settings_section_general)) as PreferenceCategory? generalCategory?.removePreference(preference) @@ -116,7 +116,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } private fun setupAppVersion() { - val preference = findPreference(APP_VERSION_ITEM_KEY) + val preference = findPreference(APP_VERSION_ITEM_KEY) as Preference? val versionName = SpannableString(BuildConfig.VERSION_NAME) versionName.setSpan( // ForegroundColorSpan(ContextCompat.getColor(activity(), R.color.textColorLight)), // @@ -125,7 +125,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } private fun setupLruCacheSize() { - val preference = findPreference(DISPLAY_LRU_CACHE_SIZE_ITEM_KEY) + val preference = findPreference(DISPLAY_LRU_CACHE_SIZE_ITEM_KEY) as Preference? val size = LruFileCacheUtil(requireContext()).totalSize() @@ -152,11 +152,11 @@ class SettingsFragment : PreferenceFragmentCompat() { private fun setupLicense() { when (BuildConfig.FLAVOR) { "apkstore" -> { - findPreference(SharedPreferencesHandler.MAIL)?.title = format(getString(R.string.screen_settings_license_mail), sharedPreferencesHandler.mail()) + (findPreference(SharedPreferencesHandler.MAIL) as Preference?)?.title = format(getString(R.string.screen_settings_license_mail), sharedPreferencesHandler.mail()) setupUpdateCheck() } "fdroid" -> { - findPreference(SharedPreferencesHandler.MAIL)?.title = format(getString(R.string.screen_settings_license_mail), sharedPreferencesHandler.mail()) + (findPreference(SharedPreferencesHandler.MAIL) as Preference?)?.title = format(getString(R.string.screen_settings_license_mail), sharedPreferencesHandler.mail()) removeUpdateCheck() } else -> { @@ -173,7 +173,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } fun setupUpdateCheck() { - val preference = findPreference(UPDATE_CHECK_ITEM_KEY) + val preference = findPreference(UPDATE_CHECK_ITEM_KEY) as Preference? val lastUpdateCheck = sharedPreferencesHandler.lastUpdateCheck() val readableDate: String = if (lastUpdateCheck != null) { @@ -194,17 +194,17 @@ class SettingsFragment : PreferenceFragmentCompat() { override fun onResume() { super.onResume() - findPreference(SEND_ERROR_REPORT_ITEM_KEY)?.onPreferenceClickListener = sendErrorReportClickListener - findPreference(LRU_CACHE_CLEAR_ITEM_KEY)?.onPreferenceClickListener = clearCacheClickListener - findPreference(SharedPreferencesHandler.DEBUG_MODE)?.onPreferenceChangeListener = debugModeChangedListener - findPreference(SharedPreferencesHandler.DISABLE_APP_WHEN_OBSCURED)?.onPreferenceChangeListener = disableAppWhenObscuredChangedListener - findPreference(SharedPreferencesHandler.SECURE_SCREEN)?.onPreferenceChangeListener = disableSecureScreenChangedListener - findPreference(SharedPreferencesHandler.SCREEN_STYLE_MODE)?.onPreferenceChangeListener = screenStyleModeChangedListener - findPreference(SharedPreferencesHandler.PHOTO_UPLOAD)?.onPreferenceChangeListener = useAutoPhotoUploadChangedListener - findPreference(SharedPreferencesHandler.USE_LRU_CACHE)?.onPreferenceChangeListener = useLruChangedListener - findPreference(SharedPreferencesHandler.LRU_CACHE_SIZE)?.onPreferenceChangeListener = useLruChangedListener + (findPreference(SEND_ERROR_REPORT_ITEM_KEY) as Preference?)?.onPreferenceClickListener = sendErrorReportClickListener + (findPreference(LRU_CACHE_CLEAR_ITEM_KEY) as Preference?)?.onPreferenceClickListener = clearCacheClickListener + (findPreference(SharedPreferencesHandler.DEBUG_MODE) as Preference?)?.onPreferenceChangeListener = debugModeChangedListener + (findPreference(SharedPreferencesHandler.DISABLE_APP_WHEN_OBSCURED) as Preference?)?.onPreferenceChangeListener = disableAppWhenObscuredChangedListener + (findPreference(SharedPreferencesHandler.SECURE_SCREEN) as Preference?)?.onPreferenceChangeListener = disableSecureScreenChangedListener + (findPreference(SharedPreferencesHandler.SCREEN_STYLE_MODE) as Preference?)?.onPreferenceChangeListener = screenStyleModeChangedListener + (findPreference(SharedPreferencesHandler.PHOTO_UPLOAD) as Preference?)?.onPreferenceChangeListener = useAutoPhotoUploadChangedListener + (findPreference(SharedPreferencesHandler.USE_LRU_CACHE) as Preference?)?.onPreferenceChangeListener = useLruChangedListener + (findPreference(SharedPreferencesHandler.LRU_CACHE_SIZE) as Preference?)?.onPreferenceChangeListener = useLruChangedListener if (BuildConfig.FLAVOR == "apkstore") { - findPreference(UPDATE_CHECK_ITEM_KEY)?.onPreferenceClickListener = updateCheckClickListener + (findPreference(UPDATE_CHECK_ITEM_KEY) as Preference?)?.onPreferenceClickListener = updateCheckClickListener } } diff --git a/presentation/src/main/res/xml/preferences.xml b/presentation/src/main/res/xml/preferences.xml index 1a3af423..c1c98343 100644 --- a/presentation/src/main/res/xml/preferences.xml +++ b/presentation/src/main/res/xml/preferences.xml @@ -6,7 +6,7 @@ android:key="license" android:title="@string/screen_settings_license"> - @@ -16,16 +16,16 @@ android:key="@string/screen_settings_section_general" android:title="@string/screen_settings_section_general"> - + - + - @@ -34,7 +34,7 @@ android:targetClass="org.cryptomator.presentation.ui.activity.BiometricAuthSettingsActivity" android:targetPackage="@string/app_id" /> - + - @@ -123,7 +123,7 @@ android:action="android.intent.action.MAIN" android:targetClass="org.cryptomator.presentation.ui.activity.AutoUploadChooseVaultActivity" android:targetPackage="@string/app_id" /> - + @@ -144,11 +144,11 @@ android:summary="%s" android:title="@string/dialog_cache_size_title" /> - - @@ -156,18 +156,18 @@ - + - + - @@ -191,40 +191,40 @@ - + - + - + - + - + - + - + - + - + - + @@ -232,7 +232,7 @@ android:key="versionCategory" android:title="@string/screen_settings_section_version"> - @@ -246,7 +246,7 @@ android:summary="%s" android:title="@string/dialog_settings_update_check_interval_title" /> - From 47e9600d27b9670c5dcdd02af0224d2eae773f34 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 5 May 2021 15:06:41 +0200 Subject: [PATCH 025/135] Update dependencies --- buildsystem/dependencies.gradle | 8 ++-- .../ui/fragment/SettingsFragment.kt | 32 ++++++------- presentation/src/main/res/xml/preferences.xml | 48 +++++++++---------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 941213ac..44e1134c 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -5,7 +5,7 @@ allprojects { } ext { - androidBuildToolsVersion = "29.0.2" + androidBuildToolsVersion = "29.0.3" androidMinSdkVersion = 24 androidTargetSdkVersion = 29 androidCompileSdkVersion = 29 @@ -26,7 +26,7 @@ ext { rxAndroidVersion = '2.1.1' rxBindingVersion = '2.2.0' - daggerVersion = '2.35' + daggerVersion = '2.35.1' gsonVersion = '2.8.6' @@ -82,10 +82,10 @@ ext { uiautomatorVersion = '2.2.0' androidxCoreVersion = '1.3.2' - androidxFragmentVersion = '1.3.2' + androidxFragmentVersion = '1.3.3' androidxViewpagerVersion = '1.0.0' androidxSwiperefreshVersion = '1.1.0' - androidxPreferenceVersion = '1.0.0' // 1.1.0 and 1.1.2 does have a bug with the text size + androidxPreferenceVersion = '1.1.1' androidxRecyclerViewVersion = '1.2.0' androidxDocumentfileVersion = '1.0.1' androidxBiometricVersion = '1.1.0' diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt index 074155b0..d86043a1 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt @@ -105,7 +105,7 @@ class SettingsFragment : PreferenceFragmentCompat() { if (biometricAuthenticationAvailable != BiometricManager.BIOMETRIC_SUCCESS && biometricAuthenticationAvailable != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { - val preference = findPreference(BIOMETRIC_AUTHENTICATION_ITEM_KEY) + val preference = findPreference(BIOMETRIC_AUTHENTICATION_ITEM_KEY) as Preference? val generalCategory = findPreference(getString(R.string.screen_settings_section_general)) as PreferenceCategory? generalCategory?.removePreference(preference) @@ -116,7 +116,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } private fun setupAppVersion() { - val preference = findPreference(APP_VERSION_ITEM_KEY) + val preference = findPreference(APP_VERSION_ITEM_KEY) as Preference? val versionName = SpannableString(BuildConfig.VERSION_NAME) versionName.setSpan( // ForegroundColorSpan(ContextCompat.getColor(activity(), R.color.textColorLight)), // @@ -125,7 +125,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } private fun setupLruCacheSize() { - val preference = findPreference(DISPLAY_LRU_CACHE_SIZE_ITEM_KEY) + val preference = findPreference(DISPLAY_LRU_CACHE_SIZE_ITEM_KEY) as Preference? val size = LruFileCacheUtil(requireContext()).totalSize() @@ -152,11 +152,11 @@ class SettingsFragment : PreferenceFragmentCompat() { private fun setupLicense() { when (BuildConfig.FLAVOR) { "apkstore" -> { - findPreference(SharedPreferencesHandler.MAIL)?.title = format(getString(R.string.screen_settings_license_mail), sharedPreferencesHandler.mail()) + (findPreference(SharedPreferencesHandler.MAIL) as Preference?)?.title = format(getString(R.string.screen_settings_license_mail), sharedPreferencesHandler.mail()) setupUpdateCheck() } "fdroid" -> { - findPreference(SharedPreferencesHandler.MAIL)?.title = format(getString(R.string.screen_settings_license_mail), sharedPreferencesHandler.mail()) + (findPreference(SharedPreferencesHandler.MAIL) as Preference?)?.title = format(getString(R.string.screen_settings_license_mail), sharedPreferencesHandler.mail()) removeUpdateCheck() } else -> { @@ -173,7 +173,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } fun setupUpdateCheck() { - val preference = findPreference(UPDATE_CHECK_ITEM_KEY) + val preference = findPreference(UPDATE_CHECK_ITEM_KEY) as Preference? val lastUpdateCheck = sharedPreferencesHandler.lastUpdateCheck() val readableDate: String = if (lastUpdateCheck != null) { @@ -194,17 +194,17 @@ class SettingsFragment : PreferenceFragmentCompat() { override fun onResume() { super.onResume() - findPreference(SEND_ERROR_REPORT_ITEM_KEY)?.onPreferenceClickListener = sendErrorReportClickListener - findPreference(LRU_CACHE_CLEAR_ITEM_KEY)?.onPreferenceClickListener = clearCacheClickListener - findPreference(SharedPreferencesHandler.DEBUG_MODE)?.onPreferenceChangeListener = debugModeChangedListener - findPreference(SharedPreferencesHandler.DISABLE_APP_WHEN_OBSCURED)?.onPreferenceChangeListener = disableAppWhenObscuredChangedListener - findPreference(SharedPreferencesHandler.SECURE_SCREEN)?.onPreferenceChangeListener = disableSecureScreenChangedListener - findPreference(SharedPreferencesHandler.SCREEN_STYLE_MODE)?.onPreferenceChangeListener = screenStyleModeChangedListener - findPreference(SharedPreferencesHandler.PHOTO_UPLOAD)?.onPreferenceChangeListener = useAutoPhotoUploadChangedListener - findPreference(SharedPreferencesHandler.USE_LRU_CACHE)?.onPreferenceChangeListener = useLruChangedListener - findPreference(SharedPreferencesHandler.LRU_CACHE_SIZE)?.onPreferenceChangeListener = useLruChangedListener + (findPreference(SEND_ERROR_REPORT_ITEM_KEY) as Preference?)?.onPreferenceClickListener = sendErrorReportClickListener + (findPreference(LRU_CACHE_CLEAR_ITEM_KEY) as Preference?)?.onPreferenceClickListener = clearCacheClickListener + (findPreference(SharedPreferencesHandler.DEBUG_MODE) as Preference?)?.onPreferenceChangeListener = debugModeChangedListener + (findPreference(SharedPreferencesHandler.DISABLE_APP_WHEN_OBSCURED) as Preference?)?.onPreferenceChangeListener = disableAppWhenObscuredChangedListener + (findPreference(SharedPreferencesHandler.SECURE_SCREEN) as Preference?)?.onPreferenceChangeListener = disableSecureScreenChangedListener + (findPreference(SharedPreferencesHandler.SCREEN_STYLE_MODE) as Preference?)?.onPreferenceChangeListener = screenStyleModeChangedListener + (findPreference(SharedPreferencesHandler.PHOTO_UPLOAD) as Preference?)?.onPreferenceChangeListener = useAutoPhotoUploadChangedListener + (findPreference(SharedPreferencesHandler.USE_LRU_CACHE) as Preference?)?.onPreferenceChangeListener = useLruChangedListener + (findPreference(SharedPreferencesHandler.LRU_CACHE_SIZE) as Preference?)?.onPreferenceChangeListener = useLruChangedListener if (BuildConfig.FLAVOR == "apkstore") { - findPreference(UPDATE_CHECK_ITEM_KEY)?.onPreferenceClickListener = updateCheckClickListener + (findPreference(UPDATE_CHECK_ITEM_KEY) as Preference?)?.onPreferenceClickListener = updateCheckClickListener } } diff --git a/presentation/src/main/res/xml/preferences.xml b/presentation/src/main/res/xml/preferences.xml index 1a3af423..c1c98343 100644 --- a/presentation/src/main/res/xml/preferences.xml +++ b/presentation/src/main/res/xml/preferences.xml @@ -6,7 +6,7 @@ android:key="license" android:title="@string/screen_settings_license"> - @@ -16,16 +16,16 @@ android:key="@string/screen_settings_section_general" android:title="@string/screen_settings_section_general"> - + - + - @@ -34,7 +34,7 @@ android:targetClass="org.cryptomator.presentation.ui.activity.BiometricAuthSettingsActivity" android:targetPackage="@string/app_id" /> - + - @@ -123,7 +123,7 @@ android:action="android.intent.action.MAIN" android:targetClass="org.cryptomator.presentation.ui.activity.AutoUploadChooseVaultActivity" android:targetPackage="@string/app_id" /> - + @@ -144,11 +144,11 @@ android:summary="%s" android:title="@string/dialog_cache_size_title" /> - - @@ -156,18 +156,18 @@ - + - + - @@ -191,40 +191,40 @@ - + - + - + - + - + - + - + - + - + - + @@ -232,7 +232,7 @@ android:key="versionCategory" android:title="@string/screen_settings_section_version"> - @@ -246,7 +246,7 @@ android:summary="%s" android:title="@string/dialog_settings_update_check_interval_title" /> - From 5a78e3206de2c2a6f002e692a2ee4d087aebd9bf Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Thu, 6 May 2021 17:28:40 +0200 Subject: [PATCH 026/135] Add the possibility to publish the app as alpha version [ci skip] --- fastlane/Fastfile | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 51ba7e52..e96bf9fa 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -25,10 +25,10 @@ platform :android do |options| # use english-change-log for french language too FileUtils.cp(release_note_path_en, "metadata/android/fr-FR/changelogs/default.txt") - deployToPlaystore(beta:options[:beta]) - deployToServer(beta:options[:beta]) - deployToFDroid(beta:options[:beta]) - createGitHubDraftRelease(beta:options[:beta]) + deployToPlaystore(alpha:options[:alpha], beta:options[:beta]) + deployToServer(alpha:options[:alpha], beta:options[:beta]) + deployToFDroid(alpha:options[:alpha], beta:options[:beta]) + createGitHubDraftRelease(alpha:options[:alpha], beta:options[:beta]) slack( default_payloads: [], # reduce the notification to the minimum @@ -43,8 +43,10 @@ platform :android do |options| lane :deployToPlaystore do |options| deploy_target = "production" - if options[:beta] - deploy_target = "beta" + if options[:alpha] + deploy_target = "alpha" + elsif options[:beta] + deploy_target = "beta" end gradle(task: "clean") @@ -121,8 +123,8 @@ platform :android do |options| latest_version_jsn.write(token) latest_version_jsn.close - if options[:beta] - puts "Skipping deployment to server cause there isn't currently a beta channel" + if options[:alpha] or options[:beta] + puts "Skipping deployment to server cause there isn't currently a alpha/beta channel" else puts "Uploading APK and release note" @@ -179,8 +181,8 @@ platform :android do |options| } ) - if options[:beta] - puts "Skipping deployment to F-Droid cause there isn't currently a beta channel" + if options[:alpha] or options[:beta] + puts "Skipping deployment to F-Droid cause there isn't currently a alpha/beta channel" else puts "Updating F-Droid" @@ -213,7 +215,7 @@ platform :android do |options| target_branch = "main" prerelease = false - if options[:beta] + if options[:alpha] or options[:beta] target_branch = git_branch prerelease = true end From db4cba3ca66f1c619e74287d789274a9b98689bc Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 7 May 2021 14:18:17 +0200 Subject: [PATCH 027/135] Bump version to 1.6.0-alpha1 [ci skip] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 93627bf2..0de4223b 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ allprojects { ext { androidApplicationId = 'org.cryptomator' androidVersionCode = getVersionCode() - androidVersionName = '1.6.0-SNAPSHOT' + androidVersionName = '1.6.0-alpha1' } repositories { mavenCentral() From e54e078c3e62f4fc5464aab205f4c78a921116dc Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 7 May 2021 15:24:59 +0200 Subject: [PATCH 028/135] Update release notes [ci skip] --- fastlane/metadata/android/de-DE/changelogs/default.txt | 7 ++++--- fastlane/metadata/android/en-US/changelogs/default.txt | 7 ++++--- fastlane/release-notes.html | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/fastlane/metadata/android/de-DE/changelogs/default.txt b/fastlane/metadata/android/de-DE/changelogs/default.txt index 4d72d043..a55eb747 100644 --- a/fastlane/metadata/android/de-DE/changelogs/default.txt +++ b/fastlane/metadata/android/de-DE/changelogs/default.txt @@ -1,3 +1,4 @@ -- Unterstützung für S3-kompatiblen Speicher hinzugefügt -- Passwortrichtlinie verbessert, so dass Tresore mit sehr schlechten Passwörtern nicht mehr erstellt werden können -- Problem bei der Dateisuche behoben, falls Globbing aktiviert, die Live-Suche deaktiviert und das Muster Großbuchstaben enthält \ No newline at end of file +- Unterstützung für Tresor-Format 8 hinzugefügt +- Minimale Android-Version auf 7 erhöht (Nebenbemerkung: Android 6 hat fast 2 Jahre lang keine Sicherheitsupdates erhalten) +- Auto-Updater des APK-Stores verbessert +- Unterstützung für schwache biometrische Authentifikatoren eingestellt \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt index ce5afae0..6606090a 100644 --- a/fastlane/metadata/android/en-US/changelogs/default.txt +++ b/fastlane/metadata/android/en-US/changelogs/default.txt @@ -1,3 +1,4 @@ -- Added support for S3 compatible storage -- Enhanced password policy so that vaults with very bad passwords can no longer be created -- Fixed problem in file search when globbing is enabled, live search is disabled and pattern contains uppercase characters \ No newline at end of file +- Added vault format 8 support +- Increased min Android version to 7 (Side note: Android 6 did not receive security updates for almost 2 years) +- Enhanced auto updater of APK store +- Dropped support for weak biometric authenticators \ No newline at end of file diff --git a/fastlane/release-notes.html b/fastlane/release-notes.html index 22d760ac..984fbb35 100644 --- a/fastlane/release-notes.html +++ b/fastlane/release-notes.html @@ -1,5 +1,6 @@
    -
  • Added support for S3 compatible storage
  • -
  • Enhanced password policy so that vaults with very bad passwords can no longer be created
  • -
  • Fixed problem in file search when globbing is enabled, live search is disabled and pattern contains uppercase characters
  • +
  • Added vault format 8 support
  • +
  • Increased min. Android version to 7 (Side note: Android 6 did not receive security updates for almost 2 years)
  • +
  • Enhanced auto updater of APK store
  • +
  • Dropped support for weak biometric authenticators
\ No newline at end of file From 35f64792f2fdc47fd8dc9ec5888211cc6075b5a5 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 7 May 2021 16:50:52 +0200 Subject: [PATCH 029/135] Fix path to the APK while calculating the SHA256 for the JWT [ci skip] --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index e96bf9fa..8237c70c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -104,7 +104,7 @@ platform :android do |options| server_host = ENV["APK_STORE_BASIC_URL"] base_url = "https://#{server_host}/android/" apk_url = "#{base_url}#{version}/Cryptomator-#{version}.apk" - apk_sha_256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_signed.apk" + apk_sha_256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}.apk" release_note_url = "#{base_url}#{version}/release-notes.html" claims = { From cb34a351608d1b82d5788a15b3aa5aa58a2ed958 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 7 May 2021 16:50:52 +0200 Subject: [PATCH 030/135] Fix path to the APK while calculating the SHA256 for the JWT [ci skip] --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index e96bf9fa..8237c70c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -104,7 +104,7 @@ platform :android do |options| server_host = ENV["APK_STORE_BASIC_URL"] base_url = "https://#{server_host}/android/" apk_url = "#{base_url}#{version}/Cryptomator-#{version}.apk" - apk_sha_256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_signed.apk" + apk_sha_256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}.apk" release_note_url = "#{base_url}#{version}/release-notes.html" claims = { From 9068f955ed01fad6c8c927e00b483035affaa351 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Fri, 7 May 2021 16:54:54 +0200 Subject: [PATCH 031/135] Bump version to 1.6.1-SNAPSHOT [ci skip] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 93627bf2..0f5115bf 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ allprojects { ext { androidApplicationId = 'org.cryptomator' androidVersionCode = getVersionCode() - androidVersionName = '1.6.0-SNAPSHOT' + androidVersionName = '1.6.1-SNAPSHOT' } repositories { mavenCentral() From 5e0f88bcff22a5cfb308318085c1b966c602dcf3 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 11 May 2021 14:56:04 +0200 Subject: [PATCH 032/135] Switch from aws-android-sdk-s3 to minio-java --- buildsystem/dependencies.gradle | 10 +- data/build.gradle | 4 +- .../data/cloud/s3/S3ClientFactory.java | 121 +++-- .../cloud/s3/S3CloudContentRepository.java | 22 +- .../data/cloud/s3/S3CloudNodeFactory.java | 28 +- .../org/cryptomator/data/cloud/s3/S3Impl.java | 484 ++++++++---------- presentation/src/main/res/xml/licenses.xml | 14 +- 7 files changed, 325 insertions(+), 358 deletions(-) diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index bee1b83d..cbb1e4af 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -53,8 +53,6 @@ ext { // do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x cryptolibVersion = '2.0.0-rc1' - awsAndroidSdkS3 = '2.23.0' - dropboxVersion = '4.0.0' googleApiServicesVersion = 'v3-rev197-1.25.0' @@ -63,6 +61,9 @@ ext { msgraphVersion = '2.10.0' + minIoVersion = '8.2.1' + staxVersion = '1.2.0' // needed for minIO + commonsCodecVersion = '1.15' recyclerViewFastScrollVersion = '2.0.1' @@ -107,7 +108,6 @@ ext { androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}", androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}", androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}", - awsAndroidS3 : "com.amazonaws:aws-android-sdk-s3:${awsAndroidSdkS3}", documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}", recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}", androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}", @@ -132,9 +132,10 @@ ext { junitParams : "org.junit.jupiter:junit-jupiter-params:${jUnitVersion}", junit4 : "org.junit.jupiter:junit-jupiter:${jUnit4Version}", junit4Engine : "org.junit.vintage:junit-vintage-engine:${jUnitVersion}", - msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}", + minIo : "io.minio:minio:${minIoVersion}", mockito : "org.mockito:mockito-core:${mockitoVersion}", mockitoInline : "org.mockito:mockito-inline:${mockitoInlineVersion}", + msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}", multidex : "androidx.multidex:multidex:${multidexVersion}", okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}", okHttpDigest : "com.burgstaller:okhttp-digest:${okHttpDigestVersion}", @@ -142,6 +143,7 @@ ext { rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}", rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", rxBinding : "com.jakewharton.rxbinding2:rxbinding:${rxBindingVersion}", + stax : "stax:stax:${staxVersion}", testingSupportLib : "com.android.support.test:testing-support-lib:${testingSupportLibVersion}", timber : "com.jakewharton.timber:timber:${timberVersion}", velocity : "org.apache.velocity:velocity:${velocityVersion}", diff --git a/data/build.gradle b/data/build.gradle index 137c58da..2d423315 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -110,10 +110,12 @@ dependencies { implementation dependencies.jsonWebTokenJson // cloud - implementation dependencies.awsAndroidS3 implementation dependencies.dropbox implementation dependencies.msgraph + implementation dependencies.stax + compile dependencies.minIo + playstoreImplementation dependencies.googlePlayServicesAuth apkstoreImplementation dependencies.googlePlayServicesAuth diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.java index ed0c1838..4765af68 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3ClientFactory.java @@ -1,51 +1,97 @@ package org.cryptomator.data.cloud.s3; import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; -import com.amazonaws.Request; -import com.amazonaws.Response; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.handlers.RequestHandler2; -import com.amazonaws.regions.Region; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; - +import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor; import org.cryptomator.domain.S3Cloud; +import org.cryptomator.util.SharedPreferencesHandler; import org.cryptomator.util.crypto.CredentialCryptor; +import org.cryptomator.util.file.LruFileCacheUtil; +import java.util.concurrent.TimeUnit; + +import io.minio.MinioClient; +import okhttp3.Cache; +import okhttp3.CacheControl; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; import timber.log.Timber; +import static org.cryptomator.data.util.NetworkTimeout.CONNECTION; +import static org.cryptomator.data.util.NetworkTimeout.READ; +import static org.cryptomator.data.util.NetworkTimeout.WRITE; +import static org.cryptomator.util.file.LruFileCacheUtil.Cache.S3; + class S3ClientFactory { - private AmazonS3 apiClient; + private MinioClient apiClient; - public AmazonS3 getClient(S3Cloud cloud, Context context) { + private static Interceptor httpLoggingInterceptor(Context context) { + return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context); + } + + public MinioClient getClient(S3Cloud cloud, Context context) { if (apiClient == null) { apiClient = createApiClient(cloud, context); } return apiClient; } - private AmazonS3 createApiClient(S3Cloud cloud, Context context) { - Region region = Region.getRegion(Regions.DEFAULT_REGION); - String endpoint = null; + private MinioClient createApiClient(S3Cloud cloud, Context context) { + final SharedPreferencesHandler sharedPreferencesHandler = new SharedPreferencesHandler(context); + + MinioClient.Builder minioClientBuilder = MinioClient.builder(); + + if (cloud.s3Endpoint() != null) { + minioClientBuilder.endpoint(cloud.s3Endpoint()); + } else { + minioClientBuilder.endpoint("https://s3.amazonaws.com"); + } if (cloud.s3Region() != null) { - region = Region.getRegion(cloud.s3Region()); - } else if (cloud.s3Endpoint() != null) { - endpoint = cloud.s3Endpoint(); + minioClientBuilder.region(cloud.s3Region()); } - AmazonS3Client client = new AmazonS3Client(new BasicAWSCredentials(decrypt(cloud.accessKey(), context), decrypt(cloud.secretKey(), context)), region); + OkHttpClient.Builder httpClientBuilder = new OkHttpClient() // + .newBuilder() // + .connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) // + .readTimeout(READ.getTimeout(), READ.getUnit()) // + .writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) // + .addInterceptor(httpLoggingInterceptor(context)); - if (endpoint != null) { - client.setEndpoint(cloud.s3Endpoint()); + if (sharedPreferencesHandler.useLruCache()) { + final Cache cache = new Cache(new LruFileCacheUtil(context).resolve(S3), sharedPreferencesHandler.lruCacheSize()); + httpClientBuilder.cache(cache).addInterceptor(provideOfflineCacheInterceptor(context)); } - client.addRequestHandler(new LoggingAwareRequestHandler()); + return minioClientBuilder.credentials(decrypt(cloud.accessKey(), context), decrypt(cloud.secretKey(), context)).httpClient(httpClientBuilder.build()).build(); + } - return client; + private static Interceptor provideOfflineCacheInterceptor(final Context context) { + return chain -> { + Request request = chain.request(); + + if (isNetworkAvailable(context)) { + final CacheControl cacheControl = new CacheControl.Builder() // + .maxAge(0, TimeUnit.DAYS) // + .build(); + + request = request.newBuilder() // + .cacheControl(cacheControl) // + .build(); + } + + return chain.proceed(request); + }; + } + + private static boolean isNetworkAvailable(final Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); + return activeNetworkInfo != null && activeNetworkInfo.isConnected(); } private String decrypt(String password, Context context) { @@ -53,35 +99,4 @@ class S3ClientFactory { .getInstance(context) // .decrypt(password); } - - private static class LoggingAwareRequestHandler extends RequestHandler2 { - - @Override - public void beforeRequest(Request request) { - Timber.tag("S3Client").d("Sending request (%s) %s", request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), request.toString()); - } - - @Override - public void afterResponse(Request request, Response response) { - Timber.tag("S3Client").d( // - "Response received (%s) with status %s (%s)", // - request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), // - response.getHttpResponse().getStatusText(), // - response.getHttpResponse().getStatusCode()); - } - - @Override - public void afterError(Request request, Response response, Exception e) { - if (response != null) { - Timber.tag("S3Client").e( // - e, // - "Error occurred (%s) with status %s (%s)", // - request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), // - response.getHttpResponse().getStatusText(), // - response.getHttpResponse().getStatusCode()); - } else { - Timber.tag("S3Client").e(e, "Error occurred (%s)", request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano()); - } - } - } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java index a6cfa94c..eb10941e 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepository.java @@ -2,8 +2,6 @@ package org.cryptomator.data.cloud.s3; import android.content.Context; -import com.amazonaws.services.s3.model.AmazonS3Exception; - import org.cryptomator.data.cloud.InterceptingCloudContentRepository; import org.cryptomator.domain.S3Cloud; import org.cryptomator.domain.exception.BackendException; @@ -23,6 +21,8 @@ import java.io.IOException; import java.io.OutputStream; import java.util.List; +import io.minio.errors.ErrorResponseException; + import static org.cryptomator.util.ExceptionUtil.contains; class S3CloudContentRepository extends InterceptingCloudContentRepository { @@ -42,9 +42,9 @@ class S3CloudContentRepository extends InterceptingCloudContentRepository encryptedTmpFile, OutputStream data, ProgressAware progressAware) throws BackendException { try { - cloud.read(file, encryptedTmpFile, data, progressAware); + cloud.read(file, data, progressAware); } catch (IOException e) { throw new FatalBackendException(e); } @@ -175,7 +175,7 @@ class S3CloudContentRepository extends InterceptingCloudContentRepository size) { return new S3File(parent, name, getNodePath(parent, name), size, Optional.empty()); } @@ -33,11 +24,6 @@ class S3CloudNodeFactory { return new S3File(parent, name, getNodePath(parent, name), size, lastModified); } - public static S3Folder folder(S3Folder parent, S3ObjectSummary folder) { - String name = getNameFromKey(folder.getKey()); - return new S3Folder(parent, name, getNodePath(parent, name)); - } - public static S3Folder folder(S3Folder parent, String name) { return new S3Folder(parent, name, getNodePath(parent, name)); } @@ -53,17 +39,9 @@ class S3CloudNodeFactory { public static String getNameFromKey(String key) { String name = key; if (key.endsWith(DELIMITER)) { - name = key.substring(0, key.length() -1); + name = key.substring(0, key.length() - 1); } return name.contains(DELIMITER) ? name.substring(name.lastIndexOf(DELIMITER) + 1) : name; } - public static S3Node from(S3Folder parent, S3ObjectSummary objectSummary) { - if (objectSummary.getKey().endsWith(DELIMITER)) { - return folder(parent, objectSummary); - } else { - return file(parent, objectSummary); - } - } - } diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java index 05a72fea..1a294758 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -2,27 +2,9 @@ package org.cryptomator.data.cloud.s3; import android.content.Context; -import com.amazonaws.event.ProgressListener; -import com.amazonaws.mobileconnectors.s3.transferutility.TransferListener; -import com.amazonaws.mobileconnectors.s3.transferutility.TransferState; -import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility; -import com.amazonaws.mobileconnectors.s3.transferutility.UploadOptions; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.AmazonS3Exception; -import com.amazonaws.services.s3.model.CopyObjectResult; -import com.amazonaws.services.s3.model.DeleteObjectsRequest; -import com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion; -import com.amazonaws.services.s3.model.GetObjectRequest; -import com.amazonaws.services.s3.model.ListObjectsV2Request; -import com.amazonaws.services.s3.model.ListObjectsV2Result; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.Owner; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.services.s3.model.S3Object; -import com.amazonaws.services.s3.model.S3ObjectSummary; -import com.tomclaw.cache.DiskLruCache; - import org.cryptomator.data.util.CopyStream; +import org.cryptomator.data.util.TransferredBytesAwareInputStream; +import org.cryptomator.data.util.TransferredBytesAwareOutputStream; import org.cryptomator.domain.S3Cloud; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException; @@ -30,7 +12,6 @@ import org.cryptomator.domain.exception.FatalBackendException; import org.cryptomator.domain.exception.ForbiddenException; import org.cryptomator.domain.exception.NoSuchBucketException; import org.cryptomator.domain.exception.NoSuchCloudFileException; -import org.cryptomator.domain.exception.UnauthorizedException; import org.cryptomator.domain.exception.authentication.WrongCredentialsException; import org.cryptomator.domain.usecases.ProgressAware; import org.cryptomator.domain.usecases.cloud.DataSource; @@ -38,31 +19,39 @@ import org.cryptomator.domain.usecases.cloud.DownloadState; import org.cryptomator.domain.usecases.cloud.Progress; import org.cryptomator.domain.usecases.cloud.UploadState; import org.cryptomator.util.Optional; -import org.cryptomator.util.SharedPreferencesHandler; -import org.cryptomator.util.concurrent.CompletableFuture; -import org.cryptomator.util.file.LruFileCacheUtil; import java.io.ByteArrayInputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; import java.util.List; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicLong; -import timber.log.Timber; +import io.minio.BucketExistsArgs; +import io.minio.CopyObjectArgs; +import io.minio.CopySource; +import io.minio.GetObjectArgs; +import io.minio.GetObjectResponse; +import io.minio.ListObjectsArgs; +import io.minio.MinioClient; +import io.minio.ObjectWriteResponse; +import io.minio.PutObjectArgs; +import io.minio.RemoveObjectArgs; +import io.minio.RemoveObjectsArgs; +import io.minio.Result; +import io.minio.StatObjectArgs; +import io.minio.StatObjectResponse; +import io.minio.errors.ErrorResponseException; +import io.minio.messages.DeleteError; +import io.minio.messages.DeleteObject; +import io.minio.messages.Item; import static org.cryptomator.domain.usecases.cloud.Progress.progress; -import static org.cryptomator.util.file.LruFileCacheUtil.Cache.S3; -import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache; -import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache; class S3Impl { - private static final long CHUNKED_UPLOAD_MAX_SIZE = 100L << 20; private static final String DELIMITER = "/"; private final S3ClientFactory clientFactory = new S3ClientFactory(); @@ -70,9 +59,6 @@ class S3Impl { private final RootS3Folder root; private final Context context; - private final SharedPreferencesHandler sharedPreferencesHandler; - private DiskLruCache diskLruCache; - S3Impl(Context context, S3Cloud cloud) { if (cloud.accessKey() == null || cloud.secretKey() == null) { throw new WrongCredentialsException(cloud); @@ -81,10 +67,9 @@ class S3Impl { this.context = context; this.cloud = cloud; this.root = new RootS3Folder(cloud); - this.sharedPreferencesHandler = new SharedPreferencesHandler(context); } - private AmazonS3 client() { + private MinioClient client() { return clientFactory.getClient(cloud, context); } @@ -118,31 +103,40 @@ class S3Impl { return S3CloudNodeFactory.folder(parent, name, parent.getKey() + name); } - public boolean exists(S3Node node) { + public boolean exists(S3Node node) throws BackendException { String key = node.getKey(); - - ListObjectsV2Result result = client().listObjectsV2(cloud.s3Bucket(), key); - - return result.getObjectSummaries().size() > 0; + try { + client().statObject(StatObjectArgs.builder().bucket(cloud.s3Bucket()).object(key).build()); + } catch (ErrorResponseException e) { + if (e.errorResponse().code().equals("NoSuchKey")) { + return false; + } + throw new FatalBackendException(e); + } catch (Exception ex) { + handleApiError(ex, node.getPath()); + } + return true; } public List list(S3Folder folder) throws IOException, BackendException { List result = new ArrayList<>(); - ListObjectsV2Request request = new ListObjectsV2Request().withBucketName(cloud.s3Bucket()).withPrefix(folder.getKey()).withDelimiter(DELIMITER); - - ListObjectsV2Result listObjects = client().listObjectsV2(request); - for (String prefix : listObjects.getCommonPrefixes()) { - // add folders - result.add(S3CloudNodeFactory.folder(folder, S3CloudNodeFactory.getNameFromKey(prefix))); - } - - for (S3ObjectSummary objectSummary : listObjects.getObjectSummaries()) { - // add files but skip parent folder - if (!objectSummary.getKey().equals(listObjects.getPrefix())) { - result.add(S3CloudNodeFactory.file(folder, objectSummary)); + ListObjectsArgs request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(folder.getKey()).delimiter(DELIMITER).build(); + Iterable> listObjects = client().listObjects(request); + for (Result object : listObjects) { + try { + Item item = object.get(); + if (item.isDir()) { + result.add(S3CloudNodeFactory.folder(folder, S3CloudNodeFactory.getNameFromKey(item.objectName()))); + } else { + S3File file = S3CloudNodeFactory.file(folder, S3CloudNodeFactory.getNameFromKey(item.objectName()), Optional.of(item.size()), Optional.of(Date.from(item.lastModified().toInstant()))); + result.add(file); + } + } catch (Exception ex) { + handleApiError(ex, folder.getPath()); } } + return result; } @@ -154,16 +148,17 @@ class S3Impl { ); } - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(0); - - InputStream emptyContent = new ByteArrayInputStream(new byte[0]); - try { - PutObjectRequest putObjectRequest = new PutObjectRequest(cloud.s3Bucket(), folder.getKey(), emptyContent, metadata); - client().putObject(putObjectRequest); - } catch(AmazonS3Exception ex) { - handleApiError(ex, folder.getName()); + PutObjectArgs putObjectArgs = PutObjectArgs // + .builder() // + .bucket(cloud.s3Bucket()) // + .object(folder.getKey()) // + .stream(new ByteArrayInputStream(new byte[0]), 0, -1) // + .build(); + + client().putObject(putObjectArgs); + } catch (Exception ex) { + handleApiError(ex, folder.getPath()); } return S3CloudNodeFactory.folder(folder.getParent(), folder.getName()); @@ -175,28 +170,58 @@ class S3Impl { } if (source instanceof S3Folder) { - ListObjectsV2Result listObjects = client().listObjectsV2(cloud.s3Bucket(), source.getPath()); + List nodes = list((S3Folder) source); - if (listObjects.getObjectSummaries().size() > 0) { + List objectsToDelete = new LinkedList<>(); - List objectsToDelete = new ArrayList<>(); + for (S3Node node : nodes) { + objectsToDelete.add(new DeleteObject(node.getKey())); - for (S3ObjectSummary summary : listObjects.getObjectSummaries()) { - objectsToDelete.add(new DeleteObjectsRequest.KeyVersion(summary.getKey())); - String destinationKey = summary.getKey().replace(source.getPath(), target.getPath()); - - client().copyObject(cloud.s3Bucket(), summary.getKey(), cloud.s3Bucket(), destinationKey); + String targetKey; + if (node instanceof S3Folder) { + targetKey = S3CloudNodeFactory.folder((S3Folder) target, node.getName()).getKey(); + } else { + targetKey = S3CloudNodeFactory.file((S3Folder) target, node.getName()).getKey(); + } + + CopySource copySource = CopySource.builder().bucket(cloud.s3Bucket()).object(node.getPath()).build(); + + CopyObjectArgs copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).object(targetKey).source(copySource).build(); + try { + client().copyObject(copyObjectArgs); + } catch (Exception ex) { + handleApiError(ex, source.getPath()); } - client().deleteObjects(new DeleteObjectsRequest(cloud.s3Bucket()).withKeys(objectsToDelete)); - } else { - throw new NoSuchCloudFileException(source.getPath()); } + + RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(cloud.s3Bucket()).objects(objectsToDelete).build(); + + for (Result result : client().removeObjects(removeObjectsArgs)) { + try { + result.get(); + } catch (Exception ex) { + handleApiError(ex, source.getPath()); + } + } + return S3CloudNodeFactory.folder(target.getParent(), target.getName()); } else { - CopyObjectResult result = client().copyObject(cloud.s3Bucket(), source.getPath(), cloud.s3Bucket(), target.getPath()); - client().deleteObject(cloud.s3Bucket(), source.getPath()); - return S3CloudNodeFactory.file(target.getParent(), target.getName(), ((S3File) source).getSize(), Optional.of(result.getLastModifiedDate())); + CopySource copySource = CopySource.builder().bucket(cloud.s3Bucket()).object(source.getPath()).build(); + CopyObjectArgs copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).object(target.getPath()).source(copySource).build(); + try { + ObjectWriteResponse result = client().copyObject(copyObjectArgs); + + delete(source); + + Date lastModified = result.headers().getDate("Last-Modified"); + + return S3CloudNodeFactory.file(target.getParent(), target.getName(), ((S3File) source).getSize(), Optional.ofNullable(lastModified)); + } catch (Exception ex) { + handleApiError(ex, source.getPath()); + } } + + throw new IllegalStateException(); } public S3File write(S3File file, DataSource data, final ProgressAware progressAware, boolean replace, long size) throws IOException, BackendException { @@ -206,212 +231,157 @@ class S3Impl { progressAware.onProgress(Progress.started(UploadState.upload(file))); - final CompletableFuture> result = new CompletableFuture<>(); - - try { - if (size <= CHUNKED_UPLOAD_MAX_SIZE) { - uploadFile(file, data, progressAware, result, size); - } else { - uploadChunkedFile(file, data, progressAware, result, size); - } - } catch(AmazonS3Exception ex) { - handleApiError(ex, file.getName()); - } - - try { - Optional objectMetadataOptional = result.get(); - ObjectMetadata objectMetadata = objectMetadataOptional.orElseGet(() -> client().getObjectMetadata(cloud.s3Bucket(), file.getPath())); - progressAware.onProgress(Progress.completed(UploadState.upload(file))); - return S3CloudNodeFactory.file(file.getParent(), file.getName(), objectMetadata); - } catch (ExecutionException | InterruptedException e) { - throw new FatalBackendException(e); - } - - } - - private void uploadFile(final S3File file, DataSource data, final ProgressAware progressAware, CompletableFuture> result, final long size) // - throws IOException { - AtomicLong bytesTransferred = new AtomicLong(0); - ProgressListener listener = progressEvent -> { - bytesTransferred.set(bytesTransferred.get() + progressEvent.getBytesTransferred()); - progressAware.onProgress( // - progress(UploadState.upload(file)) // - .between(0) // - .and(size) // - .withValue(bytesTransferred.get())); - }; - - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(data.size(context).get()); - - PutObjectRequest request = new PutObjectRequest(cloud.s3Bucket(), file.getPath(), data.open(context), metadata); - request.setGeneralProgressListener(listener); - - result.complete(Optional.of(client().putObject(request).getMetadata())); - } - - private void uploadChunkedFile(final S3File file, DataSource data, final ProgressAware progressAware, CompletableFuture> result, final long size) // - throws IOException { - - TransferUtility tu = TransferUtility // - .builder() // - .s3Client(client()) // - .context(context) // - .defaultBucket(cloud.s3Bucket()) // - .build(); - - TransferListener transferListener = new TransferListener() { + try (TransferredBytesAwareDataSource out = new TransferredBytesAwareDataSource(data) { @Override - public void onStateChanged(int id, TransferState state) { - if (state.equals(TransferState.COMPLETED)) { - progressAware.onProgress(Progress.completed(UploadState.upload(file))); - result.complete(Optional.empty()); - } - } - - @Override - public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) { + public void bytesTransferred(long transferred) { progressAware.onProgress( // progress(UploadState.upload(file)) // .between(0) // .and(size) // - .withValue(bytesCurrent)); + .withValue(transferred)); } - - @Override - public void onError(int id, Exception ex) { - result.fail(ex); + }) { + try { + PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(cloud.s3Bucket()).object(file.getKey()).stream(out.open(context), data.size(context).get(), -1).build(); + client().putObject(putObjectArgs); + StatObjectResponse statObjectResponse = client().statObject(StatObjectArgs // + .builder() // + .bucket(cloud.s3Bucket()) // + .object(file.getPath()) // + .build()); + progressAware.onProgress(Progress.completed(UploadState.upload(file))); + return S3CloudNodeFactory.file(file.getParent(), file.getName(), Optional.of(statObjectResponse.size()), Optional.of(Date.from(statObjectResponse.lastModified().toInstant()))); + } catch (Exception ex) { + handleApiError(ex, file.getPath()); } - }; - - UploadOptions uploadOptions = UploadOptions.builder().transferListener(transferListener).build(); - - tu.upload(file.getPath(), data.open(context), uploadOptions); - } - - public void read(S3File file, Optional encryptedTmpFile, OutputStream data, final ProgressAware progressAware) throws IOException, BackendException { - progressAware.onProgress(Progress.started(DownloadState.download(file))); - - Optional cacheKey = Optional.empty(); - Optional cacheFile = Optional.empty(); - - ListObjectsV2Result listObjects; - - if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) { - listObjects = client().listObjectsV2(cloud.s3Bucket(), file.getKey()); - if (listObjects.getObjectSummaries().size() != 1) { - throw new NoSuchCloudFileException(file.getKey()); - } - S3ObjectSummary summary = listObjects.getObjectSummaries().get(0); - cacheKey = Optional.of(summary.getKey() + summary.getETag()); - - File cachedFile = diskLruCache.get(cacheKey.get()); - cacheFile = cachedFile != null ? Optional.of(cachedFile) : Optional.empty(); } - if (sharedPreferencesHandler.useLruCache() && cacheFile.isPresent()) { - try { - retrieveFromLruCache(cacheFile.get(), data); - } catch (IOException e) { - Timber.tag("S3Impl").w(e, "Error while retrieving content from Cache, get from web request"); - writeToData(file, data, encryptedTmpFile, cacheKey, progressAware); - } - } else { - writeToData(file, data, encryptedTmpFile, cacheKey, progressAware); + throw new IllegalStateException(); + } + + public void read(S3File file, OutputStream data, final ProgressAware progressAware) throws IOException, BackendException { + progressAware.onProgress(Progress.started(DownloadState.download(file))); + + GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(cloud.s3Bucket()).object(file.getPath()).build(); + + try (GetObjectResponse response = client().getObject(getObjectArgs); // + TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) { + @Override + public void bytesTransferred(long transferred) { + progressAware.onProgress( // + progress(DownloadState.download(file)) // + .between(0) // + .and(file.getSize().orElse(Long.MAX_VALUE)) // + .withValue(transferred)); + } + }) { + CopyStream.copyStreamToStream(response, out); + } catch (Exception ex) { + handleApiError(ex, file.getPath()); } progressAware.onProgress(Progress.completed(DownloadState.download(file))); } - private void writeToData(final S3File file, // - final OutputStream data, // - final Optional encryptedTmpFile, // - final Optional cacheKey, // - final ProgressAware progressAware) throws IOException, BackendException { - AtomicLong bytesTransferred = new AtomicLong(0); - ProgressListener listener = progressEvent -> { - bytesTransferred.set(bytesTransferred.get() + progressEvent.getBytesTransferred()); - - progressAware.onProgress( // - progress(DownloadState.download(file)) // - .between(0) // - .and(file.getSize().orElse(Long.MAX_VALUE)) // - .withValue(bytesTransferred.get())); - }; - - GetObjectRequest request = new GetObjectRequest(cloud.s3Bucket(), file.getPath()); - request.setGeneralProgressListener(listener); - - try { - S3Object s3Object = client().getObject(request); - - CopyStream.copyStreamToStream(s3Object.getObjectContent(), data); - - if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) { - try { - storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get()); - } catch (IOException e) { - Timber.tag("S3Impl").e(e, "Failed to write downloaded file in LRU cache"); - } - } - } catch(AmazonS3Exception ex) { - handleApiError(ex, file.getName()); - } - - } - public void delete(S3Node node) throws IOException, BackendException { if (node instanceof S3Folder) { - List summaries = client().listObjectsV2(cloud.s3Bucket(), node.getPath()).getObjectSummaries(); - List keys = new ArrayList<>(); - for (S3ObjectSummary summary : summaries) { - keys.add(new KeyVersion(summary.getKey())); + List objectsToDelete = new LinkedList<>(); + + ListObjectsArgs request = ListObjectsArgs.builder().bucket(cloud.s3Bucket()).prefix(node.getKey()).recursive(true).delimiter(DELIMITER).build(); + Iterable> listObjects = client().listObjects(request); + for (Result object : listObjects) { + try { + Item item = object.get(); + objectsToDelete.add(new DeleteObject(item.objectName())); + } catch (Exception e) { + handleApiError(e, node.getPath()); + } } - DeleteObjectsRequest request = new DeleteObjectsRequest(cloud.s3Bucket()); - request.withKeys(keys); + RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(cloud.s3Bucket()).objects(objectsToDelete).build(); + Iterable> results = client().removeObjects(removeObjectsArgs); + for (Result result : results) { + try { + DeleteError error = result.get(); + System.out.println("Error in deleting object " + error.objectName() + "; " + error.message()); + } catch (Exception e) { + handleApiError(e, node.getPath()); + } + } - client().deleteObjects(request); } else { - client().deleteObject(cloud.s3Bucket(), node.getPath()); - } - } - - public String checkAuthenticationAndRetrieveCurrentAccount() throws NoSuchBucketException { - if (!client().doesBucketExist(cloud.s3Bucket())) { - throw new NoSuchBucketException(cloud.s3Bucket()); - } - - Owner currentAccount = client() // - .getS3AccountOwner(); - - return currentAccount.getDisplayName(); - } - - private boolean createLruCache(int cacheSize) { - if (diskLruCache == null) { + RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(cloud.s3Bucket()).object(node.getKey()).build(); try { - diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(S3), cacheSize); - } catch (IOException e) { - Timber.tag("S3Impl").e(e, "Failed to setup LRU cache"); - return false; + client().removeObject(removeObjectArgs); + } catch (Exception e) { + handleApiError(e, ""); } } - - return true; } - private void handleApiError(AmazonS3Exception ex, String name) throws BackendException { - String errorCode = ex.getErrorCode(); - if (S3CloudApiExceptions.isAccessProblem(errorCode)) { - throw new ForbiddenException(); - } else if (S3CloudApiErrorCodes.NO_SUCH_BUCKET.getValue().equals(errorCode)) { - throw new NoSuchBucketException(name); - } else if (S3CloudApiErrorCodes.NO_SUCH_KEY.getValue().equals(errorCode)) { - throw new NoSuchCloudFileException(name); - } else { + public String checkAuthentication() throws NoSuchBucketException, BackendException { + try { + if (!client().bucketExists(BucketExistsArgs.builder().bucket(cloud.s3Bucket()).build())) { + throw new NoSuchBucketException(cloud.s3Bucket()); + } + } catch (Exception e) { + handleApiError(e, ""); + } + + return ""; + } + + private void handleApiError(Exception ex, String name) throws BackendException { + if (ex instanceof ErrorResponseException) { + String errorCode = ((ErrorResponseException) ex).errorResponse().code(); + if (S3CloudApiExceptions.isAccessProblem(errorCode)) { + throw new ForbiddenException(); + } else if (S3CloudApiErrorCodes.NO_SUCH_BUCKET.getValue().equals(errorCode)) { + throw new NoSuchBucketException(name); + } else if (S3CloudApiErrorCodes.NO_SUCH_KEY.getValue().equals(errorCode)) { + throw new NoSuchCloudFileException(name); + } else { + throw new FatalBackendException(ex); + } + } else { throw new FatalBackendException(ex); } } + + private static abstract class TransferredBytesAwareDataSource implements DataSource { + + private final DataSource data; + + TransferredBytesAwareDataSource(DataSource data) { + this.data = data; + } + + @Override + public Optional size(Context context) { + return data.size(context); + } + + @Override + public InputStream open(Context context) throws IOException { + return new TransferredBytesAwareInputStream(data.open(context)) { + @Override + public void bytesTransferred(long transferred) { + S3Impl.TransferredBytesAwareDataSource.this.bytesTransferred(transferred); + } + }; + } + + @Override + public void close() throws IOException { + data.close(); + } + + public abstract void bytesTransferred(long transferred); + + @Override + public DataSource decorate(DataSource delegate) { + return delegate; + } + } } diff --git a/presentation/src/main/res/xml/licenses.xml b/presentation/src/main/res/xml/licenses.xml index 7f8fdfa3..a3f346e3 100644 --- a/presentation/src/main/res/xml/licenses.xml +++ b/presentation/src/main/res/xml/licenses.xml @@ -99,6 +99,13 @@ android:action="android.intent.action.VIEW" android:data="https://github.com/microsoftgraph/msgraph-sdk-android" />
+ + + @@ -134,13 +141,6 @@ android:action="android.intent.action.VIEW" android:data="https://github.com/ReactiveX/RxJava" /> - - - From bf2341af210089e4f4d4345f97ee6d14432481d7 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 11 May 2021 17:04:07 +0200 Subject: [PATCH 033/135] Use path and path prefix correctly in S3 --- .../cryptomator/data/cloud/s3/S3CloudNodeFactory.java | 2 +- .../java/org/cryptomator/data/cloud/s3/S3File.java | 5 +++++ .../java/org/cryptomator/data/cloud/s3/S3Folder.java | 3 +++ .../java/org/cryptomator/data/cloud/s3/S3Impl.java | 10 +++++----- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java index c14fabaf..12d8f712 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudNodeFactory.java @@ -33,7 +33,7 @@ class S3CloudNodeFactory { } private static String getNodePath(S3Folder parent, String name) { - return parent.getKey() + name; + return parent.getPath() + "/" + name; } public static String getNameFromKey(String key) { diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java index ce3520f8..99b45ea9 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java @@ -8,6 +8,8 @@ import java.util.Date; class S3File implements CloudFile, S3Node { + private static final String DELIMITER = "/"; + private final S3Folder parent; private final String name; private final String path; @@ -39,6 +41,9 @@ class S3File implements CloudFile, S3Node { @Override public String getKey() { + if (path.startsWith(DELIMITER)) { + return path.substring(DELIMITER.length()); + } return path; } diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.java index 591c6dea..bdfd8fc8 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Folder.java @@ -34,6 +34,9 @@ class S3Folder implements CloudFolder, S3Node { @Override public String getKey() { + if (path.startsWith(DELIMITER)) { + return path.substring(DELIMITER.length()) + DELIMITER; + } return path + DELIMITER; } diff --git a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java index 1a294758..ff402ac7 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java +++ b/data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java @@ -184,7 +184,7 @@ class S3Impl { targetKey = S3CloudNodeFactory.file((S3Folder) target, node.getName()).getKey(); } - CopySource copySource = CopySource.builder().bucket(cloud.s3Bucket()).object(node.getPath()).build(); + CopySource copySource = CopySource.builder().bucket(cloud.s3Bucket()).object(node.getKey()).build(); CopyObjectArgs copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).object(targetKey).source(copySource).build(); try { @@ -206,8 +206,8 @@ class S3Impl { return S3CloudNodeFactory.folder(target.getParent(), target.getName()); } else { - CopySource copySource = CopySource.builder().bucket(cloud.s3Bucket()).object(source.getPath()).build(); - CopyObjectArgs copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).object(target.getPath()).source(copySource).build(); + CopySource copySource = CopySource.builder().bucket(cloud.s3Bucket()).object(source.getKey()).build(); + CopyObjectArgs copyObjectArgs = CopyObjectArgs.builder().bucket(cloud.s3Bucket()).object(target.getKey()).source(copySource).build(); try { ObjectWriteResponse result = client().copyObject(copyObjectArgs); @@ -247,7 +247,7 @@ class S3Impl { StatObjectResponse statObjectResponse = client().statObject(StatObjectArgs // .builder() // .bucket(cloud.s3Bucket()) // - .object(file.getPath()) // + .object(file.getKey()) // .build()); progressAware.onProgress(Progress.completed(UploadState.upload(file))); return S3CloudNodeFactory.file(file.getParent(), file.getName(), Optional.of(statObjectResponse.size()), Optional.of(Date.from(statObjectResponse.lastModified().toInstant()))); @@ -262,7 +262,7 @@ class S3Impl { public void read(S3File file, OutputStream data, final ProgressAware progressAware) throws IOException, BackendException { progressAware.onProgress(Progress.started(DownloadState.download(file))); - GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(cloud.s3Bucket()).object(file.getPath()).build(); + GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(cloud.s3Bucket()).object(file.getKey()).build(); try (GetObjectResponse response = client().getObject(getObjectArgs); // TransferredBytesAwareOutputStream out = new TransferredBytesAwareOutputStream(data) { From 318a7422f42f4a7dae27a20de8b7c2a5bd203ed2 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 11 May 2021 17:32:22 +0200 Subject: [PATCH 034/135] Drop S3 clouds and vaults from the database This is necessary because the path/region handling has changed with the new dependency in the s3 cloud --- data/build.gradle | 2 +- .../cryptomator/data/db/DatabaseUpgrades.java | 6 ++-- .../java/org/cryptomator/data/db/Sql.java | 25 +++++++-------- .../org/cryptomator/data/db/Upgrade7To8.kt | 32 +++++++++++++++++++ 4 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 data/src/main/java/org/cryptomator/data/db/Upgrade7To8.kt diff --git a/data/build.gradle b/data/build.gradle index 2d423315..4d884510 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -78,7 +78,7 @@ android { } greendao { - schemaVersion 7 + schemaVersion 8 } configurations.all { diff --git a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java index 59fef0f2..dc1c1d70 100644 --- a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java +++ b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java @@ -25,7 +25,8 @@ class DatabaseUpgrades { Upgrade3To4 upgrade3To4, // Upgrade4To5 upgrade4To5, // Upgrade5To6 upgrade5To6, // - Upgrade6To7 upgrade6To7) { + Upgrade6To7 upgrade6To7, // + Upgrade7To8 upgrade7To8) { availableUpgrades = defineUpgrades( // upgrade0To1, // @@ -34,7 +35,8 @@ class DatabaseUpgrades { upgrade3To4, // upgrade4To5, // upgrade5To6, // - upgrade6To7); + upgrade6To7, // + upgrade7To8); } private static Comparator reverseOrder() { diff --git a/data/src/main/java/org/cryptomator/data/db/Sql.java b/data/src/main/java/org/cryptomator/data/db/Sql.java index 1fc488a4..e5d71208 100644 --- a/data/src/main/java/org/cryptomator/data/db/Sql.java +++ b/data/src/main/java/org/cryptomator/data/db/Sql.java @@ -498,27 +498,26 @@ class Sql { public static class SqlDeleteBuilder { - private final String table; - private String whereClause; - private String[] whereArgs; + private final String tableName; - public SqlDeleteBuilder(String table) { - this.table = table; + private final StringBuilder whereClause = new StringBuilder(); + private final List whereArgs = new ArrayList<>(); + + public SqlDeleteBuilder(String tableName) { + this.tableName = tableName; } - public SqlDeleteBuilder whereClause(String whereClause) { - this.whereClause = whereClause; - return this; - } - - public SqlDeleteBuilder whereArgs(String[] whereArgs) { - this.whereArgs = whereArgs; + public SqlDeleteBuilder where(String column, Criterion criterion) { + if (whereClause.length() > 0) { + whereClause.append(" AND "); + } + criterion.appendTo(column, whereClause, whereArgs); return this; } public void executeOn(Database wrapped) { SQLiteDatabase db = unwrap(wrapped); - db.delete(table, whereClause, whereArgs); + db.delete(tableName, whereClause.toString(), whereArgs.toArray(new String[whereArgs.size()])); } } diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade7To8.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade7To8.kt new file mode 100644 index 00000000..5f1a65cc --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade7To8.kt @@ -0,0 +1,32 @@ +package org.cryptomator.data.db + +import org.greenrobot.greendao.database.Database +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class Upgrade7To8 @Inject constructor() : DatabaseUpgrade(7, 8) { + + override fun internalApplyTo(db: Database, origin: Int) { + db.beginTransaction() + try { + dropS3Vaults(db) + dropS3Clouds(db) + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private fun dropS3Vaults(db: Database) { + Sql.deleteFrom("VAULT_ENTITY") // + .where("CLOUD_TYPE", Sql.eq("S3")) + .executeOn(db) + } + + private fun dropS3Clouds(db: Database) { + Sql.deleteFrom("CLOUD_ENTITY") // + .where("TYPE", Sql.eq("S3")) + .executeOn(db) + } +} From ed3401836e73eabcf2109dc9798826612cbf619e Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 11 May 2021 17:56:56 +0200 Subject: [PATCH 035/135] Using the new S3 dependency, region must be set when endpoint is set --- .../presenter/S3AddOrChangePresenter.kt | 4 +++ .../ui/fragment/S3AddOrChangeFragment.kt | 27 +++++++++-------- .../src/main/res/layout/fragment_setup_s3.xml | 29 +++++++++++++++++-- presentation/src/main/res/values/strings.xml | 1 + 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/S3AddOrChangePresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/S3AddOrChangePresenter.kt index 20fe2282..fc94ce3f 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/S3AddOrChangePresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/S3AddOrChangePresenter.kt @@ -39,6 +39,10 @@ class S3AddOrChangePresenter @Inject internal constructor( // statusMessage = getString(R.string.screen_s3_settings_msg_endpoint_and_region_not_empty) } + if (!endpoint.isNullOrEmpty() && region.isNullOrEmpty()) { + statusMessage = getString(R.string.screen_s3_settings_msg_endpoint_set_and_region_empty) + } + if (statusMessage != null) { Toast.makeText(context(), statusMessage, Toast.LENGTH_SHORT).show() } else { diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt index d80feb76..8438ce78 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt @@ -1,6 +1,7 @@ package org.cryptomator.presentation.ui.fragment import android.os.Bundle +import android.view.View import android.view.inputmethod.EditorInfo import com.google.android.material.switchmaterial.SwitchMaterial import org.cryptomator.generator.Fragment @@ -13,8 +14,9 @@ import kotlinx.android.synthetic.main.fragment_setup_s3.accessKeyEditText import kotlinx.android.synthetic.main.fragment_setup_s3.bucketEditText import kotlinx.android.synthetic.main.fragment_setup_s3.createCloudButton import kotlinx.android.synthetic.main.fragment_setup_s3.displayNameEditText -import kotlinx.android.synthetic.main.fragment_setup_s3.regionOrEndpointEditText -import kotlinx.android.synthetic.main.fragment_setup_s3.regionOrEndpointEditTextLayout +import kotlinx.android.synthetic.main.fragment_setup_s3.endpointEditText +import kotlinx.android.synthetic.main.fragment_setup_s3.ll_custom_s3 +import kotlinx.android.synthetic.main.fragment_setup_s3.regionEditText import kotlinx.android.synthetic.main.fragment_setup_s3.secretKeyEditText import kotlinx.android.synthetic.main.fragment_setup_s3.toggleCustomS3 import timber.log.Timber @@ -42,15 +44,14 @@ class S3AddOrChangeFragment : BaseFragment() { showEditableCloudContent(s3CloudModel) toggleCustomS3.setOnClickListener { switch -> - regionOrEndpointEditText.text?.clear() toggleUseAmazonS3((switch as SwitchMaterial).isChecked) } } private fun toggleUseAmazonS3(checked: Boolean) = if (checked) { - regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_region_label) + ll_custom_s3.visibility = View.GONE } else { - regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_endpoint_label) + ll_custom_s3.visibility = View.VISIBLE } private fun showEditableCloudContent(s3CloudModel: S3CloudModel?) { @@ -61,15 +62,17 @@ class S3AddOrChangeFragment : BaseFragment() { secretKeyEditText.setText(decrypt(s3CloudModel.secretKey())) bucketEditText.setText(s3CloudModel.s3Bucket()) + regionEditText.setText(s3CloudModel.s3Region()) + if (it.s3Endpoint().isNotEmpty()) { toggleCustomS3.isChecked = false - regionOrEndpointEditText.setText(s3CloudModel.s3Endpoint()) - regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_endpoint_label) + ll_custom_s3.visibility = View.VISIBLE + endpointEditText.setText(s3CloudModel.s3Endpoint()) } else { - regionOrEndpointEditText.setText(s3CloudModel.s3Region()) - regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_region_label) + toggleCustomS3.isChecked = false + ll_custom_s3.visibility = View.VISIBLE } - } ?: regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_region_label) + } } private fun decrypt(text: String?): String { @@ -92,9 +95,9 @@ class S3AddOrChangeFragment : BaseFragment() { val displayName = displayNameEditText.text.toString().trim() if (toggleCustomS3.isChecked) { - s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, null, regionOrEndpointEditText.text.toString().trim(), cloudId, displayName) + s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, null, regionEditText.text.toString().trim(), cloudId, displayName) } else { - s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, regionOrEndpointEditText.text.toString().trim(), null, cloudId, displayName) + s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, endpointEditText.text.toString().trim(), regionEditText.text.toString().trim(), cloudId, displayName) } } diff --git a/presentation/src/main/res/layout/fragment_setup_s3.xml b/presentation/src/main/res/layout/fragment_setup_s3.xml index deeb56e9..15e6c1f2 100644 --- a/presentation/src/main/res/layout/fragment_setup_s3.xml +++ b/presentation/src/main/res/layout/fragment_setup_s3.xml @@ -79,14 +79,14 @@ @@ -101,6 +101,31 @@ android:checked="true" android:text="@string/screen_s3_settings_amazon_s3_text" /> + + + + + + + + + +