From 21128ee23d8d6a7330490e92ae00faaf222d84f8 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 26 Nov 2024 15:48:37 +0100 Subject: [PATCH 1/4] Initial Hub support --- buildsystem/dependencies.gradle | 8 + data/build.gradle | 2 + .../data/cloud/crypto/CryptoCloudFactory.java | 17 +- .../cloud/crypto/CryptoCloudProvider.java | 2 + .../data/cloud/crypto/CryptoConstants.kt | 2 + .../cloud/crypto/HubkeyCryptoCloudProvider.kt | 99 ++++++++ .../crypto/MasterkeyCryptoCloudProvider.kt | 4 + .../data/cloud/crypto/VaultConfig.kt | 15 +- .../data/repository/CloudRepositoryImpl.java | 7 + .../data/repository/HubRepositoryImpl.java | 197 ++++++++++++++++ .../data/repository/RepositoryModule.java | 7 + .../domain/UnverifiedHubVaultConfig.kt | 23 ++ .../domain/UnverifiedVaultConfig.kt | 2 +- .../hub/HubAuthenticationFailedException.java | 15 ++ ...lreadyRegisteredForOtherUserException.java | 11 + .../hub/HubDeviceSetupRequiredException.java | 11 + .../hub/HubInvalidSetupCodeException.java | 11 + .../hub/HubInvalidVersionException.java | 11 + .../HubLicenseUpgradeRequiredException.java | 11 + .../hub/HubUserSetupRequiredException.java | 11 + .../hub/HubVaultAccessForbiddenException.java | 11 + .../hub/HubVaultIsArchivedException.java | 11 + ...ubVaultOperationNotSupportedException.java | 11 + .../domain/repository/CloudRepository.java | 2 + .../domain/repository/HubRepository.kt | 29 +++ .../usecases/vault/CreateHubDevice.java | 32 +++ .../domain/usecases/vault/UnlockHubVault.java | 53 +++++ presentation/build.gradle | 6 + .../di/component/ApplicationComponent.java | 3 + .../exception/ExceptionHandlers.kt | 10 + .../presentation/model/ProgressStateModel.kt | 1 + .../presenter/UnlockVaultPresenter.kt | 156 ++++++++++++- .../ui/activity/UnlockVaultActivity.kt | 61 ++++- .../ui/activity/view/UnlockVaultView.kt | 6 + .../ui/dialog/CreateHubDeviceDialog.kt | 118 ++++++++++ .../dialog/HubLicenseUpgradeRequiredDialog.kt | 43 ++++ .../ui/dialog/HubUserSetupRequiredDialog.kt | 66 ++++++ .../dialog/HubVaultAccessForbiddenDialog.kt | 42 ++++ .../ui/dialog/HubVaultArchivedDialog.kt | 46 ++++ .../res/layout/dialog_create_hub_device.xml | 77 ++++++ .../dialog_hub_license_upgrade_required.xml | 19 ++ .../layout/dialog_hub_user_setup_required.xml | 19 ++ .../dialog_hub_vault_access_forbidden.xml | 19 ++ .../res/layout/dialog_hub_vault_archived.xml | 19 ++ presentation/src/main/res/values/strings.xml | 33 +++ util/build.gradle | 4 + .../util/crypto/HubDeviceCryptorTest.java | 191 +++++++++++++++ .../util/crypto/HubDeviceCryptor.java | 219 ++++++++++++++++++ 48 files changed, 1766 insertions(+), 7 deletions(-) create mode 100644 data/src/main/java/org/cryptomator/data/cloud/crypto/HubkeyCryptoCloudProvider.kt create mode 100644 data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java create mode 100644 domain/src/main/java/org/cryptomator/domain/UnverifiedHubVaultConfig.kt create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/hub/HubAuthenticationFailedException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/hub/HubDeviceAlreadyRegisteredForOtherUserException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/hub/HubDeviceSetupRequiredException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/hub/HubInvalidSetupCodeException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/hub/HubInvalidVersionException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/hub/HubLicenseUpgradeRequiredException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/hub/HubUserSetupRequiredException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultAccessForbiddenException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultIsArchivedException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultOperationNotSupportedException.java create mode 100644 domain/src/main/java/org/cryptomator/domain/repository/HubRepository.kt create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/vault/CreateHubDevice.java create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockHubVault.java create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateHubDeviceDialog.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubLicenseUpgradeRequiredDialog.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubUserSetupRequiredDialog.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubVaultAccessForbiddenDialog.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubVaultArchivedDialog.kt create mode 100644 presentation/src/main/res/layout/dialog_create_hub_device.xml create mode 100644 presentation/src/main/res/layout/dialog_hub_license_upgrade_required.xml create mode 100644 presentation/src/main/res/layout/dialog_hub_user_setup_required.xml create mode 100644 presentation/src/main/res/layout/dialog_hub_vault_access_forbidden.xml create mode 100644 presentation/src/main/res/layout/dialog_hub_vault_archived.xml create mode 100644 util/src/androidTest/java/org/cryptomator/util/crypto/HubDeviceCryptorTest.java create mode 100644 util/src/main/java/org/cryptomator/util/crypto/HubDeviceCryptor.java diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 387a69409..87df6458c 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -64,6 +64,10 @@ ext { gsonVersion = '2.11.0' + joseJwtVersion = '9.47' + + appauthVersion = '0.11.1' + okHttpVersion = '4.12.0' okHttpDigestVersion = '3.1.0' @@ -107,6 +111,7 @@ ext { mockitoVersion = '5.12.0' mockitoKotlinVersion = '5.3.1' mockitoInlineVersion = '5.2.0' + mockitoAndroidVersion = '5.14.2' hamcrestVersion = '1.3' dexmakerVersion = '1.0' espressoVersion = '3.4.0' @@ -140,6 +145,7 @@ ext { androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}", androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}", androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}", + appauth : "net.openid:appauth:${appauthVersion}", documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}", recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}", androidxSplashscreen : "androidx.core:core-splashscreen:${androidxSplashscreenVersion}", @@ -163,6 +169,7 @@ ext { gson : "com.google.code.gson:gson:${gsonVersion}", hamcrest : "org.hamcrest:hamcrest-all:${hamcrestVersion}", javaxAnnotation : "javax.annotation:jsr250-api:${javaxAnnotationVersion}", + joseJwt : "com.nimbusds:nimbus-jose-jwt:${joseJwtVersion}", junit : "org.junit.jupiter:junit-jupiter:${jUnitVersion}", junitApi : "org.junit.jupiter:junit-jupiter-api:${jUnitVersion}", junitEngine : "org.junit.jupiter:junit-jupiter-engine:${jUnitVersion}", @@ -172,6 +179,7 @@ ext { mockito : "org.mockito:mockito-core:${mockitoVersion}", mockitoInline : "org.mockito:mockito-inline:${mockitoInlineVersion}", mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}", + mockitoAndroid : "org.mockito:mockito-android:${mockitoAndroidVersion}", msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}", msgraphAuth : "com.microsoft.identity.client:msal:${msgraphAuthVersion}", multidex : "androidx.multidex:multidex:${multidexVersion}", diff --git a/data/build.gradle b/data/build.gradle index 4e3014675..f841e558f 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -196,6 +196,8 @@ dependencies { compileOnly dependencies.javaxAnnotation implementation dependencies.gson + implementation dependencies.joseJwt + implementation dependencies.commonsCodec implementation dependencies.documentFile 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 727b292bd..524a785f2 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 @@ -21,6 +21,7 @@ import javax.inject.Inject; import javax.inject.Singleton; +import static org.cryptomator.data.cloud.crypto.CryptoConstants.HUB_SCHEME; import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME; import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME; import static org.cryptomator.domain.Vault.aCopyOf; @@ -40,7 +41,7 @@ public CryptoCloudFactory(CloudContentRepository/* unverifie return cryptoCloudProvider(unverifiedVaultConfig).unlock(token, unverifiedVaultConfig, password, cancelledFlag); } + public Vault unlock(Vault vault, UnverifiedVaultConfig unverifiedVaultConfig, String vaultKeyJwe, String userKeyJwe, Flag cancelledFlag) throws BackendException { + return cryptoCloudProvider(unverifiedVaultConfig).unlock(vault, unverifiedVaultConfig, vaultKeyJwe, userKeyJwe, cancelledFlag); + } + public UnlockToken createUnlockToken(Vault vault, Optional unverifiedVaultConfig) throws BackendException { return cryptoCloudProvider(unverifiedVaultConfig).createUnlockToken(vault, unverifiedVaultConfig); } @@ -84,10 +89,20 @@ public void changePassword(Vault vault, Optional unverifi cryptoCloudProvider(unverifiedVaultConfig).changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword); } + private CryptoCloudProvider masterkeyCryptoCloudProvider() { + return cryptoCloudProvider(Optional.absent()); + } + + private CryptoCloudProvider cryptoCloudProvider(UnverifiedVaultConfig unverifiedVaultConfigOptional) { + return cryptoCloudProvider(Optional.of(unverifiedVaultConfigOptional)); + } + private CryptoCloudProvider cryptoCloudProvider(Optional unverifiedVaultConfigOptional) { if (unverifiedVaultConfigOptional.isPresent()) { if (MASTERKEY_SCHEME.equals(unverifiedVaultConfigOptional.get().getKeyId().getScheme())) { return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom); + } else if (unverifiedVaultConfigOptional.get().getKeyId().getScheme().startsWith(HUB_SCHEME)) { + return new HubkeyCryptoCloudProvider(cryptoCloudContentRepositoryFactory, secureRandom); } throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme())); } else { 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 index 83a92dfca..818ac58e3 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudProvider.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudProvider.java @@ -19,6 +19,8 @@ public interface CryptoCloudProvider { Vault unlock(UnlockToken token, Optional unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException; + Vault unlock(Vault vault, UnverifiedVaultConfig unverifiedVaultConfig, String vaultKeyJwe, String userKeyJwe, Flag cancelledFlag) throws BackendException; + boolean isVaultPasswordValid(Vault vault, Optional unverifiedVaultConfig, CharSequence password) throws BackendException; void lock(Vault vault); diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.kt index 3820bf3a9..8e38270b6 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoConstants.kt @@ -6,6 +6,8 @@ object CryptoConstants { const val MASTERKEY_SCHEME = "masterkeyfile" const val MASTERKEY_FILE_NAME = "masterkey.cryptomator" + const val HUB_SCHEME = "hub+" + const val HUB_REDIRECT_URL = "org.cryptomator.android:/hub/auth" const val ROOT_DIR_ID = "" const val DATA_DIR_NAME = "d" const val VAULT_FILE_NAME = "vault.cryptomator" diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/HubkeyCryptoCloudProvider.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/HubkeyCryptoCloudProvider.kt new file mode 100644 index 000000000..00d91894f --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/HubkeyCryptoCloudProvider.kt @@ -0,0 +1,99 @@ +package org.cryptomator.data.cloud.crypto + +import com.google.common.base.Optional +import com.nimbusds.jose.JWEObject +import org.cryptomator.cryptolib.api.Cryptor +import org.cryptomator.cryptolib.api.CryptorProvider +import org.cryptomator.cryptolib.api.Masterkey +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException +import org.cryptomator.data.cloud.crypto.VaultConfig.Companion.verify +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.usecases.cloud.Flag +import org.cryptomator.domain.usecases.vault.UnlockToken +import org.cryptomator.util.crypto.HubDeviceCryptor +import java.security.SecureRandom + +class HubkeyCryptoCloudProvider( + private val cryptoCloudContentRepositoryFactory: CryptoCloudContentRepositoryFactory, // + private val secureRandom: SecureRandom +) : CryptoCloudProvider { + + @Throws(BackendException::class) + override fun create(location: CloudFolder, password: CharSequence) { + throw IllegalStateException("Hub can not create vaults from within the app") + } + + @Throws(BackendException::class) + override fun unlock(vault: Vault, unverifiedVaultConfig: Optional, password: CharSequence, cancelledFlag: Flag): Vault { + throw IllegalStateException("Hub can not unlock vaults using password") + } + + @Throws(BackendException::class) + override fun unlock(token: UnlockToken, unverifiedVaultConfig: Optional, password: CharSequence, cancelledFlag: Flag): Vault { + throw IllegalStateException("Hub can not unlock vaults using password") + } + + override fun unlock(vault: Vault, unverifiedVaultConfig: UnverifiedVaultConfig, vaultKeyJwe: String, userKeyJwe: String, cancelledFlag: Flag): Vault { + val vaultKey = JWEObject.parse(vaultKeyJwe) + val userKey = JWEObject.parse(userKeyJwe) + val masterkey = HubDeviceCryptor.getInstance().decryptVaultKey(vaultKey, userKey) + val vaultConfig = verify(masterkey.encoded, unverifiedVaultConfig) + val vaultFormat = vaultConfig.vaultFormat + assertVaultVersionIsSupported(vaultConfig.vaultFormat) + val shorteningThreshold = vaultConfig.shorteningThreshold + val cryptor = cryptorFor(masterkey, vaultConfig.cipherCombo) + if (cancelledFlag.get()) { + throw CancellationException() + } + val unlockedVault = Vault.aCopyOf(vault) // + .withUnlocked(true) // + .withFormat(vaultFormat) // + .withShorteningThreshold(shorteningThreshold) // + .build() + cryptoCloudContentRepositoryFactory.registerCryptor(unlockedVault, cryptor) + return unlockedVault + } + + @Throws(BackendException::class) + override fun createUnlockToken(vault: Vault, unverifiedVaultConfig: Optional): UnlockTokenImpl { + throw IllegalStateException("Hub can not unlock vaults using password") + } + + // Visible for testing + fun cryptorFor(keyFile: Masterkey, vaultCipherCombo: CryptorProvider.Scheme): Cryptor { + return CryptorProvider.forScheme(vaultCipherCombo).provide(keyFile, secureRandom) + } + + @Throws(BackendException::class) + override fun isVaultPasswordValid(vault: Vault, unverifiedVaultConfig: Optional, password: CharSequence): Boolean { + throw IllegalStateException("Hub can not unlock vaults using password") + } + + override fun lock(vault: Vault) { + cryptoCloudContentRepositoryFactory.deregisterCryptor(vault) + } + + private fun assertVaultVersionIsSupported(version: Int) { + if (version < CryptoConstants.MIN_VAULT_VERSION) { + throw UnsupportedVaultFormatException(version, CryptoConstants.MIN_VAULT_VERSION) + } else if (version > CryptoConstants.MAX_VAULT_VERSION) { + throw UnsupportedVaultFormatException(version, CryptoConstants.MAX_VAULT_VERSION) + } + } + + @Throws(BackendException::class) + override fun changePassword(vault: Vault, unverifiedVaultConfig: Optional, oldPassword: String, newPassword: String) { + throw IllegalStateException("Hub can not unlock vaults using password") + } + + class UnlockTokenImpl(private val vault: Vault) : UnlockToken { + + override fun getVault(): Vault { + return vault + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.kt index 5699a95b4..c2af14fe8 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/MasterkeyCryptoCloudProvider.kt @@ -126,6 +126,10 @@ class MasterkeyCryptoCloudProvider( } } + override fun unlock(vault: Vault, unverifiedVaultConfig: UnverifiedVaultConfig, vaultKeyJwe: String, userKeyJwe: String, cancelledFlag: Flag): Vault { + throw IllegalStateException("Password based vaults do not support hub unlock") + } + @Throws(BackendException::class) override fun createUnlockToken(vault: Vault, unverifiedVaultConfig: Optional): UnlockTokenImpl { val vaultLocation = vaultLocation(vault) 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 2a324f142..e58091139 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 @@ -7,6 +7,7 @@ import com.auth0.jwt.exceptions.JWTVerificationException import com.auth0.jwt.exceptions.SignatureVerificationException import com.auth0.jwt.interfaces.DecodedJWT import org.cryptomator.cryptolib.api.CryptorProvider +import org.cryptomator.domain.UnverifiedHubVaultConfig import org.cryptomator.domain.UnverifiedVaultConfig import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException @@ -83,7 +84,19 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { val unverifiedJwt = JWT.decode(token) val vaultFormat = unverifiedJwt.getClaim(JSON_KEY_VAULTFORMAT).asInt() val keyId = URI.create(unverifiedJwt.keyId) - return UnverifiedVaultConfig(token, keyId, vaultFormat) + if (keyId.scheme.startsWith(CryptoConstants.HUB_SCHEME)) { + val hubClaim = unverifiedJwt.getHeaderClaim("hub").asMap() + val clientId = hubClaim["clientId"] as String + val authEndpoint = hubClaim["authEndpoint"] as String + val tokenEndpoint = hubClaim["tokenEndpoint"] as String + val authSuccessUrl = hubClaim["authSuccessUrl"] as String + val authErrorUrl = hubClaim["authErrorUrl"] as String + val apiBaseUrl = hubClaim["apiBaseUrl"] as String + val devicesResourceUrl = hubClaim["devicesResourceUrl"] as String + return UnverifiedHubVaultConfig(token, keyId, vaultFormat, clientId, authEndpoint, tokenEndpoint, authSuccessUrl, authErrorUrl, apiBaseUrl, devicesResourceUrl) + } else { + return UnverifiedVaultConfig(token, keyId, vaultFormat) + } } @JvmStatic 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 f08137f2f..bcda1f3a2 100644 --- a/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java @@ -109,6 +109,13 @@ public Cloud unlock(UnlockToken token, Optional unverifie return decryptedViewOf(vaultWithVersion); } + + @Override + public Cloud unlock(Vault vault, UnverifiedVaultConfig unverifiedVaultConfig, String vaultKeyJwe, String userKeyJwe, Flag cancelledFlag) throws BackendException { + Vault vaultWithVersion = cryptoCloudFactory.unlock(vault, unverifiedVaultConfig, vaultKeyJwe, userKeyJwe, cancelledFlag); + return decryptedViewOf(vaultWithVersion); + } + @Override public UnlockToken prepareUnlock(Vault vault, Optional unverifiedVaultConfig) throws BackendException { return cryptoCloudFactory.createUnlockToken(vault, unverifiedVaultConfig); diff --git a/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java b/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java new file mode 100644 index 000000000..23a76122d --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java @@ -0,0 +1,197 @@ +package org.cryptomator.data.repository; + +import android.content.Context; + +import com.google.common.io.BaseEncoding; +import com.nimbusds.jose.JWEObject; + +import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor; +import org.cryptomator.data.util.NetworkTimeout; +import org.cryptomator.domain.UnverifiedHubVaultConfig; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.exception.FatalBackendException; +import org.cryptomator.domain.exception.hub.HubDeviceAlreadyRegisteredForOtherUserException; +import org.cryptomator.domain.exception.hub.HubDeviceSetupRequiredException; +import org.cryptomator.domain.exception.hub.HubInvalidSetupCodeException; +import org.cryptomator.domain.exception.hub.HubLicenseUpgradeRequiredException; +import org.cryptomator.domain.exception.hub.HubUserSetupRequiredException; +import org.cryptomator.domain.exception.hub.HubVaultAccessForbiddenException; +import org.cryptomator.domain.exception.hub.HubVaultIsArchivedException; +import org.cryptomator.domain.repository.HubRepository; +import org.cryptomator.util.crypto.HubDeviceCryptor; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.text.ParseException; +import java.time.Instant; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import timber.log.Timber; + +@Singleton +public class HubRepositoryImpl implements HubRepository { + + private final OkHttpClient httpClient; + private final HubDeviceCryptor hubDeviceCryptor; + + @Inject + public HubRepositoryImpl(Context context) { + this.httpClient = new OkHttpClient.Builder() // + .addInterceptor(httpLoggingInterceptor(context)) // + .connectTimeout(NetworkTimeout.CONNECTION.getTimeout(), NetworkTimeout.CONNECTION.getUnit()) // + .readTimeout(NetworkTimeout.READ.getTimeout(), NetworkTimeout.READ.getUnit()) // + .writeTimeout(NetworkTimeout.WRITE.getTimeout(), NetworkTimeout.WRITE.getUnit()) // + .build(); + this.hubDeviceCryptor = HubDeviceCryptor.getInstance(); + } + + private Interceptor httpLoggingInterceptor(Context context) { + HttpLoggingInterceptor.Logger logger = message -> Timber.tag("OkHttp").d(message); + return new HttpLoggingInterceptor(logger, context); + } + + @Override + public String getVaultKeyJwe(UnverifiedHubVaultConfig unverifiedHubVaultConfig, String accessToken) throws BackendException { + var request = new Request.Builder().get() // + .header("Authorization", "Bearer " + accessToken) // + .url(unverifiedHubVaultConfig.getApiBaseUrl() + "vaults/" + unverifiedHubVaultConfig.vaultId() + "/access-token") // + .build(); + try (var response = httpClient.newCall(request).execute()) { + switch (response.code()) { + case HttpURLConnection.HTTP_OK: + if (response.body() != null) { + return response.body().string(); + } else { + throw new FatalBackendException("Failed to load JWE, response code good but no body"); + } + case HttpURLConnection.HTTP_PAYMENT_REQUIRED: + throw new HubLicenseUpgradeRequiredException(); + case HttpURLConnection.HTTP_FORBIDDEN: + throw new HubVaultAccessForbiddenException(); + case HttpURLConnection.HTTP_GONE: + throw new HubVaultIsArchivedException(); + case 449: + throw new HubUserSetupRequiredException(); + default: + throw new FatalBackendException("Failed with response code " + response.code()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public UserDto getUser(UnverifiedHubVaultConfig unverifiedHubVaultConfig, String accessToken) throws FatalBackendException { + var request = new Request.Builder().get() // + .header("Authorization", "Bearer " + accessToken) // + .url(unverifiedHubVaultConfig.getApiBaseUrl() + "users/me") // + .build(); + try (var response = httpClient.newCall(request).execute()) { + if (response.isSuccessful() && response.body() != null) { + JSONObject jsonObject = new JSONObject(response.body().string()); + return new UserDto(jsonObject.getString("id"), jsonObject.getString("name"), jsonObject.getString("publicKey"), jsonObject.getString("privateKey"), jsonObject.getString("setupCode")); + } + throw new FatalBackendException("Failed to load user, bad response code " + response.code()); + } catch (IOException | JSONException e) { + throw new FatalBackendException("Failed to load user", e); + } + } + + @Override + public DeviceDto getDevice(UnverifiedHubVaultConfig unverifiedHubVaultConfig, String accessToken) throws BackendException { + var request = new Request.Builder().get() // + .header("Authorization", "Bearer " + accessToken) // + .url(unverifiedHubVaultConfig.getApiBaseUrl() + "devices/" + hubDeviceCryptor.getDeviceId()).build(); + try (var response = httpClient.newCall(request).execute()) { + switch (response.code()) { + case HttpURLConnection.HTTP_OK: + if (response.body() != null) { + JSONObject jsonObject = new JSONObject(response.body().string()); + return new DeviceDto(jsonObject.getString("userPrivateKey")); + } else { + throw new FatalBackendException("Failed to load device, response code good but no body"); + } + case HttpURLConnection.HTTP_NOT_FOUND: + throw new HubDeviceSetupRequiredException(); + default: + throw new FatalBackendException("Failed to load device with response code " + response.code()); + } + } catch (IOException | JSONException e) { + throw new FatalBackendException("Failed to load device", e); + } + } + + @Override + public void createDevice(UnverifiedHubVaultConfig unverifiedHubVaultConfig, String accessToken, String deviceName, String setupCode, String userPrivateKey) throws BackendException { + var deviceId = hubDeviceCryptor.getDeviceId(); + var publicKey = BaseEncoding.base64().encode(hubDeviceCryptor.getDevicePublicKey().getEncoded()); + + JWEObject encryptedUserKey; + try { + encryptedUserKey = hubDeviceCryptor.encryptUserKey(JWEObject.parse(userPrivateKey), setupCode); + } catch (HubDeviceCryptor.InvalidJweKeyException e) { + throw new HubInvalidSetupCodeException(e); + } catch (ParseException e) { + throw new FatalBackendException("Failed to parse user private key", e); + } + var dto = new JSONObject(); + try { + dto.put("id", deviceId); + dto.put("name", deviceName); + dto.put("publicKey", publicKey); + dto.put("type", "MOBILE"); + dto.put("userPrivateKey", encryptedUserKey.serialize()); + dto.put("creationTime", Instant.now().toString()); + } catch (JSONException e) { + throw new FatalBackendException("Failed to parse user private key", e); + } + + var request = new Request.Builder() // + .put(RequestBody.create(dto.toString(), MediaType.parse("application/json; charset=utf-8"))) // + .header("Authorization", "Bearer " + accessToken) // + .url(unverifiedHubVaultConfig.getApiBaseUrl() + "devices/" + deviceId) // + .build(); + try (var response = httpClient.newCall(request).execute()) { + switch (response.code()) { + case HttpURLConnection.HTTP_CREATED: + Timber.tag("HubRepositoryImpl").i("Device created"); + break; + case HttpURLConnection.HTTP_CONFLICT: + throw new HubDeviceAlreadyRegisteredForOtherUserException(); + default: + throw new FatalBackendException("Failed to load device with response code " + response.code()); + } + } catch (IOException e) { + throw new FatalBackendException(e); + } + } + + @Override + public ConfigDto getConfig(UnverifiedHubVaultConfig unverifiedHubVaultConfig, String accessToken) throws BackendException { + var request = new Request.Builder().get() // + .header("Authorization", "Bearer " + accessToken) // + .url(unverifiedHubVaultConfig.getApiBaseUrl() + "config").build(); + try (var response = httpClient.newCall(request).execute()) { + if (response.isSuccessful()) { + if (response.body() != null) { + JSONObject jsonObject = new JSONObject(response.body().string()); + return new ConfigDto(jsonObject.getInt("apiLevel")); + } else { + throw new FatalBackendException("Failed to load device, response code good but no body"); + } + } + throw new FatalBackendException("Failed to load device with response code " + response.code()); + } catch (IOException | JSONException e) { + throw new FatalBackendException("Failed to load device", e); + } + } +} 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 7bd1fff98..13799da4b 100644 --- a/data/src/main/java/org/cryptomator/data/repository/RepositoryModule.java +++ b/data/src/main/java/org/cryptomator/data/repository/RepositoryModule.java @@ -2,6 +2,7 @@ import org.cryptomator.domain.repository.CloudContentRepository; import org.cryptomator.domain.repository.CloudRepository; +import org.cryptomator.domain.repository.HubRepository; import org.cryptomator.domain.repository.UpdateCheckRepository; import org.cryptomator.domain.repository.VaultRepository; @@ -31,6 +32,12 @@ public CloudContentRepository provideCloudContentRepository(DispatchingCloudCont return cloudContentRepository; } + @Singleton + @Provides + public HubRepository provideHubRepositoryRepository(HubRepositoryImpl hubRepository) { + return hubRepository; + } + @Singleton @Provides public UpdateCheckRepository provideBetaStatusRepository(UpdateCheckRepositoryImpl updateCheckRepository) { diff --git a/domain/src/main/java/org/cryptomator/domain/UnverifiedHubVaultConfig.kt b/domain/src/main/java/org/cryptomator/domain/UnverifiedHubVaultConfig.kt new file mode 100644 index 000000000..92da3ac7d --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/UnverifiedHubVaultConfig.kt @@ -0,0 +1,23 @@ +package org.cryptomator.domain + +import java.net.URI + +class UnverifiedHubVaultConfig( + override val jwt: String, + override val keyId: URI, + override val vaultFormat: Int, + val clientId: String, + val authEndpoint: String, + val tokenEndpoint: String, + val authSuccessUrl: String, + val authErrorUrl: String, + val apiBaseUrl: String?, + val devicesResourceUrl: String, +) : UnverifiedVaultConfig(jwt, keyId, vaultFormat) { + + fun vaultId(): String { + assert(keyId.scheme.startsWith("hub+")) + val path = keyId.path + return path.substring(path.lastIndexOf('/') + 1) + } +} diff --git a/domain/src/main/java/org/cryptomator/domain/UnverifiedVaultConfig.kt b/domain/src/main/java/org/cryptomator/domain/UnverifiedVaultConfig.kt index 2bb8b14d5..ed242fb28 100644 --- a/domain/src/main/java/org/cryptomator/domain/UnverifiedVaultConfig.kt +++ b/domain/src/main/java/org/cryptomator/domain/UnverifiedVaultConfig.kt @@ -3,4 +3,4 @@ package org.cryptomator.domain import java.io.Serializable import java.net.URI -class UnverifiedVaultConfig(val jwt: String, val keyId: URI, val vaultFormat: Int) : Serializable +open class UnverifiedVaultConfig(open val jwt: String, open val keyId: URI, open val vaultFormat: Int) : Serializable diff --git a/domain/src/main/java/org/cryptomator/domain/exception/hub/HubAuthenticationFailedException.java b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubAuthenticationFailedException.java new file mode 100644 index 000000000..b4e18d3cf --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubAuthenticationFailedException.java @@ -0,0 +1,15 @@ +package org.cryptomator.domain.exception.hub; + +import org.cryptomator.domain.exception.BackendException; + +public class HubAuthenticationFailedException extends BackendException { + + public HubAuthenticationFailedException() { + super(); + } + + public HubAuthenticationFailedException(Exception e) { + super(e); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/hub/HubDeviceAlreadyRegisteredForOtherUserException.java b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubDeviceAlreadyRegisteredForOtherUserException.java new file mode 100644 index 000000000..0543217f1 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubDeviceAlreadyRegisteredForOtherUserException.java @@ -0,0 +1,11 @@ +package org.cryptomator.domain.exception.hub; + +import org.cryptomator.domain.exception.BackendException; + +public class HubDeviceAlreadyRegisteredForOtherUserException extends BackendException { + + public HubDeviceAlreadyRegisteredForOtherUserException() { + super(); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/hub/HubDeviceSetupRequiredException.java b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubDeviceSetupRequiredException.java new file mode 100644 index 000000000..7081def2c --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubDeviceSetupRequiredException.java @@ -0,0 +1,11 @@ +package org.cryptomator.domain.exception.hub; + +import org.cryptomator.domain.exception.BackendException; + +public class HubDeviceSetupRequiredException extends BackendException { + + public HubDeviceSetupRequiredException() { + super(); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/hub/HubInvalidSetupCodeException.java b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubInvalidSetupCodeException.java new file mode 100644 index 000000000..350264c3b --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubInvalidSetupCodeException.java @@ -0,0 +1,11 @@ +package org.cryptomator.domain.exception.hub; + +import org.cryptomator.domain.exception.BackendException; + +public class HubInvalidSetupCodeException extends BackendException { + + public HubInvalidSetupCodeException(Throwable e) { + super(e); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/hub/HubInvalidVersionException.java b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubInvalidVersionException.java new file mode 100644 index 000000000..363aa7fb7 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubInvalidVersionException.java @@ -0,0 +1,11 @@ +package org.cryptomator.domain.exception.hub; + +import org.cryptomator.domain.exception.BackendException; + +public class HubInvalidVersionException extends BackendException { + + public HubInvalidVersionException(String message) { + super(message); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/hub/HubLicenseUpgradeRequiredException.java b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubLicenseUpgradeRequiredException.java new file mode 100644 index 000000000..4da6c6e66 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubLicenseUpgradeRequiredException.java @@ -0,0 +1,11 @@ +package org.cryptomator.domain.exception.hub; + +import org.cryptomator.domain.exception.BackendException; + +public class HubLicenseUpgradeRequiredException extends BackendException { + + public HubLicenseUpgradeRequiredException() { + super(); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/hub/HubUserSetupRequiredException.java b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubUserSetupRequiredException.java new file mode 100644 index 000000000..4659bd183 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubUserSetupRequiredException.java @@ -0,0 +1,11 @@ +package org.cryptomator.domain.exception.hub; + +import org.cryptomator.domain.exception.BackendException; + +public class HubUserSetupRequiredException extends BackendException { + + public HubUserSetupRequiredException() { + super(); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultAccessForbiddenException.java b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultAccessForbiddenException.java new file mode 100644 index 000000000..d2e0a1b94 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultAccessForbiddenException.java @@ -0,0 +1,11 @@ +package org.cryptomator.domain.exception.hub; + +import org.cryptomator.domain.exception.BackendException; + +public class HubVaultAccessForbiddenException extends BackendException { + + public HubVaultAccessForbiddenException() { + super(); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultIsArchivedException.java b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultIsArchivedException.java new file mode 100644 index 000000000..3112dfa82 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultIsArchivedException.java @@ -0,0 +1,11 @@ +package org.cryptomator.domain.exception.hub; + +import org.cryptomator.domain.exception.BackendException; + +public class HubVaultIsArchivedException extends BackendException { + + public HubVaultIsArchivedException() { + super(); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultOperationNotSupportedException.java b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultOperationNotSupportedException.java new file mode 100644 index 000000000..2db6e509d --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/hub/HubVaultOperationNotSupportedException.java @@ -0,0 +1,11 @@ +package org.cryptomator.domain.exception.hub; + +import org.cryptomator.domain.exception.BackendException; + +public class HubVaultOperationNotSupportedException extends BackendException { + + public HubVaultOperationNotSupportedException() { + super(); + } + +} 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 e3147f931..a16a75cfa 100644 --- a/domain/src/main/java/org/cryptomator/domain/repository/CloudRepository.java +++ b/domain/src/main/java/org/cryptomator/domain/repository/CloudRepository.java @@ -41,4 +41,6 @@ public interface CloudRepository { Cloud unlock(Vault vault, Optional vaultFile, CharSequence password, Flag cancelledFlag) throws BackendException; + Cloud unlock(Vault vault, UnverifiedVaultConfig unverifiedVaultConfig, String vaultKeyJwe, String userKeyJwe, Flag cancelledFlag) throws BackendException; + } diff --git a/domain/src/main/java/org/cryptomator/domain/repository/HubRepository.kt b/domain/src/main/java/org/cryptomator/domain/repository/HubRepository.kt new file mode 100644 index 000000000..02db0d8ed --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/repository/HubRepository.kt @@ -0,0 +1,29 @@ +package org.cryptomator.domain.repository + +import org.cryptomator.domain.UnverifiedHubVaultConfig +import org.cryptomator.domain.exception.BackendException + +interface HubRepository { + + @Throws(BackendException::class) + fun getVaultKeyJwe(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, accessToken: String): String + + @Throws(BackendException::class) + fun getUser(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, accessToken: String): UserDto + + @Throws(BackendException::class) + fun getDevice(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, accessToken: String): DeviceDto + + @Throws(BackendException::class) + fun createDevice(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, accessToken: String, deviceName: String, setupCode: String, userPrivateKey: String) + + @Throws(BackendException::class) + fun getConfig(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, accessToken: String): ConfigDto + + data class DeviceDto(val userPrivateKey: String) + + data class ConfigDto(val apiLevel: Int) + + data class UserDto(val id: String, val name: String, val publicKey: String, val privateKey: String, val setupCode: String) + +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/CreateHubDevice.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/CreateHubDevice.java new file mode 100644 index 000000000..2241dc019 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/CreateHubDevice.java @@ -0,0 +1,32 @@ +package org.cryptomator.domain.usecases.vault; + +import org.cryptomator.domain.UnverifiedHubVaultConfig; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.repository.HubRepository; +import org.cryptomator.generator.Parameter; +import org.cryptomator.generator.UseCase; + + +@UseCase +class CreateHubDevice { + + private final HubRepository hubRepository; + private final UnverifiedHubVaultConfig unverifiedVaultConfig; + private final String accessToken; + private final String deviceName; + private final String setupCode; + + public CreateHubDevice(HubRepository hubRepository, @Parameter UnverifiedHubVaultConfig unverifiedVaultConfig, @Parameter String accessToken, @Parameter String deviceName, @Parameter String setupCode) { + this.hubRepository = hubRepository; + this.unverifiedVaultConfig = unverifiedVaultConfig; + this.accessToken = accessToken; + this.deviceName = deviceName; + this.setupCode = setupCode; + } + + public void execute() throws BackendException { + HubRepository.UserDto user = hubRepository.getUser(unverifiedVaultConfig, accessToken); + hubRepository.createDevice(unverifiedVaultConfig, accessToken, deviceName, setupCode, user.getPrivateKey()); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockHubVault.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockHubVault.java new file mode 100644 index 000000000..b9744b24a --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/UnlockHubVault.java @@ -0,0 +1,53 @@ +package org.cryptomator.domain.usecases.vault; + +import org.cryptomator.domain.Cloud; +import org.cryptomator.domain.UnverifiedHubVaultConfig; +import org.cryptomator.domain.Vault; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.exception.hub.HubInvalidVersionException; +import org.cryptomator.domain.repository.CloudRepository; +import org.cryptomator.domain.repository.HubRepository; +import org.cryptomator.domain.usecases.cloud.Flag; +import org.cryptomator.generator.Parameter; +import org.cryptomator.generator.UseCase; + +@UseCase +class UnlockHubVault { + + private final int HUB_MINIMUM_VERSION = 1; + private final CloudRepository cloudRepository; + private final Vault vault; + private final HubRepository hubRepository; + private final UnverifiedHubVaultConfig unverifiedVaultConfig; + private final String accessToken; + private volatile boolean cancelled; + private final Flag cancelledFlag = new Flag() { + @Override + public boolean get() { + return cancelled; + } + }; + + public UnlockHubVault(CloudRepository cloudRepository, HubRepository hubRepository, @Parameter Vault vault, @Parameter UnverifiedHubVaultConfig unverifiedVaultConfig, @Parameter String accessToken) { + this.cloudRepository = cloudRepository; + this.vault = vault; + this.hubRepository = hubRepository; + this.unverifiedVaultConfig = unverifiedVaultConfig; + this.accessToken = accessToken; + } + + public void onCancel() { + cancelled = true; + } + + public Cloud execute() throws BackendException { + HubRepository.ConfigDto config = hubRepository.getConfig(unverifiedVaultConfig, accessToken); + if (config.getApiLevel() < HUB_MINIMUM_VERSION) { + throw new HubInvalidVersionException("Version is " + config.getApiLevel() + " but minimum is " + HUB_MINIMUM_VERSION); + } + String vaultKeyJwe = hubRepository.getVaultKeyJwe(unverifiedVaultConfig, accessToken); + HubRepository.DeviceDto device = hubRepository.getDevice(unverifiedVaultConfig, accessToken); + return cloudRepository.unlock(vault, unverifiedVaultConfig, vaultKeyJwe, device.getUserPrivateKey(), cancelledFlag); + } + +} diff --git a/presentation/build.gradle b/presentation/build.gradle index 39e5fcf06..82ad0e45b 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -48,6 +48,10 @@ android { viewBinding true } + defaultConfig.manifestPlaceholders = [ + 'appAuthRedirectScheme': 'org.cryptomator.android' + ] + buildTypes { release { crunchPngs false @@ -171,6 +175,8 @@ dependencies { implementation dependencies.androidxPreference implementation dependencies.androidxBiometric + implementation dependencies.appauth + // cloud playstoreImplementation dependencies.dropboxCore playstoreImplementation dependencies.dropboxAndroid diff --git a/presentation/src/main/java/org/cryptomator/presentation/di/component/ApplicationComponent.java b/presentation/src/main/java/org/cryptomator/presentation/di/component/ApplicationComponent.java index 19b3c96c9..ca0232028 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/di/component/ApplicationComponent.java +++ b/presentation/src/main/java/org/cryptomator/presentation/di/component/ApplicationComponent.java @@ -9,6 +9,7 @@ import org.cryptomator.domain.executor.ThreadExecutor; import org.cryptomator.domain.repository.CloudContentRepository; import org.cryptomator.domain.repository.CloudRepository; +import org.cryptomator.domain.repository.HubRepository; import org.cryptomator.domain.repository.UpdateCheckRepository; import org.cryptomator.domain.repository.VaultRepository; import org.cryptomator.presentation.di.module.ApplicationModule; @@ -36,6 +37,8 @@ public interface ApplicationComponent { CloudRepository cloudRepository(); + HubRepository hubRepository(); + UpdateCheckRepository updateCheckRepository(); FileUtil fileUtil(); 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 a6d26333f..465663565 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt @@ -12,6 +12,11 @@ import org.cryptomator.domain.exception.NoSuchCloudFileException import org.cryptomator.domain.exception.UnableToDecryptWebdavPasswordException import org.cryptomator.domain.exception.VaultAlreadyExistException import org.cryptomator.domain.exception.authentication.AuthenticationException +import org.cryptomator.domain.exception.hub.HubAuthenticationFailedException +import org.cryptomator.domain.exception.hub.HubDeviceAlreadyRegisteredForOtherUserException +import org.cryptomator.domain.exception.hub.HubInvalidSetupCodeException +import org.cryptomator.domain.exception.hub.HubInvalidVersionException +import org.cryptomator.domain.exception.hub.HubVaultOperationNotSupportedException import org.cryptomator.domain.exception.license.DesktopSupporterCertificateException import org.cryptomator.domain.exception.license.LicenseNotValidException import org.cryptomator.domain.exception.license.NoLicenseAvailableException @@ -74,6 +79,11 @@ class ExceptionHandlers @Inject constructor(private val context: Context, defaul 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) + staticHandler(HubVaultOperationNotSupportedException::class.java, R.string.error_hub_vault_operation_not_supported) + staticHandler(HubAuthenticationFailedException::class.java, R.string.error_hub_authentication_failed) + staticHandler(HubDeviceAlreadyRegisteredForOtherUserException::class.java, R.string.error_hub_device_already_register_for_other_user) + staticHandler(HubInvalidSetupCodeException::class.java, R.string.error_hub_invalid_setup_code) + staticHandler(HubInvalidVersionException::class.java, R.string.error_hub_invalid_version) exceptionHandlers.add(MissingCryptorExceptionHandler()) exceptionHandlers.add(CancellationExceptionHandler()) exceptionHandlers.add(NoSuchVaultExceptionHandler()) diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/ProgressStateModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/ProgressStateModel.kt index 93acd60ca..d11d0b1fc 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/ProgressStateModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/ProgressStateModel.kt @@ -47,6 +47,7 @@ open class ProgressStateModel private constructor(private val name: String, imag val UNLOCKING_VAULT = ProgressStateModel("VAULT", text(R.string.dialog_progress_unlocking_vault)) val CHANGING_PASSWORD = ProgressStateModel("PASSWORD", text(R.string.dialog_progress_change_password)) val CREATING_VAULT = ProgressStateModel("VAULT", text(R.string.dialog_progress_creating_vault)) + val CREATING_HUB_DEVICE = ProgressStateModel("HUB_DEVICE", text(R.string.dialog_progress_creating_hub_device_setup)) val UNKNOWN = ProgressStateModel("UNKNOWN_MIMETYPE", text(R.string.dialog_progress_please_wait)) val COMPLETED = ProgressStateModel("COMPLETED") 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 3d57504c4..8a2dedba1 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -1,22 +1,40 @@ package org.cryptomator.presentation.presenter +import android.content.Intent +import android.net.Uri import android.os.Handler import androidx.biometric.BiometricManager import com.google.common.base.Optional +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationRequest +import net.openid.appauth.AuthorizationResponse +import net.openid.appauth.AuthorizationService +import net.openid.appauth.AuthorizationServiceConfiguration +import net.openid.appauth.ResponseTypeValues import org.cryptomator.data.cloud.crypto.CryptoConstants import org.cryptomator.domain.Cloud +import org.cryptomator.domain.UnverifiedHubVaultConfig 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.exception.hub.HubAuthenticationFailedException +import org.cryptomator.domain.exception.hub.HubDeviceSetupRequiredException +import org.cryptomator.domain.exception.hub.HubLicenseUpgradeRequiredException +import org.cryptomator.domain.exception.hub.HubUserSetupRequiredException +import org.cryptomator.domain.exception.hub.HubVaultAccessForbiddenException +import org.cryptomator.domain.exception.hub.HubVaultIsArchivedException +import org.cryptomator.domain.exception.hub.HubVaultOperationNotSupportedException import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase +import org.cryptomator.domain.usecases.vault.CreateHubDeviceUseCase 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 import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsAndDisableBiometricAuthUseCase import org.cryptomator.domain.usecases.vault.SaveVaultUseCase +import org.cryptomator.domain.usecases.vault.UnlockHubVaultUseCase import org.cryptomator.domain.usecases.vault.UnlockToken import org.cryptomator.domain.usecases.vault.UnlockVaultUsingMasterkeyUseCase import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken @@ -47,6 +65,8 @@ class UnlockVaultPresenter @Inject constructor( private val lockVaultUseCase: LockVaultUseCase, private val unlockVaultUsingMasterkeyUseCase: UnlockVaultUsingMasterkeyUseCase, private val prepareUnlockUseCase: PrepareUnlockUseCase, + private val unlockHubVaultUseCase: UnlockHubVaultUseCase, + private val createHubDeviceUseCase: CreateHubDeviceUseCase, private val removeStoredVaultPasswordsAndDisableBiometricAuthUseCase: RemoveStoredVaultPasswordsAndDisableBiometricAuthUseCase, private val saveVaultUseCase: SaveVaultUseCase, private val authenticationExceptionHandler: AuthenticationExceptionHandler, @@ -57,6 +77,7 @@ class UnlockVaultPresenter @Inject constructor( private var startedUsingPrepareUnlock = false private var retryUnlockHandler: Handler? = null private var pendingUnlock: PendingUnlock? = null + private var hubAuthService: AuthorizationService? = null @InjectIntent lateinit var intent: UnlockVaultIntent @@ -70,6 +91,7 @@ class UnlockVaultPresenter @Inject constructor( running = false retryUnlockHandler?.removeCallbacksAndMessages(null) } + hubAuthService?.dispose() } fun setup() { @@ -83,7 +105,7 @@ class UnlockVaultPresenter @Inject constructor( .withVault(vault) .run(object : DefaultResultHandler>() { override fun onSuccess(unverifiedVaultConfig: Optional) { - onUnverifiedVaultConfigRetrieved(unverifiedVaultConfig) + onUnverifiedVaultConfigRetrieved(unverifiedVaultConfig, vault) } override fun onError(e: Throwable) { @@ -103,7 +125,7 @@ class UnlockVaultPresenter @Inject constructor( .withVault(Vault.aCopyOf(vault).withCloud(cloud).build()) .run(object : DefaultResultHandler>() { override fun onSuccess(unverifiedVaultConfig: Optional) { - onUnverifiedVaultConfigRetrieved(unverifiedVaultConfig) + onUnverifiedVaultConfigRetrieved(unverifiedVaultConfig, vault) } override fun onError(e: Throwable) { @@ -119,7 +141,7 @@ class UnlockVaultPresenter @Inject constructor( } } - private fun onUnverifiedVaultConfigRetrieved(unverifiedVaultConfig: Optional) { + private fun onUnverifiedVaultConfigRetrieved(unverifiedVaultConfig: Optional, vault: Vault) { if (!unverifiedVaultConfig.isPresent || unverifiedVaultConfig.get().keyId.scheme == CryptoConstants.MASTERKEY_SCHEME) { when (intent.vaultAction()) { UnlockVaultIntent.VaultAction.UNLOCK, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> { @@ -130,6 +152,117 @@ class UnlockVaultPresenter @Inject constructor( UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(intent.vaultModel(), unverifiedVaultConfig.orNull()) else -> {} } + } else if (unverifiedVaultConfig.isPresent && unverifiedVaultConfig.get().keyId.scheme.startsWith(CryptoConstants.HUB_SCHEME)) { + when (intent.vaultAction()) { + UnlockVaultIntent.VaultAction.UNLOCK -> { + val unverifiedHubVaultConfig = unverifiedVaultConfig.get() as UnverifiedHubVaultConfig + if (hubAuthService == null) { + hubAuthService = AuthorizationService(context()) + } + view?.showProgress(ProgressModel.GENERIC) + unlockHubVault(unverifiedHubVaultConfig, vault) + } + UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> showErrorAndFinish(HubVaultOperationNotSupportedException()) + UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> showErrorAndFinish(HubVaultOperationNotSupportedException()) + UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD -> showErrorAndFinish(HubVaultOperationNotSupportedException()) + } + } + } + + private fun showErrorAndFinish(e: Throwable) { + showError(e) + finishWithResult(null) + } + + private fun unlockHubVault(unverifiedVaultConfig: UnverifiedHubVaultConfig, vault: Vault) { + val authIntent = buildHubAuthIntent(unverifiedVaultConfig) + requestActivityResult(ActivityResultCallbacks.hubAuthenticationUnlock(vault, unverifiedVaultConfig), authIntent) + } + + private fun buildHubAuthIntent(unverifiedVaultConfig: UnverifiedHubVaultConfig): Intent? { + val serviceConfig = AuthorizationServiceConfiguration(Uri.parse(unverifiedVaultConfig.authEndpoint), Uri.parse(unverifiedVaultConfig.tokenEndpoint)) + val authRequestBuilder = AuthorizationRequest.Builder( + serviceConfig, + unverifiedVaultConfig.clientId, + ResponseTypeValues.CODE, + Uri.parse(CryptoConstants.HUB_REDIRECT_URL) + ) + return hubAuthService?.getAuthorizationRequestIntent(authRequestBuilder.build()) + } + + @Callback(dispatchResultOkOnly = false) + fun hubAuthenticationUnlock(result: ActivityResult, vault: Vault, unverifiedHubVaultConfig: UnverifiedHubVaultConfig) { + if (result.isResultOk) { + val resp = AuthorizationResponse.fromIntent(result.intent()) + if (resp != null) { + hubAuthService?.performTokenRequest(resp.createTokenExchangeRequest()) { token, ex -> + token?.accessToken?.let { + unlockHubVaultUseCase + .withVault(vault) + .andUnverifiedVaultConfig(unverifiedHubVaultConfig) + .andAccessToken(it) + .run(object : DefaultResultHandler() { + override fun onSuccess(cloud: Cloud) { + finishWithResult(cloud) + } + + override fun onError(e: Throwable) { + when (e) { + is HubDeviceSetupRequiredException -> view?.showCreateHubDeviceDialog(VaultModel(vault), unverifiedHubVaultConfig) + is HubUserSetupRequiredException -> view?.showHubUserSetupRequiredDialog(unverifiedHubVaultConfig) + is HubLicenseUpgradeRequiredException -> view?.showHubLicenseUpgradeRequiredDialog() + is HubVaultAccessForbiddenException -> view?.showHubVaultAccessForbiddenDialog() + is HubVaultIsArchivedException -> view?.showHubVaultIsArchivedDialog() + else -> { + super.onError(e) + finishWithResult(null) + } + } + } + }) + } ?: showErrorAndFinish(HubAuthenticationFailedException(ex)) + } + } else { + val ex = AuthorizationException.fromIntent(result.intent()) + showErrorAndFinish(HubAuthenticationFailedException(ex)) + } + } else { + showErrorAndFinish(HubAuthenticationFailedException()) + } + } + + fun onCreateHubDeviceClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedHubVaultConfig, deviceName: String, setupCode: String) { + view?.showProgress(ProgressModel.GENERIC) + val authIntent = buildHubAuthIntent(unverifiedVaultConfig) + requestActivityResult(ActivityResultCallbacks.hubAuthenticationCreateDevice(vaultModel, unverifiedVaultConfig, deviceName, setupCode), authIntent) + } + + @Callback(dispatchResultOkOnly = false) + fun hubAuthenticationCreateDevice(result: ActivityResult, vault: VaultModel, unverifiedHubVaultConfig: UnverifiedHubVaultConfig, deviceName: String, setupCode: String) { + if (result.isResultOk) { + val resp = AuthorizationResponse.fromIntent(result.intent()) + if (resp != null) { + hubAuthService?.performTokenRequest(resp.createTokenExchangeRequest()) { token, ex -> + token?.accessToken?.let { + createHubDeviceUseCase + .withAccessToken(it) + .andUnverifiedVaultConfig(unverifiedHubVaultConfig) + .andDeviceName(deviceName) + .andSetupCode(setupCode) + .run(object : DefaultResultHandler() { + override fun onSuccess(void: Void?) { + view?.showProgress(ProgressModel.COMPLETED) + unlockHubVault(unverifiedHubVaultConfig, vault.toVault()) + } + }) + } ?: showErrorAndFinish(HubAuthenticationFailedException(ex)) + } + } else { + val ex = AuthorizationException.fromIntent(result.intent()) + showErrorAndFinish(HubAuthenticationFailedException(ex)) + } + } else { + showErrorAndFinish(HubAuthenticationFailedException()) } } @@ -411,6 +544,21 @@ class UnlockVaultPresenter @Inject constructor( finishWithResult(null) } + fun onGoToHubProfileClicked(unverifiedVaultConfig: UnverifiedHubVaultConfig) { + val userProfileUri = unverifiedVaultConfig.apiBaseUrl.let { baseUrl -> + val trimmedPath = baseUrl.toString().removeSuffix("/").substringBeforeLast("/") + Uri.parse("$trimmedPath/app/profile") + } + val intent = Intent(Intent.ACTION_VIEW) + intent.data = userProfileUri + requestActivityResult(ActivityResultCallbacks.onGoToHubProfileFinished(), intent) + } + + @Callback(dispatchResultOkOnly = false) + fun onGoToHubProfileFinished(result: ActivityResult) { + finishWithResult(null) + } + private open class PendingUnlock(private val vault: Vault?) : Serializable { private var unlockToken: UnlockToken? = null @@ -458,7 +606,9 @@ class UnlockVaultPresenter @Inject constructor( getUnverifiedVaultConfigUseCase, // lockVaultUseCase, // unlockVaultUsingMasterkeyUseCase, // + unlockHubVaultUseCase, // prepareUnlockUseCase, // + createHubDeviceUseCase, // removeStoredVaultPasswordsAndDisableBiometricAuthUseCase, // 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 8014d7c87..9e4258ca0 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.UnverifiedHubVaultConfig import org.cryptomator.domain.UnverifiedVaultConfig import org.cryptomator.domain.Vault import org.cryptomator.generator.Activity @@ -14,7 +15,12 @@ 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.CreateHubDeviceDialog import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog +import org.cryptomator.presentation.ui.dialog.HubLicenseUpgradeRequiredDialog +import org.cryptomator.presentation.ui.dialog.HubUserSetupRequiredDialog +import org.cryptomator.presentation.ui.dialog.HubVaultAccessForbiddenDialog +import org.cryptomator.presentation.ui.dialog.HubVaultArchivedDialog import org.cryptomator.presentation.ui.dialog.VaultNotFoundDialog import org.cryptomator.presentation.ui.fragment.UnlockVaultFragment import org.cryptomator.presentation.util.BiometricAuthentication @@ -25,7 +31,12 @@ class UnlockVaultActivity : BaseActivity(ActivityUnl UnlockVaultView, // BiometricAuthentication.Callback, // ChangePasswordDialog.Callback, // - VaultNotFoundDialog.Callback { + VaultNotFoundDialog.Callback, // + CreateHubDeviceDialog.Callback, // + HubUserSetupRequiredDialog.Callback, // + HubVaultArchivedDialog.Callback, // + HubLicenseUpgradeRequiredDialog.Callback, // + HubVaultAccessForbiddenDialog.Callback { @Inject lateinit var presenter: UnlockVaultPresenter @@ -107,6 +118,26 @@ class UnlockVaultActivity : BaseActivity(ActivityUnl showDialog(ChangePasswordDialog.newInstance(vaultModel, unverifiedVaultConfig)) } + override fun showCreateHubDeviceDialog(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedHubVaultConfig) { + showDialog(CreateHubDeviceDialog.newInstance(vaultModel, unverifiedVaultConfig)) + } + + override fun showHubUserSetupRequiredDialog(unverifiedHubVaultConfig: UnverifiedHubVaultConfig) { + showDialog(HubUserSetupRequiredDialog.newInstance(unverifiedHubVaultConfig)) + } + + override fun showHubLicenseUpgradeRequiredDialog() { + showDialog(HubLicenseUpgradeRequiredDialog.newInstance()) + } + + override fun showHubVaultAccessForbiddenDialog() { + showDialog(HubVaultAccessForbiddenDialog.newInstance()) + } + + override fun showHubVaultIsArchivedDialog() { + showDialog(HubVaultArchivedDialog.newInstance()) + } + override fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String) { presenter.onChangePasswordClick(vaultModel, unverifiedVaultConfig, oldPassword, newPassword) } @@ -123,4 +154,32 @@ class UnlockVaultActivity : BaseActivity(ActivityUnl presenter.onCancelMissingVaultClicked(vault) } + override fun onCreateHubDeviceClicked(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedHubVaultConfig, deviceName: String, setupCode: String) { + presenter.onCreateHubDeviceClick(vaultModel, unverifiedVaultConfig, deviceName, setupCode) + } + + override fun onCreateHubDeviceCanceled() { + finish() + } + + override fun onGoToHubProfileClicked(unverifiedVaultConfig: UnverifiedHubVaultConfig) { + presenter.onGoToHubProfileClicked(unverifiedVaultConfig) + } + + override fun onCancelHubUserSetupClicked() { + finish() + } + + override fun onHubVaultArchivedDialogFinished() { + finish() + } + + override fun onHubLicenseUpgradeRequiredDialogFinished() { + finish() + } + + override fun onVaultAccessForbiddenDialogFinished() { + finish() + } + } 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 ae777241a..d920e777b 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.UnverifiedHubVaultConfig import org.cryptomator.domain.UnverifiedVaultConfig import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog @@ -13,5 +14,10 @@ interface UnlockVaultView : View, EnterPasswordDialog.Callback { fun cancelBasicAuthIfRunning() fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean fun showChangePasswordDialog(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?) + fun showCreateHubDeviceDialog(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedHubVaultConfig) + fun showHubUserSetupRequiredDialog(unverifiedHubVaultConfig: UnverifiedHubVaultConfig) + fun showHubLicenseUpgradeRequiredDialog() + fun showHubVaultAccessForbiddenDialog() + fun showHubVaultIsArchivedDialog() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateHubDeviceDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateHubDeviceDialog.kt new file mode 100644 index 000000000..6e8d790f3 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateHubDeviceDialog.kt @@ -0,0 +1,118 @@ +package org.cryptomator.presentation.ui.dialog + +import android.content.DialogInterface +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import android.widget.Button +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import org.cryptomator.domain.UnverifiedHubVaultConfig +import org.cryptomator.generator.Dialog +import org.cryptomator.presentation.R +import org.cryptomator.presentation.databinding.DialogCreateHubDeviceBinding +import org.cryptomator.presentation.databinding.ViewDialogErrorBinding +import org.cryptomator.presentation.model.ProgressModel +import org.cryptomator.presentation.model.ProgressStateModel +import org.cryptomator.presentation.model.VaultModel + +@Dialog +class CreateHubDeviceDialog : BaseProgressErrorDialog(DialogCreateHubDeviceBinding::inflate) { + + private var createDeviceButton: Button? = null + + interface Callback { + + fun onCreateHubDeviceClicked(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedHubVaultConfig, deviceName: String, setupCode: String) + fun onCreateHubDeviceCanceled() + } + + override fun onStart() { + super.onStart() + val dialog = dialog as AlertDialog? + dialog?.let { + createDeviceButton = dialog.getButton(android.app.Dialog.BUTTON_POSITIVE) + createDeviceButton?.setOnClickListener { + showProgress(ProgressModel(ProgressStateModel.CREATING_HUB_DEVICE)) + val vaultModel = requireArguments().getSerializable(VAULT_ARG) as VaultModel + val unverifiedVaultConfig = requireArguments().getSerializable(VAULT_CONFIG_ARG) as UnverifiedHubVaultConfig + if (valid(binding.etDeviceName.text.toString(), binding.etSetupCode.text.toString())) { + callback?.onCreateHubDeviceClicked(vaultModel, unverifiedVaultConfig, binding.etDeviceName.text.toString(), binding.etSetupCode.text.toString()) + onWaitForResponse(binding.etDeviceName) + } + } + dialog.setCanceledOnTouchOutside(false) + dialog.setOnKeyListener { _, keyCode, _ -> + if (keyCode == KeyEvent.KEYCODE_BACK) { + dialog.dismiss() + callback?.onCreateHubDeviceCanceled() + true + } else { + false + } + } + binding.etDeviceName.requestFocus() + binding.etDeviceName.nextFocusForwardId = binding.etSetupCode.nextFocusForwardId + createDeviceButton?.let { + binding.etSetupCode.nextFocusForwardId = it.id + registerOnEditorDoneActionAndPerformButtonClick(binding.etSetupCode) { it } + } + } + } + + private fun valid(name: String, setupCode: String): Boolean { + return when { + name.isEmpty() -> { + showError(R.string.dialog_create_hub_device_name_empty) + false + } + setupCode.isEmpty() -> { + showError(R.string.dialog_create_hub_device_setup_code_empty) + false + } + else -> true + } + } + + override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog { + return builder.setTitle(requireContext().getString(R.string.dialog_create_hub_device_title)) + .setPositiveButton(requireContext().getString(R.string.dialog_create_hub_device_positive_button)) { _: DialogInterface, _: Int -> } + .setNegativeButton(requireContext().getString(R.string.dialog_button_cancel)) { _: DialogInterface, _: Int -> callback?.onCreateHubDeviceCanceled() } + .create() + } + + override fun setupView() { + } + + override fun dialogProgressLayout(): LinearLayout { + return binding.llDialogProgress.llProgress + } + + override fun dialogProgressTextView(): TextView { + return binding.llDialogProgress.tvProgress + } + + override fun dialogErrorBinding(): ViewDialogErrorBinding { + return binding.llDialogError + } + + override fun enableViewAfterError(): View { + return binding.etDeviceName + } + + companion object { + + private const val VAULT_ARG = "vault" + private const val VAULT_CONFIG_ARG = "vaultConfig" + fun newInstance(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedHubVaultConfig): CreateHubDeviceDialog { + val args = Bundle() + args.putSerializable(VAULT_ARG, vaultModel) + args.putSerializable(VAULT_CONFIG_ARG, unverifiedVaultConfig) + val fragment = CreateHubDeviceDialog() + fragment.arguments = args + return fragment + } + } + +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubLicenseUpgradeRequiredDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubLicenseUpgradeRequiredDialog.kt new file mode 100644 index 000000000..c2a82becf --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubLicenseUpgradeRequiredDialog.kt @@ -0,0 +1,43 @@ +package org.cryptomator.presentation.ui.dialog + +import android.content.DialogInterface +import android.view.KeyEvent +import androidx.appcompat.app.AlertDialog +import org.cryptomator.generator.Dialog +import org.cryptomator.presentation.R +import org.cryptomator.presentation.databinding.DialogHubLicenseUpgradeRequiredBinding + +@Dialog +class HubLicenseUpgradeRequiredDialog : BaseDialog(DialogHubLicenseUpgradeRequiredBinding::inflate) { + + interface Callback { + + fun onHubLicenseUpgradeRequiredDialogFinished() + } + + public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog { + builder // + .setTitle(R.string.dialog_hub_license_upgrade_required_title) // + .setNeutralButton(getString(R.string.dialog_hub_license_upgrade_required_neutral_button)) { _: DialogInterface, _: Int -> callback?.onHubLicenseUpgradeRequiredDialogFinished() } + .setOnKeyListener { _, keyCode, _ -> + if (keyCode == KeyEvent.KEYCODE_BACK) { + dialog?.dismiss() + callback?.onHubLicenseUpgradeRequiredDialogFinished() + true + } else { + false + } + } + return builder.create() + } + + public override fun setupView() { + } + + companion object { + + fun newInstance(): HubLicenseUpgradeRequiredDialog { + return HubLicenseUpgradeRequiredDialog() + } + } +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubUserSetupRequiredDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubUserSetupRequiredDialog.kt new file mode 100644 index 000000000..2ccf4b674 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubUserSetupRequiredDialog.kt @@ -0,0 +1,66 @@ +package org.cryptomator.presentation.ui.dialog + +import android.content.DialogInterface +import android.os.Bundle +import android.view.KeyEvent +import android.widget.Button +import androidx.appcompat.app.AlertDialog +import org.cryptomator.domain.UnverifiedHubVaultConfig +import org.cryptomator.generator.Dialog +import org.cryptomator.presentation.R +import org.cryptomator.presentation.databinding.DialogHubUserSetupRequiredBinding + +@Dialog +class HubUserSetupRequiredDialog : BaseDialog(DialogHubUserSetupRequiredBinding::inflate) { + + private var goToProfileButton: Button? = null + + interface Callback { + + fun onGoToHubProfileClicked(unverifiedVaultConfig: UnverifiedHubVaultConfig) + fun onCancelHubUserSetupClicked() + + } + + public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog { + builder // + .setTitle(R.string.dialog_hub_user_setup_required_title) // + .setPositiveButton(getString(R.string.dialog_hub_user_setup_required_neutral_button)) { _: DialogInterface, _: Int -> } + .setNegativeButton(getString(R.string.dialog_hub_user_setup_required_negative_button)) { _: DialogInterface, _: Int -> callback?.onCancelHubUserSetupClicked() } + .setOnKeyListener { _, keyCode, _ -> + if (keyCode == KeyEvent.KEYCODE_BACK) { + dialog?.dismiss() + callback?.onCancelHubUserSetupClicked() + true + } else { + false + } + } + return builder.create() + } + + public override fun setupView() { + super.onStart() + val dialog = dialog as AlertDialog? + dialog?.let { + goToProfileButton = dialog.getButton(android.app.Dialog.BUTTON_POSITIVE) + goToProfileButton?.setOnClickListener { + val unverifiedVaultConfig = requireArguments().getSerializable(VAULT_CONFIG_ARG) as UnverifiedHubVaultConfig + callback?.onGoToHubProfileClicked(unverifiedVaultConfig) + } + dialog.setCanceledOnTouchOutside(false) + } + } + + companion object { + + private const val VAULT_CONFIG_ARG = "vaultConfig" + fun newInstance(unverifiedVaultConfig: UnverifiedHubVaultConfig): HubUserSetupRequiredDialog { + val args = Bundle() + args.putSerializable(VAULT_CONFIG_ARG, unverifiedVaultConfig) + val fragment = HubUserSetupRequiredDialog() + fragment.arguments = args + return fragment + } + } +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubVaultAccessForbiddenDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubVaultAccessForbiddenDialog.kt new file mode 100644 index 000000000..a9cb12820 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubVaultAccessForbiddenDialog.kt @@ -0,0 +1,42 @@ +package org.cryptomator.presentation.ui.dialog + +import android.content.DialogInterface +import android.view.KeyEvent +import androidx.appcompat.app.AlertDialog +import org.cryptomator.generator.Dialog +import org.cryptomator.presentation.R +import org.cryptomator.presentation.databinding.DialogHubVaultAccessForbiddenBinding + +@Dialog +class HubVaultAccessForbiddenDialog : BaseDialog(DialogHubVaultAccessForbiddenBinding::inflate) { + + interface Callback { + + fun onVaultAccessForbiddenDialogFinished() + } + + public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog { + builder // + .setTitle(R.string.dialog_hub_vault_access_forbidden_title) // + .setNeutralButton(getString(R.string.dialog_hub_vault_access_forbidden_neutral_button)) { _: DialogInterface, _: Int -> callback?.onVaultAccessForbiddenDialogFinished() } .setOnKeyListener { _, keyCode, _ -> + if (keyCode == KeyEvent.KEYCODE_BACK) { + dialog?.dismiss() + callback?.onVaultAccessForbiddenDialogFinished() + true + } else { + false + } + } + return builder.create() + } + + public override fun setupView() { + } + + companion object { + + fun newInstance(): HubVaultAccessForbiddenDialog { + return HubVaultAccessForbiddenDialog() + } + } +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubVaultArchivedDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubVaultArchivedDialog.kt new file mode 100644 index 000000000..30923ee82 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubVaultArchivedDialog.kt @@ -0,0 +1,46 @@ +package org.cryptomator.presentation.ui.dialog + +import android.content.DialogInterface +import android.view.KeyEvent +import androidx.appcompat.app.AlertDialog +import org.cryptomator.generator.Dialog +import org.cryptomator.presentation.R +import org.cryptomator.presentation.databinding.DialogHubVaultArchivedBinding + +@Dialog +class HubVaultArchivedDialog : BaseDialog(DialogHubVaultArchivedBinding::inflate) { + + interface Callback { + + fun onHubVaultArchivedDialogFinished() + } + + public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog { + builder // + .setTitle(R.string.dialog_hub_vault_archived_title) // + .setNeutralButton(getString(R.string.dialog_hub_vault_archived_positive_button)) { _: DialogInterface, _: Int -> callback?.onHubVaultArchivedDialogFinished() } + .setOnKeyListener { _, keyCode, _ -> + if (keyCode == KeyEvent.KEYCODE_BACK) { + dialog?.dismiss() + callback?.onHubVaultArchivedDialogFinished() + true + } else { + false + } + } + return builder.create() + } + + public override fun setupView() { + super.onStart() + val dialog = dialog as AlertDialog? + dialog?.setCanceledOnTouchOutside(false) + } + + companion object { + + fun newInstance(): HubVaultArchivedDialog { + return HubVaultArchivedDialog() + } + } +} diff --git a/presentation/src/main/res/layout/dialog_create_hub_device.xml b/presentation/src/main/res/layout/dialog_create_hub_device.xml new file mode 100644 index 000000000..c68a71f5e --- /dev/null +++ b/presentation/src/main/res/layout/dialog_create_hub_device.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/layout/dialog_hub_license_upgrade_required.xml b/presentation/src/main/res/layout/dialog_hub_license_upgrade_required.xml new file mode 100644 index 000000000..40461f58b --- /dev/null +++ b/presentation/src/main/res/layout/dialog_hub_license_upgrade_required.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/presentation/src/main/res/layout/dialog_hub_user_setup_required.xml b/presentation/src/main/res/layout/dialog_hub_user_setup_required.xml new file mode 100644 index 000000000..8b0473436 --- /dev/null +++ b/presentation/src/main/res/layout/dialog_hub_user_setup_required.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/presentation/src/main/res/layout/dialog_hub_vault_access_forbidden.xml b/presentation/src/main/res/layout/dialog_hub_vault_access_forbidden.xml new file mode 100644 index 000000000..f01563b46 --- /dev/null +++ b/presentation/src/main/res/layout/dialog_hub_vault_access_forbidden.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/presentation/src/main/res/layout/dialog_hub_vault_archived.xml b/presentation/src/main/res/layout/dialog_hub_vault_archived.xml new file mode 100644 index 000000000..cd69e775b --- /dev/null +++ b/presentation/src/main/res/layout/dialog_hub_vault_archived.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 11c5702f5..b12e9084f 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -44,6 +44,12 @@ Custom Masterkey location not supported yet Failed to communicate with the F-Droid app. Not installed? + Vault operation not supported for Hub + Authentication against Hub failed + This device is already registered for a different user. Try to change the user account or use a different device. + The Account Key is invalid. + Unsupported Hub version. + @@ -524,6 +530,33 @@ Migrate @string/screen_vault_list_vault_action_delete + Register Hub device + @string/screen_enter_vault_name_button_text + Name + This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device. + Name can\'t be empty. + Account Key + Your Account Key is required to login from new apps or browsers. It can be found in your profile. + Account Key can\'t be empty. + Creating device… + + Access denied + Your user has not yet been authorized to access this vault. Ask the vault owner to authorize it." + @string/dialog_unable_to_share_positive_button + + Check Hub license + Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license." + @string/dialog_unable_to_share_positive_button + + Vault archived + This vault is archived and cannot be unlocked. To unlock it, a vault owner must unarchive it. + @string/dialog_unable_to_share_positive_button + + User setup required + To proceed, please complete the steps required in your Hub user profile. + Go to Profile + @string/dialog_button_cancel + Cryptomator needs storage access to use local vaults Cryptomator needs storage access to use auto photo upload Cryptomator needs notification permissions to display vault status for example diff --git a/util/build.gradle b/util/build.gradle index 0fe3afade..0521c8092 100644 --- a/util/build.gradle +++ b/util/build.gradle @@ -53,7 +53,10 @@ dependencies { implementation dependencies.androidxPreference + implementation dependencies.joseJwt + // test + androidTestImplementation dependencies.junitParams androidTestImplementation dependencies.androidxTestCore androidTestImplementation(dependencies.runner) { exclude group: 'com.android.support', module: 'support-annotations' @@ -62,6 +65,7 @@ dependencies { androidTestImplementation(dependencies.rules) { exclude group: 'com.android.support', module: 'support-annotations' } + androidTestImplementation dependencies.mockitoAndroid testImplementation dependencies.junit testImplementation dependencies.junitApi diff --git a/util/src/androidTest/java/org/cryptomator/util/crypto/HubDeviceCryptorTest.java b/util/src/androidTest/java/org/cryptomator/util/crypto/HubDeviceCryptorTest.java new file mode 100644 index 000000000..d378fd67d --- /dev/null +++ b/util/src/androidTest/java/org/cryptomator/util/crypto/HubDeviceCryptorTest.java @@ -0,0 +1,191 @@ +package org.cryptomator.util.crypto; + +import com.nimbusds.jose.JWEObject; + +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; +import org.cryptomator.cryptolib.common.P384KeyPair; +import org.junit.Before; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.mockito.Mockito; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Base64; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class HubDeviceCryptorTest { + + // key pairs from frontend tests (crypto.spec.ts): + private static final String USER_PRIV_KEY = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDCi4K1Ts3DgTz/ufkLX7EGMHjGpJv+WJmFgyzLwwaDFSfLpDw0Kgf3FKK+LAsV8r+hZANiAARLOtFebIjxVYUmDV09Q1sVxz2Nm+NkR8fu6UojVSRcCW13tEZatx8XGrIY9zC7oBCEdRqDc68PMSvS5RA0Pg9cdBNc/kgMZ1iEmEv5YsqOcaNADDSs0bLlXb35pX7Kx5Y="; + private static final String USER_PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAESzrRXmyI8VWFJg1dPUNbFcc9jZvjZEfH7ulKI1UkXAltd7RGWrcfFxqyGPcwu6AQhHUag3OvDzEr0uUQND4PXHQTXP5IDGdYhJhL+WLKjnGjQAw0rNGy5V29+aV+yseW"; + private static final String DEVICE_PRIV_KEY = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB2bmFCWy2p+EbAn8NWS5Om+GA7c5LHhRZb8g2pSMSf0fsd7k7dZDVrnyHFiLdd/YGhZANiAAR6bsjTEdXKWIuu1Bvj6Y8wySlIROy7YpmVZTY128ItovCD8pcR4PnFljvAIb2MshCdr1alX4g6cgDOqcTeREiObcSfucOU9Ry1pJ/GnX6KA0eSljrk6rxjSDos8aiZ6Mg="; + private static final String DEVICE_PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEem7I0xHVyliLrtQb4+mPMMkpSETsu2KZlWU2NdvCLaLwg/KXEeD5xZY7wCG9jLIQna9WpV+IOnIAzqnE3kRIjm3En7nDlPUctaSfxp1+igNHkpY65Oq8Y0g6LPGomejI"; + + // used for JWE generation in frontend: (jwe.spec.ts): + private static final String PRIV_KEY = "ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ"; + private static final String PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERxQR+NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0aq6uqmUy4jUhuxnKxsv59A6JeK7Unn+mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu"; + + private KeyStore keystore; + private HubDeviceCryptor inTest; + + @Before + public void setup() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, UnrecoverableKeyException, InvalidKeySpecException { + var keyStoreSpiMock = Mockito.mock(KeyStoreSpi.class); + keystore = new KeyStore(keyStoreSpiMock, null, "test") { + }; + keystore.load(null); + + var mockCertificate = Mockito.mock(Certificate.class); + var publicKey = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PUB_KEY))); + var devicePrivateKey = KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PRIV_KEY))); + Mockito.when(mockCertificate.getPublicKey()).thenReturn(publicKey); + Mockito.when(keystore.getCertificate(HubDeviceCryptor.DEFAULT_KEY_ALIAS)).thenReturn(mockCertificate); + Mockito.when(keystore.getKey(HubDeviceCryptor.DEFAULT_KEY_ALIAS, null)).thenReturn(devicePrivateKey); + Mockito.when(keystore.containsAlias(HubDeviceCryptor.DEFAULT_KEY_ALIAS)).thenReturn(true); + + inTest = new HubDeviceCryptor(keystore); + } + + @Test + public void deviceId() { + String deviceId = inTest.getDeviceId(); + assertThat(deviceId, is("F82D0F002724A2916C5695016A17A7E8A3092FE99E0BF65B44998630330C54CA")); + } + + @Test + public void testGetDevicePublicKey() throws Exception { + var expectedPublicKey = KeyFactory // + .getInstance("EC") // + .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PUB_KEY))); + var actualPublicKey = inTest.getDevicePublicKey(); + assertThat(expectedPublicKey, is(actualPublicKey)); + } + + @Test + public void testDecryptMasterkeyUsingDeviceKey() throws ParseException { + var userKeyJwe = JWEObject.parse(""" + eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\ + xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\ + aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\ + P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\ + mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\ + kDjUaxwUKqpvT7qaAQ + """); + var vaultKeyJwe = JWEObject.parse(""" + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlA\ + tMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0\ + FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52N\ + m02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD\ + -kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2\ + U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA"""); + + var userKey = inTest.encryptUserKey(userKeyJwe, "123456"); + var masterkey = inTest.decryptVaultKey(vaultKeyJwe, userKey); + + var expectedEncKey = new byte[32]; + var expectedMacKey = new byte[32]; + Arrays.fill(expectedEncKey, (byte) 0x55); + Arrays.fill(expectedMacKey, (byte) 0x77); + assertThat(expectedEncKey, is(masterkey.getEncKey().getEncoded())); + assertThat(expectedMacKey, is(masterkey.getMacKey().getEncoded())); + } + + @Test + public void testDecryptVaultKey() throws ParseException, InvalidKeySpecException { + var vaultKeyJwe = JWEObject.parse(""" + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlA\ + tMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0\ + FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52N\ + m02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD\ + -kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2\ + U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA"""); + + var privateKey = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))).getPrivate(); + var masterkey = HubDeviceCryptor.decryptVaultKey(vaultKeyJwe, privateKey); + + var expectedEncKey = new byte[32]; + var expectedMacKey = new byte[32]; + Arrays.fill(expectedEncKey, (byte) 0x55); + Arrays.fill(expectedMacKey, (byte) 0x77); + assertThat(expectedEncKey, is(masterkey.getEncKey().getEncoded())); + assertThat(expectedMacKey, is(masterkey.getMacKey().getEncoded())); + } + + @Test + public void testEncryptUserKeyWrongSetupCode() throws ParseException { + var userKeyJwe = JWEObject.parse(""" + eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\ + xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\ + aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\ + P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\ + mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\ + kDjUaxwUKqpvT7qaAQ + """); + assertThrows(HubDeviceCryptor.InvalidJweKeyException.class, () -> HubDeviceCryptor.decryptUserKey(userKeyJwe, "WRONG_SETUP_CODE")); + } + + @Test + public void testDecryptInvalidVaultKey() throws ParseException, InvalidKeySpecException { + var wrongKey = JWEObject.parse("eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImdodGR3VnNoUU8wRGFBdjVBOXBiZ1NCTW0yYzZKWVF4dkloR3p6RVdQTncxczZZcEFYeTRQTjBXRFJUWExtQ2wiLCJ5IjoiN3Rncm1Gd016NGl0ZmVQNzBndkpLcjRSaGdjdENCMEJHZjZjWE9WZ2M0bjVXMWQ4dFgxZ1RQakdrczNVSm1zUiJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..x6JWRGSojUJUJYpp.5BRuzcaV.lLIhGH7Wz0n_iTBAubDFZA"); + var payloadIsNotJson = JWEObject.parse("eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkM2bWhsNE5BTHhEdHMwUlFlNXlyZWxQVDQyOGhDVzJNeUNYS3EwdUI0TDFMdnpXRHhVaVk3YTdZcEhJakJXcVoiLCJ5IjoiakM2dWc1NE9tbmdpNE9jUk1hdkNrczJpcFpXQjdkUmotR3QzOFhPSDRwZ2tpQ0lybWNlUnFxTnU3Z0c3Qk1yOSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..HNJJghL-SvERFz2v.N0z8YwFg.rYw29iX4i8XujdM4P4KKWg"); + var payloadFieldKeyNotAstring = JWEObject.parse("eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkJyYm9UQkl5Y0NDUEdJQlBUekU2RjBnbTRzRjRCamZPN1I0a2x0aWlCaThKZkxxcVdXNVdUSVBLN01yMXV5QVUiLCJ5IjoiNUpGVUI0WVJiYjM2RUZpN2Y0TUxMcFFyZXd2UV9Tc3dKNHRVbFd1a2c1ZU04X1ZyM2pkeml2QXI2WThRczVYbSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..QEq4Z2m6iwBx2ioS.IBo8TbKJTS4pug.61Z-agIIXgP8bX10O_yEMA"); + var payloadFieldKeyInValidBase64Data = JWEObject.parse("eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImNZdlVFZm9LYkJjenZySE5zQjUxOGpycUxPMGJDOW5lZjR4NzFFMUQ5dk95MXRqd1piZzV3cFI0OE5nU1RQdHgiLCJ5IjoiaWRJekhCWERzSzR2NTZEeU9yczJOcDZsSG1zb29fMXV0VTlzX3JNdVVkbkxuVXIzUXdLZkhYMWdaVXREM1RKayJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..0VZqu5ei9U3blGtq.eDvhU6drw7mIwvXu6Q.f05QnhI7JWG3IYHvexwdFQ"); + var privateKey = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))).getPrivate(); + + assertThrows(HubDeviceCryptor.InvalidJweKeyException.class, () -> HubDeviceCryptor.decryptVaultKey(wrongKey, privateKey)); + assertThrows(MasterkeyLoadingFailedException.class, () -> HubDeviceCryptor.decryptVaultKey(payloadIsNotJson, privateKey)); + assertThrows(MasterkeyLoadingFailedException.class, () -> HubDeviceCryptor.decryptVaultKey(payloadFieldKeyNotAstring, privateKey)); + assertThrows(MasterkeyLoadingFailedException.class, () -> HubDeviceCryptor.decryptVaultKey(payloadFieldKeyInValidBase64Data, privateKey)); + } + + @Test + public void testDecryptUserKeyECDHESWrongKey() throws ParseException, InvalidKeySpecException, NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException { + var jwe = JWEObject.parse(""" + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\ + 0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\ + NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\ + jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\ + L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\ + TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\ + PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\ + QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\ + q2koh2NbQ"""); + + var userKeyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(USER_PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(USER_PRIV_KEY))); + var incorrectDevicePrivateKey = userKeyPair.getPrivate(); + + Assertions.assertThrows(HubDeviceCryptor.InvalidJweKeyException.class, () -> HubDeviceCryptor.decryptUserKey(jwe, incorrectDevicePrivateKey)); + } + + @Test + public void testEncryptAndDecryptUserKey() { + var userKey = P384KeyPair.generate(); + var deviceKey = P384KeyPair.generate(); + + var encrypted = HubDeviceCryptor.encryptUserKey(userKey.getPrivate(), deviceKey.getPublic()); + var decrypted = HubDeviceCryptor.decryptUserKey(encrypted, deviceKey.getPrivate()); + + Assertions.assertArrayEquals(userKey.getPrivate().getEncoded(), decrypted.getEncoded()); + } + +} + diff --git a/util/src/main/java/org/cryptomator/util/crypto/HubDeviceCryptor.java b/util/src/main/java/org/cryptomator/util/crypto/HubDeviceCryptor.java new file mode 100644 index 000000000..b227a9234 --- /dev/null +++ b/util/src/main/java/org/cryptomator/util/crypto/HubDeviceCryptor.java @@ -0,0 +1,219 @@ +package org.cryptomator.util.crypto; + +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWEHeader; +import com.nimbusds.jose.JWEObject; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.crypto.ECDHDecrypter; +import com.nimbusds.jose.crypto.ECDHEncrypter; +import com.nimbusds.jose.crypto.PasswordBasedDecrypter; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.gen.ECKeyGenerator; + +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; +import org.cryptomator.cryptolib.common.MessageDigestSupplier; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.Map; +import java.util.function.Function; + +import timber.log.Timber; + +public class HubDeviceCryptor { + + static final String DEFAULT_KEYSTORE_NAME = "AndroidKeyStore"; + static final String DEFAULT_KEY_ALIAS = "hubDeviceKey"; + private static final String JWE_PAYLOAD_KEY_FIELD = "key"; + private static final String EC_ALG = "EC"; + private final KeyStore keyStore; + + HubDeviceCryptor(KeyStore keyStore) { + try { + this.keyStore = keyStore; + this.keyStore.load(null); + if (!this.keyStore.containsAlias(DEFAULT_KEY_ALIAS)) { + var keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, DEFAULT_KEYSTORE_NAME); + var parameterSpec = new KeyGenParameterSpec // + .Builder(DEFAULT_KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_AGREE_KEY) // + .setAlgorithmParameterSpec(new ECGenParameterSpec("secp384r1")) // + .setDigests(KeyProperties.DIGEST_SHA256) // + .setUserAuthenticationRequired(false) // + .build(); + keyPairGenerator.initialize(parameterSpec); + keyPairGenerator.generateKeyPair(); + } + } catch (KeyStoreException | NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException | CertificateException | IOException e) { + throw new RuntimeException(e); + } + } + + public static HubDeviceCryptor getInstance() { + try { + var keyStore = KeyStore.getInstance(DEFAULT_KEYSTORE_NAME); + return new HubDeviceCryptor(keyStore); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + } + + public static ECPrivateKey decryptUserKey(JWEObject jwe, String setupCode) throws InvalidJweKeyException { + try { + jwe.decrypt(new PasswordBasedDecrypter(setupCode)); + return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, HubDeviceCryptor::decodeECPrivateKey); + } catch (JOSEException e) { + throw new InvalidJweKeyException(e); + } + } + + public static ECPrivateKey decryptUserKey(JWEObject jwe, PrivateKey deviceKey) { + try { + jwe.decrypt(new ECDHDecrypter(deviceKey, null, Curve.P_384)); + return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, HubDeviceCryptor::decodeECPrivateKey); + } catch (JOSEException e) { + throw new InvalidJweKeyException(e); + } + } + + private static ECPrivateKey decodeECPrivateKey(byte[] encoded) throws KeyDecodeFailedException { + try { + var factory = KeyFactory.getInstance(EC_ALG); + var privateKey = factory.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + if (privateKey instanceof ECPrivateKey ecPrivateKey) { + return ecPrivateKey; + } else { + throw new IllegalStateException(EC_ALG + " key factory not generating ECPrivateKeys"); + } + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(EC_ALG + " not supported"); + } catch (InvalidKeySpecException e) { + throw new KeyDecodeFailedException(e); + } + } + + private static T readKey(JWEObject jwe, String keyField, Function rawKeyFactory) throws MasterkeyLoadingFailedException { + Preconditions.checkArgument(jwe.getState() == JWEObject.State.DECRYPTED); + var fields = jwe.getPayload().toJSONObject(); + if (fields == null) { + Timber.tag("HubDeviceCryptor").e("Expected JWE payload to be JSON: " + jwe.getPayload()); + throw new MasterkeyLoadingFailedException("Expected JWE payload to be JSON"); + } + var keyBytes = new byte[0]; + try { + if (fields.get(keyField) instanceof String key) { + keyBytes = Base64.getDecoder().decode(key); + return rawKeyFactory.apply(keyBytes); + } else { + throw new IllegalArgumentException("JWE payload doesn't contain field " + keyField); + } + } catch (IllegalArgumentException | KeyDecodeFailedException e) { + Timber.tag("HubDeviceCryptor").e("Unexpected JWE payload: " + jwe.getPayload()); + throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e); + } finally { + Arrays.fill(keyBytes, (byte) 0x00); + } + } + + private static JWEObject encryptKey(Key key, ECPublicKey userKey) { + try { + var encodedVaultKey = Base64.getEncoder().encodeToString(key.getEncoded()); + var keyGen = new ECKeyGenerator(Curve.P_384); + var ephemeralKeyPair = keyGen.generate(); + var header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM).ephemeralPublicKey(ephemeralKeyPair.toPublicJWK()).build(); + var payload = new Payload(Map.of(JWE_PAYLOAD_KEY_FIELD, encodedVaultKey)); + var jwe = new JWEObject(header, payload); + jwe.encrypt(new ECDHEncrypter(userKey)); + return jwe; + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + + public static JWEObject encryptUserKey(ECPrivateKey userKey, ECPublicKey deviceKey) { + return encryptKey(userKey, deviceKey); + } + + public static Masterkey decryptVaultKey(JWEObject jwe, ECPrivateKey privateKey) throws InvalidJweKeyException { + try { + jwe.decrypt(new ECDHDecrypter(privateKey)); + return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, Masterkey::new); + } catch (JOSEException e) { + throw new InvalidJweKeyException(e); + } + } + + public JWEObject encryptUserKey(JWEObject userKey, String setupCode) { + var userPrivateKey = decryptUserKey(userKey, setupCode); + var devicePublicKey = getDevicePublicKey(); + return encryptUserKey(userPrivateKey, devicePublicKey); + } + + public Masterkey decryptVaultKey(JWEObject vaultKeyJwe, JWEObject userKeyJwe) { + try { + var privateKey = (PrivateKey) keyStore.getKey(DEFAULT_KEY_ALIAS, null); + var userKey = decryptUserKey(userKeyJwe, privateKey); + return decryptVaultKey(vaultKeyJwe, userKey); + } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { + throw new RuntimeException(e); + } + } + + public ECPublicKey getDevicePublicKey() { + try { + var certificate = keyStore.getCertificate(DEFAULT_KEY_ALIAS); + return (ECPublicKey) certificate.getPublicKey(); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + } + + public String getDeviceId() { + var devicePublicKey = getDevicePublicKey(); + try (var instance = MessageDigestSupplier.SHA256.instance()) { + var hashedKey = instance.get().digest(devicePublicKey.getEncoded()); + return BaseEncoding.base16().encode(hashedKey); + } + } + + public static class KeyDecodeFailedException extends CryptoException { + + public KeyDecodeFailedException(Throwable cause) { + super("Malformed key", cause); + } + } + + public static class InvalidJweKeyException extends CryptoException { + + public InvalidJweKeyException(Throwable cause) { + super("Invalid key", cause); + } + + } + +} From 729c8a056c8df387fdbc9b9bd53a4bbdeefa839c Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 26 Nov 2024 18:29:16 +0100 Subject: [PATCH 2/4] Apply suggestions from review --- .../data/cloud/crypto/VaultConfig.kt | 28 +++++++++++++------ .../data/repository/HubRepositoryImpl.java | 2 +- .../domain/UnverifiedHubVaultConfig.kt | 9 ++---- .../vaultconfig/VaultConfigLoadException.java | 4 +-- .../presenter/UnlockVaultPresenter.kt | 8 ++---- .../ui/dialog/CreateHubDeviceDialog.kt | 2 +- .../ui/dialog/HubUserSetupRequiredDialog.kt | 1 - 7 files changed, 27 insertions(+), 27 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 e58091139..11374ae4f 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 @@ -83,22 +83,32 @@ class VaultConfig private constructor(builder: VaultConfigBuilder) { fun decode(token: String): UnverifiedVaultConfig { val unverifiedJwt = JWT.decode(token) val vaultFormat = unverifiedJwt.getClaim(JSON_KEY_VAULTFORMAT).asInt() - val keyId = URI.create(unverifiedJwt.keyId) + val keyId = try { + URI.create(unverifiedJwt.keyId) + } catch (e: IllegalArgumentException) { + throw VaultConfigLoadException("Invalid 'keyId' in JWT: ${e.message}", e) + } if (keyId.scheme.startsWith(CryptoConstants.HUB_SCHEME)) { val hubClaim = unverifiedJwt.getHeaderClaim("hub").asMap() - val clientId = hubClaim["clientId"] as String - val authEndpoint = hubClaim["authEndpoint"] as String - val tokenEndpoint = hubClaim["tokenEndpoint"] as String - val authSuccessUrl = hubClaim["authSuccessUrl"] as String - val authErrorUrl = hubClaim["authErrorUrl"] as String - val apiBaseUrl = hubClaim["apiBaseUrl"] as String - val devicesResourceUrl = hubClaim["devicesResourceUrl"] as String - return UnverifiedHubVaultConfig(token, keyId, vaultFormat, clientId, authEndpoint, tokenEndpoint, authSuccessUrl, authErrorUrl, apiBaseUrl, devicesResourceUrl) + val clientId = hubClaim["clientId"] as? String ?: throw VaultConfigLoadException("Missing or invalid 'clientId' claim in JWT header") + val authEndpoint = parseUri(hubClaim, "authEndpoint") + val tokenEndpoint = parseUri(hubClaim, "tokenEndpoint") + val apiBaseUrl = parseUri(hubClaim, "apiBaseUrl") + return UnverifiedHubVaultConfig(token, keyId, vaultFormat, clientId, authEndpoint, tokenEndpoint, apiBaseUrl) } else { return UnverifiedVaultConfig(token, keyId, vaultFormat) } } + private fun parseUri(uriValue: Map, fieldName: String): URI { + val uriString = uriValue[fieldName] as? String ?: throw VaultConfigLoadException("Missing or invalid '$fieldName' claim in JWT header") + return try { + URI.create(uriString) + } catch (e: IllegalArgumentException) { + throw VaultConfigLoadException("Invalid '$fieldName' URI: ${e.message}", e) + } + } + @JvmStatic @Throws(VaultKeyInvalidException::class, VaultVersionMismatchException::class, VaultConfigLoadException::class) fun verify(rawKey: ByteArray, unverifiedVaultConfig: UnverifiedVaultConfig): VaultConfig { diff --git a/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java b/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java index 23a76122d..e906fb385 100644 --- a/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java @@ -85,7 +85,7 @@ public String getVaultKeyJwe(UnverifiedHubVaultConfig unverifiedHubVaultConfig, throw new FatalBackendException("Failed with response code " + response.code()); } } catch (IOException e) { - throw new RuntimeException(e); + throw new FatalBackendException(e); } } diff --git a/domain/src/main/java/org/cryptomator/domain/UnverifiedHubVaultConfig.kt b/domain/src/main/java/org/cryptomator/domain/UnverifiedHubVaultConfig.kt index 92da3ac7d..db23a0980 100644 --- a/domain/src/main/java/org/cryptomator/domain/UnverifiedHubVaultConfig.kt +++ b/domain/src/main/java/org/cryptomator/domain/UnverifiedHubVaultConfig.kt @@ -7,12 +7,9 @@ class UnverifiedHubVaultConfig( override val keyId: URI, override val vaultFormat: Int, val clientId: String, - val authEndpoint: String, - val tokenEndpoint: String, - val authSuccessUrl: String, - val authErrorUrl: String, - val apiBaseUrl: String?, - val devicesResourceUrl: String, + val authEndpoint: URI, + val tokenEndpoint: URI, + val apiBaseUrl: URI ) : UnverifiedVaultConfig(jwt, keyId, vaultFormat) { fun vaultId(): String { 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 088bfa708..9334524ec 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 @@ -1,7 +1,5 @@ package org.cryptomator.domain.exception.vaultconfig; -import com.auth0.jwt.exceptions.JWTVerificationException; - import org.cryptomator.domain.exception.BackendException; public class VaultConfigLoadException extends BackendException { @@ -10,7 +8,7 @@ public VaultConfigLoadException(String message) { super(message); } - public VaultConfigLoadException(String message, JWTVerificationException e) { + public VaultConfigLoadException(String message, Throwable e) { super(message, e); } 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 8a2dedba1..fde9e0bb9 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -180,7 +180,7 @@ class UnlockVaultPresenter @Inject constructor( } private fun buildHubAuthIntent(unverifiedVaultConfig: UnverifiedHubVaultConfig): Intent? { - val serviceConfig = AuthorizationServiceConfiguration(Uri.parse(unverifiedVaultConfig.authEndpoint), Uri.parse(unverifiedVaultConfig.tokenEndpoint)) + val serviceConfig = AuthorizationServiceConfiguration(Uri.parse(unverifiedVaultConfig.authEndpoint.toString()), Uri.parse(unverifiedVaultConfig.tokenEndpoint.toString())) val authRequestBuilder = AuthorizationRequest.Builder( serviceConfig, unverifiedVaultConfig.clientId, @@ -545,12 +545,8 @@ class UnlockVaultPresenter @Inject constructor( } fun onGoToHubProfileClicked(unverifiedVaultConfig: UnverifiedHubVaultConfig) { - val userProfileUri = unverifiedVaultConfig.apiBaseUrl.let { baseUrl -> - val trimmedPath = baseUrl.toString().removeSuffix("/").substringBeforeLast("/") - Uri.parse("$trimmedPath/app/profile") - } val intent = Intent(Intent.ACTION_VIEW) - intent.data = userProfileUri + intent.data = Uri.parse(unverifiedVaultConfig.apiBaseUrl.resolve("../app/profile").toString()) requestActivityResult(ActivityResultCallbacks.onGoToHubProfileFinished(), intent) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateHubDeviceDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateHubDeviceDialog.kt index 6e8d790f3..5e63e9c26 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateHubDeviceDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CreateHubDeviceDialog.kt @@ -34,10 +34,10 @@ class CreateHubDeviceDialog : BaseProgressErrorDialog Date: Tue, 26 Nov 2024 19:07:53 +0100 Subject: [PATCH 3/4] Fix HubUserSetupRequired Dialog --- .../presentation/presenter/UnlockVaultPresenter.kt | 2 +- .../presentation/ui/dialog/HubUserSetupRequiredDialog.kt | 7 ++++++- 2 files changed, 7 insertions(+), 2 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 fde9e0bb9..24f6d6aa9 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -546,7 +546,7 @@ class UnlockVaultPresenter @Inject constructor( fun onGoToHubProfileClicked(unverifiedVaultConfig: UnverifiedHubVaultConfig) { val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse(unverifiedVaultConfig.apiBaseUrl.resolve("../app/profile").toString()) + intent.data = Uri.parse(unverifiedVaultConfig.apiBaseUrl.resolve("../app/profile").toString()) requestActivityResult(ActivityResultCallbacks.onGoToHubProfileFinished(), intent) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubUserSetupRequiredDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubUserSetupRequiredDialog.kt index 161c91462..beb7094a2 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubUserSetupRequiredDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubUserSetupRequiredDialog.kt @@ -39,7 +39,8 @@ class HubUserSetupRequiredDialog : BaseDialog Date: Wed, 27 Nov 2024 13:18:46 +0100 Subject: [PATCH 4/4] Apply further suggestions from review --- .../data/cloud/crypto/HubkeyCryptoCloudProvider.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/HubkeyCryptoCloudProvider.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/HubkeyCryptoCloudProvider.kt index 00d91894f..20b834a29 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/HubkeyCryptoCloudProvider.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/HubkeyCryptoCloudProvider.kt @@ -12,10 +12,12 @@ 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.usecases.cloud.Flag import org.cryptomator.domain.usecases.vault.UnlockToken import org.cryptomator.util.crypto.HubDeviceCryptor import java.security.SecureRandom +import java.text.ParseException class HubkeyCryptoCloudProvider( private val cryptoCloudContentRepositoryFactory: CryptoCloudContentRepositoryFactory, // @@ -38,8 +40,14 @@ class HubkeyCryptoCloudProvider( } override fun unlock(vault: Vault, unverifiedVaultConfig: UnverifiedVaultConfig, vaultKeyJwe: String, userKeyJwe: String, cancelledFlag: Flag): Vault { - val vaultKey = JWEObject.parse(vaultKeyJwe) - val userKey = JWEObject.parse(userKeyJwe) + val vaultKey: JWEObject + val userKey: JWEObject + try { + vaultKey = JWEObject.parse(vaultKeyJwe) + userKey = JWEObject.parse(userKeyJwe) + } catch (e: ParseException) { + throw FatalBackendException("Failed to parse JWE strings", e) + } val masterkey = HubDeviceCryptor.getInstance().decryptVaultKey(vaultKey, userKey) val vaultConfig = verify(masterkey.encoded, unverifiedVaultConfig) val vaultFormat = vaultConfig.vaultFormat