From 64999c451171729c86d19d610864cf6f62c0e10e Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 10 Jun 2026 10:47:42 +0200 Subject: [PATCH 1/2] Check Android License during Hub unlock --- .../data/repository/HubRepositoryImpl.java | 34 +++++++++++++-- .../usecases/AndroidLicenseVerifier.java | 43 +++++++++++++++++++ .../domain/usecases/DoLicenseCheck.java | 30 +------------ 3 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/AndroidLicenseVerifier.java 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 a6ee6025e..2c63aa2e2 100644 --- a/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java @@ -2,6 +2,7 @@ import android.content.Context; +import com.auth0.jwt.exceptions.JWTVerificationException; import com.google.common.io.BaseEncoding; import com.nimbusds.jose.JWEObject; @@ -18,12 +19,15 @@ import org.cryptomator.domain.exception.hub.HubVaultAccessForbiddenException; import org.cryptomator.domain.exception.hub.HubVaultIsArchivedException; import org.cryptomator.domain.repository.HubRepository; +import org.cryptomator.domain.usecases.AndroidLicenseVerifier; import org.cryptomator.util.crypto.HubDeviceCryptor; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.net.HttpURLConnection; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; import java.text.ParseException; import java.time.Instant; @@ -35,6 +39,7 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; +import okhttp3.Response; import timber.log.Timber; @Singleton @@ -65,9 +70,7 @@ public HubRepository.VaultAccess getVaultAccess(UnverifiedHubVaultConfig unverif switch (response.code()) { case HttpURLConnection.HTTP_OK: if (response.body() != null) { - String subscriptionHeader = response.header("Hub-Subscription-State"); - HubRepository.SubscriptionState state = "ACTIVE".equalsIgnoreCase(subscriptionHeader) ? HubRepository.SubscriptionState.ACTIVE : HubRepository.SubscriptionState.INACTIVE; - return new HubRepository.VaultAccess(response.body().string(), state); + return new HubRepository.VaultAccess(response.body().string(), resolveSubscriptionState(response)); } else { throw new FatalBackendException("Failed to load JWE, response code good but no body"); } @@ -87,6 +90,31 @@ public HubRepository.VaultAccess getVaultAccess(UnverifiedHubVaultConfig unverif } } + private HubRepository.SubscriptionState resolveSubscriptionState(Response response) { + String androidLicense = response.header("Hub-Android-License"); + if (androidLicense == null || androidLicense.isEmpty()) { + return subscriptionStateFromHeader(response.header("Hub-Subscription-State")); + } + return isAndroidLicenseValid(androidLicense) ? HubRepository.SubscriptionState.ACTIVE : HubRepository.SubscriptionState.INACTIVE; + } + + private boolean isAndroidLicenseValid(String androidLicense) { + if (androidLicense == null || androidLicense.isEmpty()) { + return false; + } + try { + AndroidLicenseVerifier.verify(androidLicense, AndroidLicenseVerifier.ANDROID_PUB_KEY); + return true; + } catch (JWTVerificationException | NoSuchAlgorithmException | InvalidKeySpecException | FatalBackendException e) { + Timber.tag("HubRepositoryImpl").e("Failed to validate Android license retrieved from Hub", e); + return false; + } + } + + private HubRepository.SubscriptionState subscriptionStateFromHeader(String subscriptionHeader) { + return "ACTIVE".equalsIgnoreCase(subscriptionHeader) ? HubRepository.SubscriptionState.ACTIVE : HubRepository.SubscriptionState.INACTIVE; + } + @Override public UserDto getUser(UnverifiedHubVaultConfig unverifiedHubVaultConfig, String accessToken) throws FatalBackendException { var request = new Request.Builder().get() // diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/AndroidLicenseVerifier.java b/domain/src/main/java/org/cryptomator/domain/usecases/AndroidLicenseVerifier.java new file mode 100644 index 000000000..76fdf0ec8 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/AndroidLicenseVerifier.java @@ -0,0 +1,43 @@ +package org.cryptomator.domain.usecases; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; +import com.google.common.io.BaseEncoding; + +import org.cryptomator.domain.exception.FatalBackendException; + +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +public final class AndroidLicenseVerifier { + + public static final String ANDROID_PUB_KEY = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBcnb81CfNeL3qBVFMx/yRfm1Y1yib" + // + "ajIJkV1s82AQt+mOl4+Kub64wq1OCgBVwWUlKwqgnyF39nmkoXEjakRPFngBzg2J" + // + "zo4UR0B7OYmn0uGf3K+zQfxKnNMxGVPtlzE8j9Nqz/dm2YvYLLVwvTSDQX/GaxoP" + // + "/EH84Hupw2wuU7qAaFU="; + + private AndroidLicenseVerifier() { + } + + public static DecodedJWT verify(String license, String base64PublicKey) throws NoSuchAlgorithmException, InvalidKeySpecException { + Algorithm algorithm = Algorithm.ECDSA512(getPublicKey(base64PublicKey), null); + JWTVerifier verifier = JWT.require(algorithm).build(); + return verifier.verify(license); + } + + private static ECPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException { + final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(BaseEncoding.base64().decode(publicKey)); + Key key = KeyFactory.getInstance("EC").generatePublic(keySpec); + if (key instanceof ECPublicKey) { + return (ECPublicKey) key; + } else { + throw new FatalBackendException("Key not an EC public key."); + } + } +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/DoLicenseCheck.java b/domain/src/main/java/org/cryptomator/domain/usecases/DoLicenseCheck.java index ed548b646..8a8cc9e58 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/DoLicenseCheck.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/DoLicenseCheck.java @@ -1,12 +1,8 @@ package org.cryptomator.domain.usecases; -import com.auth0.jwt.JWT; -import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; -import com.auth0.jwt.interfaces.JWTVerifier; -import com.google.common.io.BaseEncoding; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.FatalBackendException; @@ -17,20 +13,12 @@ import org.cryptomator.generator.UseCase; import org.cryptomator.util.SharedPreferencesHandler; -import java.security.Key; -import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; -import java.security.interfaces.ECPublicKey; import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; @UseCase public class DoLicenseCheck { - private static final String ANDROID_PUB_KEY = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBcnb81CfNeL3qBVFMx/yRfm1Y1yib" + // - "ajIJkV1s82AQt+mOl4+Kub64wq1OCgBVwWUlKwqgnyF39nmkoXEjakRPFngBzg2J" + // - "zo4UR0B7OYmn0uGf3K+zQfxKnNMxGVPtlzE8j9Nqz/dm2YvYLLVwvTSDQX/GaxoP" + // - "/EH84Hupw2wuU7qAaFU="; private static final String DESKTOP_SUPPORTER_CERTIFICATE_PUB_KEY = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW" + // "fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV" + // "gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE" + // @@ -46,9 +34,7 @@ public class DoLicenseCheck { public LicenseCheck execute() throws BackendException { license = useLicenseOrRetrieveFromPreferences(license); try { - Algorithm algorithm = Algorithm.ECDSA512(getPublicKey(ANDROID_PUB_KEY), null); - JWTVerifier verifier = JWT.require(algorithm).build(); - DecodedJWT jwt = verifier.verify(license); + DecodedJWT jwt = AndroidLicenseVerifier.verify(license, AndroidLicenseVerifier.ANDROID_PUB_KEY); sharedPreferencesHandler.setLicenseToken(license); return jwt::getSubject; } catch (SignatureVerificationException | JWTDecodeException | FatalBackendException e) { @@ -72,21 +58,9 @@ private String useLicenseOrRetrieveFromPreferences(String license) throws NoLice return stored; } - private ECPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException { - final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(BaseEncoding.base64().decode(publicKey)); - Key key = KeyFactory.getInstance("EC").generatePublic(keySpec); - if (key instanceof ECPublicKey) { - return (ECPublicKey) key; - } else { - throw new FatalBackendException("Key not an EC public key."); - } - } - private boolean isDesktopSupporterCertificate(String license) { try { - Algorithm algorithm = Algorithm.ECDSA512(getPublicKey(DESKTOP_SUPPORTER_CERTIFICATE_PUB_KEY), null); - JWTVerifier verifier = JWT.require(algorithm).build(); - verifier.verify(license); + AndroidLicenseVerifier.verify(license, DESKTOP_SUPPORTER_CERTIFICATE_PUB_KEY); return true; } catch (SignatureVerificationException | NoSuchAlgorithmException | InvalidKeySpecException e) { return false; From bcdc5c39ee596e0c6e711385a449ab94111a79d9 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 10 Jun 2026 11:30:56 +0200 Subject: [PATCH 2/2] Apply suggestions from review --- .../org/cryptomator/data/repository/HubRepositoryImpl.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 2c63aa2e2..58fd772b3 100644 --- a/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/HubRepositoryImpl.java @@ -99,14 +99,11 @@ private HubRepository.SubscriptionState resolveSubscriptionState(Response respon } private boolean isAndroidLicenseValid(String androidLicense) { - if (androidLicense == null || androidLicense.isEmpty()) { - return false; - } try { AndroidLicenseVerifier.verify(androidLicense, AndroidLicenseVerifier.ANDROID_PUB_KEY); return true; } catch (JWTVerificationException | NoSuchAlgorithmException | InvalidKeySpecException | FatalBackendException e) { - Timber.tag("HubRepositoryImpl").e("Failed to validate Android license retrieved from Hub", e); + Timber.tag("HubRepositoryImpl").e(e, "Failed to validate Android license retrieved from Hub"); return false; } }