Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hub support #560

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions buildsystem/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ ext {

gsonVersion = '2.11.0'

joseJwtVersion = '9.47'

appauthVersion = '0.11.1'

okHttpVersion = '4.12.0'
okHttpDigestVersion = '3.1.0'

Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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}",
Expand All @@ -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}",
Expand All @@ -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}",
Expand Down
2 changes: 2 additions & 0 deletions data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ dependencies {
compileOnly dependencies.javaxAnnotation
implementation dependencies.gson

implementation dependencies.joseJwt

implementation dependencies.commonsCodec

implementation dependencies.documentFile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,7 +41,7 @@ public CryptoCloudFactory(CloudContentRepository/*<Cloud, CloudNode, CloudFolder
}

public void create(CloudFolder location, CharSequence password) throws BackendException {
cryptoCloudProvider(Optional.absent()).create(location, password);
masterkeyCryptoCloudProvider().create(location, password);
}

public Cloud decryptedViewOf(Vault vault) throws BackendException {
Expand Down Expand Up @@ -68,6 +69,10 @@ public Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> 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> unverifiedVaultConfig) throws BackendException {
return cryptoCloudProvider(unverifiedVaultConfig).createUnlockToken(vault, unverifiedVaultConfig);
}
Expand All @@ -84,10 +89,20 @@ public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> 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<UnverifiedVaultConfig> 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);
SailReal marked this conversation as resolved.
Show resolved Hide resolved
}
throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme()));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public interface CryptoCloudProvider {

Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> 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> unverifiedVaultConfig, CharSequence password) throws BackendException;

void lock(Vault vault);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
SailReal marked this conversation as resolved.
Show resolved Hide resolved
const val ROOT_DIR_ID = ""
const val DATA_DIR_NAME = "d"
const val VAULT_FILE_NAME = "vault.cryptomator"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UnverifiedVaultConfig>, password: CharSequence, cancelledFlag: Flag): Vault {
throw IllegalStateException("Hub can not unlock vaults using password")
}

@Throws(BackendException::class)
override fun unlock(token: UnlockToken, unverifiedVaultConfig: Optional<UnverifiedVaultConfig>, 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)
SailReal marked this conversation as resolved.
Show resolved Hide resolved
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<UnverifiedVaultConfig>): 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<UnverifiedVaultConfig>, 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<UnverifiedVaultConfig>, 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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<UnverifiedVaultConfig>): UnlockTokenImpl {
val vaultLocation = vaultLocation(vault)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -82,8 +83,30 @@ 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)
return UnverifiedVaultConfig(token, keyId, vaultFormat)
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 ?: 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)
SailReal marked this conversation as resolved.
Show resolved Hide resolved
} else {
return UnverifiedVaultConfig(token, keyId, vaultFormat)
}
}

private fun parseUri(uriValue: Map<String, Any>, 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ public Cloud unlock(UnlockToken token, Optional<UnverifiedVaultConfig> 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> unverifiedVaultConfig) throws BackendException {
return cryptoCloudFactory.createUnlockToken(vault, unverifiedVaultConfig);
Expand Down
Loading