Verify SHA256 of the APK in update process
This commit is contained in:
parent
714223743e
commit
1eb8c079c6
@ -74,7 +74,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
greendao {
|
greendao {
|
||||||
schemaVersion 6
|
schemaVersion 7
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.all {
|
configurations.all {
|
||||||
|
@ -24,7 +24,8 @@ class DatabaseUpgrades {
|
|||||||
Upgrade2To3 upgrade2To3, //
|
Upgrade2To3 upgrade2To3, //
|
||||||
Upgrade3To4 upgrade3To4, //
|
Upgrade3To4 upgrade3To4, //
|
||||||
Upgrade4To5 upgrade4To5, //
|
Upgrade4To5 upgrade4To5, //
|
||||||
Upgrade5To6 upgrade5To6) {
|
Upgrade5To6 upgrade5To6, //
|
||||||
|
Upgrade6To7 upgrade6To7) {
|
||||||
|
|
||||||
availableUpgrades = defineUpgrades( //
|
availableUpgrades = defineUpgrades( //
|
||||||
upgrade0To1, //
|
upgrade0To1, //
|
||||||
@ -32,7 +33,8 @@ class DatabaseUpgrades {
|
|||||||
upgrade2To3, //
|
upgrade2To3, //
|
||||||
upgrade3To4, //
|
upgrade3To4, //
|
||||||
upgrade4To5, //
|
upgrade4To5, //
|
||||||
upgrade5To6);
|
upgrade5To6, //
|
||||||
|
upgrade6To7);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Comparator<DatabaseUpgrade> reverseOrder() {
|
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 urlToApk;
|
||||||
|
|
||||||
|
private String apkSha256;
|
||||||
|
|
||||||
private String urlToReleaseNote;
|
private String urlToReleaseNote;
|
||||||
|
|
||||||
public UpdateCheckEntity() {
|
public UpdateCheckEntity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Generated(hash = 38676936)
|
@Generated(hash = 67239496)
|
||||||
public UpdateCheckEntity(Long id, String licenseToken, String releaseNote, String version, String urlToApk, String urlToReleaseNote) {
|
public UpdateCheckEntity(Long id, String licenseToken, String releaseNote, String version, String urlToApk, String apkSha256,
|
||||||
|
String urlToReleaseNote) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.licenseToken = licenseToken;
|
this.licenseToken = licenseToken;
|
||||||
this.releaseNote = releaseNote;
|
this.releaseNote = releaseNote;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.urlToApk = urlToApk;
|
this.urlToApk = urlToApk;
|
||||||
|
this.apkSha256 = apkSha256;
|
||||||
this.urlToReleaseNote = urlToReleaseNote;
|
this.urlToReleaseNote = urlToReleaseNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,4 +85,12 @@ public class UpdateCheckEntity extends DatabaseEntity {
|
|||||||
public void setUrlToReleaseNote(String urlToReleaseNote) {
|
public void setUrlToReleaseNote(String urlToReleaseNote) {
|
||||||
this.urlToReleaseNote = 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;
|
package org.cryptomator.data.repository;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Hex;
|
||||||
import org.cryptomator.data.db.Database;
|
import org.cryptomator.data.db.Database;
|
||||||
import org.cryptomator.data.db.entities.UpdateCheckEntity;
|
import org.cryptomator.data.db.entities.UpdateCheckEntity;
|
||||||
import org.cryptomator.data.util.UserAgentInterceptor;
|
import org.cryptomator.data.util.UserAgentInterceptor;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.exception.FatalBackendException;
|
import org.cryptomator.domain.exception.FatalBackendException;
|
||||||
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException;
|
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.repository.UpdateCheckRepository;
|
||||||
import org.cryptomator.domain.usecases.UpdateCheck;
|
import org.cryptomator.domain.usecases.UpdateCheck;
|
||||||
import org.cryptomator.util.Optional;
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.security.DigestInputStream;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.interfaces.ECPublicKey;
|
import java.security.interfaces.ECPublicKey;
|
||||||
import java.security.spec.InvalidKeySpecException;
|
import java.security.spec.InvalidKeySpecException;
|
||||||
@ -25,7 +31,6 @@ import java.security.spec.X509EncodedKeySpec;
|
|||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import javax.net.ssl.SSLHandshakeException;
|
|
||||||
|
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
@ -42,11 +47,13 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
|||||||
|
|
||||||
private final Database database;
|
private final Database database;
|
||||||
private final OkHttpClient httpClient;
|
private final OkHttpClient httpClient;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
UpdateCheckRepositoryImpl(Database database) {
|
UpdateCheckRepositoryImpl(Database database, Context context) {
|
||||||
this.httpClient = httpClient();
|
this.httpClient = httpClient();
|
||||||
this.database = database;
|
this.database = database;
|
||||||
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private OkHttpClient httpClient() {
|
private OkHttpClient httpClient() {
|
||||||
@ -65,13 +72,14 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
|||||||
|
|
||||||
final UpdateCheckEntity entity = database.load(UpdateCheckEntity.class, 1L);
|
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));
|
return Optional.of(new UpdateCheckImpl("", entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateCheck updateCheck = loadUpdateStatus(latestVersion);
|
UpdateCheck updateCheck = loadUpdateStatus(latestVersion);
|
||||||
entity.setUrlToApk(updateCheck.getUrlApk());
|
entity.setUrlToApk(updateCheck.getUrlApk());
|
||||||
entity.setVersion(updateCheck.getVersion());
|
entity.setVersion(updateCheck.getVersion());
|
||||||
|
entity.setApkSha256(updateCheck.getApkSha256());
|
||||||
|
|
||||||
database.store(entity);
|
database.store(entity);
|
||||||
|
|
||||||
@ -107,7 +115,18 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
|||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
final BufferedSink sink = Okio.buffer(Okio.sink(file));
|
final BufferedSink sink = Okio.buffer(Okio.sink(file));
|
||||||
sink.writeAll(response.body().source());
|
sink.writeAll(response.body().source());
|
||||||
|
sink.flush();
|
||||||
sink.close();
|
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 {
|
} else {
|
||||||
throw new GeneralUpdateErrorException("Failed to load update file, status code is not correct: " + response.code());
|
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 {
|
private LatestVersion loadLatestVersion() throws BackendException {
|
||||||
try {
|
try {
|
||||||
final Request request = new Request //
|
final Request request = new Request //
|
||||||
@ -123,12 +156,6 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
|||||||
.url(HOSTNAME_LATEST_VERSION) //
|
.url(HOSTNAME_LATEST_VERSION) //
|
||||||
.build();
|
.build();
|
||||||
return toLatestVersion(httpClient.newCall(request).execute());
|
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) {
|
} catch (IOException e) {
|
||||||
throw new GeneralUpdateErrorException("Failed to update. General error occurred.", 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 releaseNote;
|
||||||
private final String version;
|
private final String version;
|
||||||
private final String urlApk;
|
private final String urlApk;
|
||||||
|
private final String apkSha256;
|
||||||
private final String urlReleaseNote;
|
private final String urlReleaseNote;
|
||||||
|
|
||||||
private UpdateCheckImpl(String releaseNote, LatestVersion latestVersion) {
|
private UpdateCheckImpl(String releaseNote, LatestVersion latestVersion) {
|
||||||
this.releaseNote = releaseNote;
|
this.releaseNote = releaseNote;
|
||||||
this.version = latestVersion.version;
|
this.version = latestVersion.version;
|
||||||
this.urlApk = latestVersion.urlApk;
|
this.urlApk = latestVersion.urlApk;
|
||||||
|
this.apkSha256 = latestVersion.apkSha256;
|
||||||
this.urlReleaseNote = latestVersion.urlReleaseNote;
|
this.urlReleaseNote = latestVersion.urlReleaseNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,6 +223,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
|||||||
this.releaseNote = releaseNote;
|
this.releaseNote = releaseNote;
|
||||||
this.version = updateCheckEntity.getVersion();
|
this.version = updateCheckEntity.getVersion();
|
||||||
this.urlApk = updateCheckEntity.getUrlToApk();
|
this.urlApk = updateCheckEntity.getUrlToApk();
|
||||||
|
this.apkSha256 = updateCheckEntity.getApkSha256();
|
||||||
this.urlReleaseNote = updateCheckEntity.getUrlToReleaseNote();
|
this.urlReleaseNote = updateCheckEntity.getUrlToReleaseNote();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,6 +242,11 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
|||||||
return urlApk;
|
return urlApk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getApkSha256() {
|
||||||
|
return apkSha256;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUrlReleaseNote() {
|
public String getUrlReleaseNote() {
|
||||||
return urlReleaseNote;
|
return urlReleaseNote;
|
||||||
@ -222,6 +257,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
|||||||
|
|
||||||
private final String version;
|
private final String version;
|
||||||
private final String urlApk;
|
private final String urlApk;
|
||||||
|
private final String apkSha256;
|
||||||
private final String urlReleaseNote;
|
private final String urlReleaseNote;
|
||||||
|
|
||||||
LatestVersion(String json) throws GeneralUpdateErrorException {
|
LatestVersion(String json) throws GeneralUpdateErrorException {
|
||||||
@ -234,6 +270,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
|||||||
|
|
||||||
version = jws.get("version", String.class);
|
version = jws.get("version", String.class);
|
||||||
urlApk = jws.get("url", String.class);
|
urlApk = jws.get("url", String.class);
|
||||||
|
apkSha256 = jws.get("apk_sha_256", String.class);
|
||||||
urlReleaseNote = jws.get("release_notes", String.class);
|
urlReleaseNote = jws.get("release_notes", String.class);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new GeneralUpdateErrorException("Failed to parse latest version", 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) {
|
public GeneralUpdateErrorException(final String message, final Exception e) {
|
||||||
super(message, 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 getUrlApk();
|
||||||
|
|
||||||
|
String getApkSha256();
|
||||||
|
|
||||||
String getUrlReleaseNote();
|
String getUrlReleaseNote();
|
||||||
}
|
}
|
||||||
|
@ -102,11 +102,13 @@ platform :android do |options|
|
|||||||
server_host = ENV["APK_STORE_BASIC_URL"]
|
server_host = ENV["APK_STORE_BASIC_URL"]
|
||||||
base_url = "https://#{server_host}/android/"
|
base_url = "https://#{server_host}/android/"
|
||||||
apk_url = "#{base_url}#{version}/Cryptomator-#{version}.apk"
|
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"
|
release_note_url = "#{base_url}#{version}/release-notes.html"
|
||||||
|
|
||||||
claims = {
|
claims = {
|
||||||
"version": version,
|
"version": version,
|
||||||
"url": apk_url,
|
"url": apk_url,
|
||||||
|
"apk_sha_256": apk_sha_256,
|
||||||
"release_notes": release_note_url
|
"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.LicenseNotValidException
|
||||||
import org.cryptomator.domain.exception.license.NoLicenseAvailableException
|
import org.cryptomator.domain.exception.license.NoLicenseAvailableException
|
||||||
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException
|
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException
|
||||||
|
import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException
|
||||||
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException
|
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException
|
||||||
import org.cryptomator.presentation.R
|
import org.cryptomator.presentation.R
|
||||||
import org.cryptomator.presentation.ui.activity.view.View
|
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(UnableToDecryptWebdavPasswordException::class.java, R.string.error_failed_to_decrypt_webdav_password)
|
||||||
staticHandler(LicenseNotValidException::class.java, R.string.dialog_enter_license_not_valid_content)
|
staticHandler(LicenseNotValidException::class.java, R.string.dialog_enter_license_not_valid_content)
|
||||||
staticHandler(NoLicenseAvailableException::class.java, R.string.dialog_enter_license_no_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(GeneralUpdateErrorException::class.java, R.string.error_general_update)
|
||||||
staticHandler(SSLHandshakePreAndroid5UpdateCheckException::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)
|
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_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_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_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_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_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>
|
<string name="error_play_services_not_available">Play Services not installed</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user