diff --git a/data/build.gradle b/data/build.gradle index 881c59b7..b59f7d48 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -74,7 +74,7 @@ android { } greendao { - schemaVersion 6 + schemaVersion 7 } configurations.all { diff --git a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java index 100755cd..59fef0f2 100644 --- a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java +++ b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java @@ -24,7 +24,8 @@ class DatabaseUpgrades { Upgrade2To3 upgrade2To3, // Upgrade3To4 upgrade3To4, // Upgrade4To5 upgrade4To5, // - Upgrade5To6 upgrade5To6) { + Upgrade5To6 upgrade5To6, // + Upgrade6To7 upgrade6To7) { availableUpgrades = defineUpgrades( // upgrade0To1, // @@ -32,7 +33,8 @@ class DatabaseUpgrades { upgrade2To3, // upgrade3To4, // upgrade4To5, // - upgrade5To6); + upgrade5To6, // + upgrade6To7); } private static Comparator reverseOrder() { diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt new file mode 100644 index 00000000..63c2e4a7 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt @@ -0,0 +1,41 @@ +package org.cryptomator.data.db + +import org.greenrobot.greendao.database.Database +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class Upgrade6To7 @Inject constructor() : DatabaseUpgrade(6, 7) { + + override fun internalApplyTo(db: Database, origin: Int) { + db.beginTransaction() + try { + changeUpdateEntityToSupportSha256Verification(db) + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private fun changeUpdateEntityToSupportSha256Verification(db: Database) { + Sql.alterTable("UPDATE_CHECK_ENTITY").renameTo("UPDATE_CHECK_ENTITY_OLD").executeOn(db) + + Sql.createTable("UPDATE_CHECK_ENTITY") // + .id() // + .optionalText("LICENSE_TOKEN") // + .optionalText("RELEASE_NOTE") // + .optionalText("VERSION") // + .optionalText("URL_TO_APK") // + .optionalText("APK_SHA256") // + .optionalText("URL_TO_RELEASE_NOTE") // + .executeOn(db) + + Sql.insertInto("UPDATE_CHECK_ENTITY") // + .select("_id", "LICENSE_TOKEN", "RELEASE_NOTE", "VERSION", "URL_TO_APK", "URL_TO_RELEASE_NOTE") // + .columns("_id", "LICENSE_TOKEN", "RELEASE_NOTE", "VERSION", "URL_TO_APK", "URL_TO_RELEASE_NOTE") // + .from("UPDATE_CHECK_ENTITY_OLD") // + .executeOn(db) + + Sql.dropTable("UPDATE_CHECK_ENTITY_OLD").executeOn(db) + } +} diff --git a/data/src/main/java/org/cryptomator/data/db/entities/UpdateCheckEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/UpdateCheckEntity.java index 10323409..bb27dfdc 100644 --- a/data/src/main/java/org/cryptomator/data/db/entities/UpdateCheckEntity.java +++ b/data/src/main/java/org/cryptomator/data/db/entities/UpdateCheckEntity.java @@ -18,18 +18,22 @@ public class UpdateCheckEntity extends DatabaseEntity { private String urlToApk; + private String apkSha256; + private String urlToReleaseNote; public UpdateCheckEntity() { } - @Generated(hash = 38676936) - public UpdateCheckEntity(Long id, String licenseToken, String releaseNote, String version, String urlToApk, String urlToReleaseNote) { + @Generated(hash = 67239496) + public UpdateCheckEntity(Long id, String licenseToken, String releaseNote, String version, String urlToApk, String apkSha256, + String urlToReleaseNote) { this.id = id; this.licenseToken = licenseToken; this.releaseNote = releaseNote; this.version = version; this.urlToApk = urlToApk; + this.apkSha256 = apkSha256; this.urlToReleaseNote = urlToReleaseNote; } @@ -81,4 +85,12 @@ public class UpdateCheckEntity extends DatabaseEntity { public void setUrlToReleaseNote(String urlToReleaseNote) { this.urlToReleaseNote = urlToReleaseNote; } + + public String getApkSha256() { + return this.apkSha256; + } + + public void setApkSha256(String apkSha256) { + this.apkSha256 = apkSha256; + } } diff --git a/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java b/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java index 2c053cfb..41229dfe 100644 --- a/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java @@ -1,22 +1,28 @@ package org.cryptomator.data.repository; +import android.content.Context; +import android.net.Uri; + import com.google.common.io.BaseEncoding; +import org.apache.commons.codec.binary.Hex; import org.cryptomator.data.db.Database; import org.cryptomator.data.db.entities.UpdateCheckEntity; import org.cryptomator.data.util.UserAgentInterceptor; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.FatalBackendException; import org.cryptomator.domain.exception.update.GeneralUpdateErrorException; -import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException; +import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException; import org.cryptomator.domain.repository.UpdateCheckRepository; import org.cryptomator.domain.usecases.UpdateCheck; import org.cryptomator.util.Optional; import java.io.File; import java.io.IOException; +import java.security.DigestInputStream; import java.security.Key; import java.security.KeyFactory; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.interfaces.ECPublicKey; import java.security.spec.InvalidKeySpecException; @@ -25,7 +31,6 @@ import java.security.spec.X509EncodedKeySpec; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; -import javax.net.ssl.SSLHandshakeException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -42,11 +47,13 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { private final Database database; private final OkHttpClient httpClient; + private final Context context; @Inject - UpdateCheckRepositoryImpl(Database database) { + UpdateCheckRepositoryImpl(Database database, Context context) { this.httpClient = httpClient(); this.database = database; + this.context = context; } private OkHttpClient httpClient() { @@ -65,13 +72,14 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { final UpdateCheckEntity entity = database.load(UpdateCheckEntity.class, 1L); - if (entity.getVersion() != null && entity.getVersion().equals(latestVersion.version)) { + if (entity.getVersion() != null && entity.getVersion().equals(latestVersion.version) && entity.getApkSha256() != null) { return Optional.of(new UpdateCheckImpl("", entity)); } UpdateCheck updateCheck = loadUpdateStatus(latestVersion); entity.setUrlToApk(updateCheck.getUrlApk()); entity.setVersion(updateCheck.getVersion()); + entity.setApkSha256(updateCheck.getApkSha256()); database.store(entity); @@ -107,7 +115,18 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { if (response.isSuccessful()) { final BufferedSink sink = Okio.buffer(Okio.sink(file)); sink.writeAll(response.body().source()); + sink.flush(); sink.close(); + + String apkSha256 = calculateSha256(file); + + if(!apkSha256.equals(entity.getApkSha256())) { + file.delete(); + throw new HashMismatchUpdateCheckException(String.format( // + "Sha of calculated hash (%s) doesn't match the specified one (%s)", // + apkSha256, // + entity.getApkSha256())); + } } else { throw new GeneralUpdateErrorException("Failed to load update file, status code is not correct: " + response.code()); } @@ -116,6 +135,20 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { } } + private String calculateSha256(File file) throws GeneralUpdateErrorException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try(DigestInputStream digestInputStream = new DigestInputStream(context.getContentResolver().openInputStream(Uri.fromFile(file)), digest)) { + byte[] buffer = new byte[8192]; + while(digestInputStream.read(buffer) > -1) { + } + } + return new String(Hex.encodeHex(digest.digest())); + } catch (Exception e) { + throw new GeneralUpdateErrorException(e); + } + } + private LatestVersion loadLatestVersion() throws BackendException { try { final Request request = new Request // @@ -123,12 +156,6 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { .url(HOSTNAME_LATEST_VERSION) // .build(); return toLatestVersion(httpClient.newCall(request).execute()); - } catch (SSLHandshakeException e) { - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { - throw new SSLHandshakePreAndroid5UpdateCheckException("Failed to update.", e); - } else { - throw new GeneralUpdateErrorException("Failed to update. General error occurred.", e); - } } catch (IOException e) { throw new GeneralUpdateErrorException("Failed to update. General error occurred.", e); } @@ -181,12 +208,14 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { private final String releaseNote; private final String version; private final String urlApk; + private final String apkSha256; private final String urlReleaseNote; private UpdateCheckImpl(String releaseNote, LatestVersion latestVersion) { this.releaseNote = releaseNote; this.version = latestVersion.version; this.urlApk = latestVersion.urlApk; + this.apkSha256 = latestVersion.apkSha256; this.urlReleaseNote = latestVersion.urlReleaseNote; } @@ -194,6 +223,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { this.releaseNote = releaseNote; this.version = updateCheckEntity.getVersion(); this.urlApk = updateCheckEntity.getUrlToApk(); + this.apkSha256 = updateCheckEntity.getApkSha256(); this.urlReleaseNote = updateCheckEntity.getUrlToReleaseNote(); } @@ -212,6 +242,11 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { return urlApk; } + @Override + public String getApkSha256() { + return apkSha256; + } + @Override public String getUrlReleaseNote() { return urlReleaseNote; @@ -222,6 +257,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { private final String version; private final String urlApk; + private final String apkSha256; private final String urlReleaseNote; LatestVersion(String json) throws GeneralUpdateErrorException { @@ -234,6 +270,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository { version = jws.get("version", String.class); urlApk = jws.get("url", String.class); + apkSha256 = jws.get("apk_sha_256", String.class); urlReleaseNote = jws.get("release_notes", String.class); } catch (Exception e) { throw new GeneralUpdateErrorException("Failed to parse latest version", e); diff --git a/domain/src/main/java/org/cryptomator/domain/exception/update/GeneralUpdateErrorException.java b/domain/src/main/java/org/cryptomator/domain/exception/update/GeneralUpdateErrorException.java index dee9870a..5b6d14f6 100644 --- a/domain/src/main/java/org/cryptomator/domain/exception/update/GeneralUpdateErrorException.java +++ b/domain/src/main/java/org/cryptomator/domain/exception/update/GeneralUpdateErrorException.java @@ -11,4 +11,8 @@ public class GeneralUpdateErrorException extends BackendException { public GeneralUpdateErrorException(final String message, final Exception e) { super(message, e); } + + public GeneralUpdateErrorException(Exception e) { + super(e); + } } diff --git a/domain/src/main/java/org/cryptomator/domain/exception/update/HashMismatchUpdateCheckException.java b/domain/src/main/java/org/cryptomator/domain/exception/update/HashMismatchUpdateCheckException.java new file mode 100644 index 00000000..5168ab01 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/exception/update/HashMismatchUpdateCheckException.java @@ -0,0 +1,9 @@ +package org.cryptomator.domain.exception.update; + +public class HashMismatchUpdateCheckException extends GeneralUpdateErrorException { + + public HashMismatchUpdateCheckException(final String message) { + super(message); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/UpdateCheck.java b/domain/src/main/java/org/cryptomator/domain/usecases/UpdateCheck.java index 494b33da..99bd3f1b 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/UpdateCheck.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/UpdateCheck.java @@ -8,5 +8,7 @@ public interface UpdateCheck { String getUrlApk(); + String getApkSha256(); + String getUrlReleaseNote(); } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6e06db4a..51ba7e52 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -102,11 +102,13 @@ platform :android do |options| server_host = ENV["APK_STORE_BASIC_URL"] base_url = "https://#{server_host}/android/" apk_url = "#{base_url}#{version}/Cryptomator-#{version}.apk" + apk_sha_256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_signed.apk" release_note_url = "#{base_url}#{version}/release-notes.html" claims = { "version": version, "url": apk_url, + "apk_sha_256": apk_sha_256, "release_notes": release_note_url } diff --git a/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt b/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt index 93c2365f..89949aff 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt @@ -16,6 +16,7 @@ import org.cryptomator.domain.exception.authentication.AuthenticationException import org.cryptomator.domain.exception.license.LicenseNotValidException import org.cryptomator.domain.exception.license.NoLicenseAvailableException import org.cryptomator.domain.exception.update.GeneralUpdateErrorException +import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException import org.cryptomator.presentation.R import org.cryptomator.presentation.ui.activity.view.View @@ -44,6 +45,7 @@ class ExceptionHandlers @Inject constructor(private val context: Context, defaul staticHandler(UnableToDecryptWebdavPasswordException::class.java, R.string.error_failed_to_decrypt_webdav_password) staticHandler(LicenseNotValidException::class.java, R.string.dialog_enter_license_not_valid_content) staticHandler(NoLicenseAvailableException::class.java, R.string.dialog_enter_license_no_content) + staticHandler(HashMismatchUpdateCheckException::class.java, R.string.error_hash_mismatch_update) staticHandler(GeneralUpdateErrorException::class.java, R.string.error_general_update) staticHandler(SSLHandshakePreAndroid5UpdateCheckException::class.java, R.string.error_general_update) staticHandler(NoSuchBucketException::class.java, R.string.error_no_such_bucket) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 25a5c224..94fbfbc4 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -28,6 +28,7 @@ File names can\'t contain special characters. Vault name can\'t contain special characters. Update check failed. General error occurred. + Update check failed. Calculated hash doesn\'t match the uploaded file Update check failed. No internet connection. Failed to decrypt WebDAV password, please re add in settings Play Services not installed