Verify SHA256 of the APK in update process
This commit is contained in:
parent
714223743e
commit
1eb8c079c6
@ -74,7 +74,7 @@ android {
|
||||
}
|
||||
|
||||
greendao {
|
||||
schemaVersion 6
|
||||
schemaVersion 7
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
|
@ -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<DatabaseUpgrade> reverseOrder() {
|
||||
|
41
data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt
Normal file
41
data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
package org.cryptomator.domain.exception.update;
|
||||
|
||||
public class HashMismatchUpdateCheckException extends GeneralUpdateErrorException {
|
||||
|
||||
public HashMismatchUpdateCheckException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
@ -8,5 +8,7 @@ public interface UpdateCheck {
|
||||
|
||||
String getUrlApk();
|
||||
|
||||
String getApkSha256();
|
||||
|
||||
String getUrlReleaseNote();
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -28,6 +28,7 @@
|
||||
<string name="error_names_contains_invalid_characters">File names can\'t contain special characters.</string>
|
||||
<string name="error_vault_name_contains_invalid_characters">Vault name can\'t contain special characters.</string>
|
||||
<string name="error_general_update">Update check failed. General error occurred.</string>
|
||||
<string name="error_hash_mismatch_update">Update check failed. Calculated hash doesn\'t match the uploaded file</string>
|
||||
<string name="error_update_no_internet">Update check failed. No internet connection.</string>
|
||||
<string name="error_failed_to_decrypt_webdav_password">Failed to decrypt WebDAV password, please re add in settings</string>
|
||||
<string name="error_play_services_not_available">Play Services not installed</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user