Skip to content

Commit

Permalink
Initial Hub support
Browse files Browse the repository at this point in the history
  • Loading branch information
SailReal committed Nov 26, 2024
1 parent 5076ee7 commit 96c0b3d
Show file tree
Hide file tree
Showing 47 changed files with 1,764 additions and 7 deletions.
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
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);
}
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"
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)
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 @@ -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
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

0 comments on commit 96c0b3d

Please sign in to comment.