Verify SHA256 of the APK in update process

This commit is contained in:
Julian Raufelder 2021-05-01 14:32:14 +02:00
parent 714223743e
commit 1eb8c079c6
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
11 changed files with 127 additions and 15 deletions

View File

@ -74,7 +74,7 @@ android {
}
greendao {
schemaVersion 6
schemaVersion 7
}
configurations.all {

View File

@ -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() {

View 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)
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -0,0 +1,9 @@
package org.cryptomator.domain.exception.update;
public class HashMismatchUpdateCheckException extends GeneralUpdateErrorException {
public HashMismatchUpdateCheckException(final String message) {
super(message);
}
}

View File

@ -8,5 +8,7 @@ public interface UpdateCheck {
String getUrlApk();
String getApkSha256();
String getUrlReleaseNote();
}

View File

@ -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
}

View File

@ -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)

View File

@ -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>