Commit f133d060 authored by Ulrich Scheller's avatar Ulrich Scheller
Browse files

Release 1.9.0

parent 1f21347d
# Release 1.9.0
- Bug fixes and improvement
- Text and UI adjustments
- In-App rating
This update includes bug fixes and UI improvements that optimize usage. Additionally, it is now possible to give in-app ratings.
# Release 1.8.5
- Bug fix
......
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="1.8" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
......
......@@ -65,7 +65,7 @@ pipeline {
sh '''
cd Luca
STAGING_PARAMS="-PSTAGING_API_USERNAME=$QUOTE$STAGING_API_USERNAME$QUOTE -PSTAGING_API_PASSWORD=$QUOTE$STAGING_API_PASSWORD$QUOTE"
CMD="cd /Luca/app; ./../gradlew :app:assembleQs :app:assembleAqs :app:assembleHotfix :app:assembleRelease :app:assemblePreprod $STAGING_PARAMS"
CMD="cd /Luca/app; ./../gradlew :app:assembleQs :app:assembleAqs :app:assembleHotfix :app:assemblePentest :app:assembleRelease :app:assemblePreprod $STAGING_PARAMS"
docker run --rm -u $(id -u):$(id -g) -v `pwd`:/Luca ${CONTAINER} bash -c "$CMD"
'''
}
......
......@@ -10,8 +10,8 @@ android {
applicationId "de.culture4life.luca"
minSdkVersion 21
targetSdkVersion 30
versionCode 73
versionName "1.8.5"
versionCode 75
versionName "1.9.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
......@@ -69,6 +69,12 @@ android {
applicationIdSuffix ".hotfix"
buildConfigField "String", "API_BASE_URL", '"https://app-hotfix.luca-app.de"'
}
pentest {
initWith debug
versionNameSuffix " Pentest"
applicationIdSuffix ".pentest"
buildConfigField "String", "API_BASE_URL", '"https://app-pentest.luca-app.de"'
}
release {
initWith debug
versionNameSuffix " Release"
......@@ -138,6 +144,7 @@ dependencies {
implementation 'com.github.neXenio:RxKeyStore:0.6.1'
implementation 'com.github.neXenio:RxPreferences:1.1.0'
implementation 'com.github.Nivador:RxPermissions:0.11.2'
implementation 'com.github.numerative:Five-Star-Me:2.1.0'
implementation 'com.google.android.gms:play-services-location:18.0.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.code.gson:gson:2.8.6'
......@@ -147,6 +154,7 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.ncorti:slidetoact:0.9.0'
implementation 'com.networknt:json-schema-validator:1.0.53'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
......@@ -164,8 +172,8 @@ dependencies {
implementation jackson_dataformat_cbor
implementation 'joda-time:joda-time:2.10.10'
implementation 'net.grandcentrix.tray:tray:0.12.0'
implementation 'net.yslibrary.keyboardvisibilityevent:keyboardvisibilityevent:2.3.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'com.networknt:json-schema-validator:1.0.53'
testImplementation 'androidx.test:core:1.3.0'
......
......@@ -6,16 +6,20 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.StrictMode;
import android.provider.Settings;
import de.culture4life.luca.checkin.CheckInManager;
import de.culture4life.luca.crypto.CryptoManager;
import de.culture4life.luca.dataaccess.DataAccessManager;
import de.culture4life.luca.document.DocumentManager;
import de.culture4life.luca.history.HistoryManager;
import de.culture4life.luca.location.GeofenceManager;
import de.culture4life.luca.location.LocationManager;
......@@ -26,7 +30,6 @@ import de.culture4life.luca.notification.LucaNotificationManager;
import de.culture4life.luca.preference.PreferencesManager;
import de.culture4life.luca.registration.RegistrationManager;
import de.culture4life.luca.service.LucaService;
import de.culture4life.luca.document.DocumentManager;
import de.culture4life.luca.ui.ViewError;
import de.culture4life.luca.ui.dialog.BaseDialogFragment;
import de.culture4life.luca.util.TimeUtil;
......@@ -86,6 +89,13 @@ public class LucaApplication extends MultiDexApplication {
if (BuildConfig.DEBUG) {
Timber.plant(new Timber.DebugTree());
RxJavaAssemblyTracking.enable();
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build());
}
preferencesManager = new PreferencesManager();
......@@ -99,7 +109,7 @@ public class LucaApplication extends MultiDexApplication {
meetingManager = new MeetingManager(preferencesManager, networkManager, locationManager, historyManager, cryptoManager);
checkInManager = new CheckInManager(preferencesManager, networkManager, geofenceManager, locationManager, historyManager, cryptoManager, notificationManager);
dataAccessManager = new DataAccessManager(preferencesManager, networkManager, notificationManager, checkInManager, historyManager, cryptoManager);
documentManager = new DocumentManager(preferencesManager, networkManager, historyManager, registrationManager, cryptoManager);
documentManager = new DocumentManager(preferencesManager, networkManager, historyManager, cryptoManager, registrationManager);
applicationDisposable = new CompositeDisposable();
......@@ -130,11 +140,17 @@ public class LucaApplication extends MultiDexApplication {
super.onCreate();
Timber.d("Creating application");
if (!isRunningUnitTests()) {
long t = System.currentTimeMillis();
initializeBlocking().blockingAwait(10, TimeUnit.SECONDS);
initializeAsync().subscribeOn(Schedulers.io()).doFinally(() -> {
Timber.d("initialization took %d ms", (System.currentTimeMillis() - t));
}).subscribe();
long initializationStartTimestamp = System.currentTimeMillis();
initializeBlocking()
.subscribeOn(Schedulers.io())
.doOnComplete(() -> Timber.d("Blocking initialization completed after %d ms", (System.currentTimeMillis() - initializationStartTimestamp)))
.blockingAwait(10, TimeUnit.SECONDS);
initializeAsync()
.subscribeOn(Schedulers.io())
.doOnComplete(() -> Timber.d("Async initialization completed after %d ms", (System.currentTimeMillis() - initializationStartTimestamp)))
.subscribe();
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
}
Timber.d("Application created");
......@@ -182,7 +198,7 @@ public class LucaApplication extends MultiDexApplication {
.subscribeOn(Schedulers.io())
.subscribe(
timestampOffset -> {
Timber.d("Timestamp offset: %d", timestampOffset);
Timber.d("Timestamp offset: %d ms", timestampOffset);
if (timestampOffset > MAXIMUM_TIMESTAMP_OFFSET) {
showErrorAsDialog(new ViewError.Builder(this)
.withTitle(R.string.error_timestamp_offset_title)
......@@ -310,6 +326,15 @@ public class LucaApplication extends MultiDexApplication {
System.exit(0);
}
public void restart() {
PackageManager packageManager = getPackageManager();
Intent intent = packageManager.getLaunchIntentForPackage(getPackageName());
ComponentName componentName = intent.getComponent();
Intent mainIntent = Intent.makeRestartActivityTask(componentName);
startActivity(mainIntent);
stop();
}
public void openUrl(@NonNull String url) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
......
......@@ -155,6 +155,10 @@ public class Document {
@SerializedName("dateOfBirth")
private long dateOfBirth;
@Expose
@SerializedName("provider")
private String provider;
@Expose
@SerializedName("encodedData")
private String encodedData;
......@@ -257,6 +261,14 @@ public class Document {
this.dateOfBirth = dateOfBirth;
}
public String getProvider() {
return provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
public String getEncodedData() {
return encodedData;
}
......@@ -306,8 +318,8 @@ public class Document {
}
/**
* The timestamp after which the document should be treated as invalid. Depends on {@link
* #type} and {@link #testingTimestamp} or the values set directly from a certificate.
* The timestamp after which the document should be treated as invalid. Depends on {@link #type}
* and {@link #testingTimestamp} or the values set directly from a certificate.
*/
public long getExpirationTimestamp() {
if (expirationTimestamp != 0) {
......@@ -317,8 +329,8 @@ public class Document {
}
/**
* The duration in milliseconds after which a document with the specified {@link Type} should
* be treated as invalid.
* The duration in milliseconds after which a document with the specified {@link Type} should be
* treated as invalid.
*/
public static long getExpirationDuration(@Type int type) {
switch (type) {
......@@ -351,6 +363,7 @@ public class Document {
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", dateOfBirth=" + dateOfBirth +
", provider=" + provider +
", encodedData='" + encodedData + '\'' +
", hashableEncodedData='" + hashableEncodedData + '\'' +
", procedures=" + procedures +
......
......@@ -21,6 +21,9 @@ import de.culture4life.luca.document.provider.opentestcheck.OpenTestCheckDocumen
import de.culture4life.luca.document.provider.ubirch.UbirchDocumentProvider;
import de.culture4life.luca.history.HistoryManager;
import de.culture4life.luca.network.NetworkManager;
import de.culture4life.luca.network.endpoints.LucaEndpointsV3;
import de.culture4life.luca.network.pojo.DocumentProviderData;
import de.culture4life.luca.network.pojo.DocumentProviderDataList;
import de.culture4life.luca.preference.PreferencesManager;
import de.culture4life.luca.registration.RegistrationManager;
import de.culture4life.luca.util.TimeUtil;
......@@ -43,32 +46,32 @@ public class DocumentManager extends Manager {
public static final String KEY_DOCUMENTS = "test_results";
public static final String KEY_DOCUMENT_TAG = "test_result_tag_";
public static final String KEY_PROVIDER_DATA = "document_provider_data";
private static final byte[] DOCUMENT_REDEEM_HASH_SUFFIX = "testRedeemCheck".getBytes(StandardCharsets.UTF_8);
private final PreferencesManager preferencesManager;
private final NetworkManager networkManager;
private final HistoryManager historyManager;
private final RegistrationManager registrationManager;
private final CryptoManager cryptoManager;
private final RegistrationManager registrationManager;
private final AppointmentProvider appointmentProvider;
private final UbirchDocumentProvider ubirchDocumentProvider;
private final OpenTestCheckDocumentProvider openTestCheckDocumentProvider;
private final EudccDocumentProvider eudccDocumentProvider;
private EudccDocumentProvider eudccDocumentProvider;
private BaercodeDocumentProvider baercodeDocumentProvider;
private Documents documents;
public DocumentManager(@NonNull PreferencesManager preferencesManager, @NonNull NetworkManager networkManager, @NonNull HistoryManager historyManager, @NonNull RegistrationManager registrationManager, @NonNull CryptoManager cryptoManager) {
public DocumentManager(@NonNull PreferencesManager preferencesManager, @NonNull NetworkManager networkManager, @NonNull HistoryManager historyManager, @NonNull CryptoManager cryptoManager, @NonNull RegistrationManager registrationManager) {
this.preferencesManager = preferencesManager;
this.networkManager = networkManager;
this.historyManager = historyManager;
this.registrationManager = registrationManager;
this.cryptoManager = cryptoManager;
this.registrationManager = registrationManager;
this.appointmentProvider = new AppointmentProvider();
this.ubirchDocumentProvider = new UbirchDocumentProvider();
this.openTestCheckDocumentProvider = new OpenTestCheckDocumentProvider();
this.eudccDocumentProvider = new EudccDocumentProvider();
this.openTestCheckDocumentProvider = new OpenTestCheckDocumentProvider(this);
}
@Override
......@@ -81,6 +84,7 @@ public class DocumentManager extends Manager {
cryptoManager.initialize(context)
).andThen(deleteExpiredDocuments())
.doOnComplete(() -> {
this.eudccDocumentProvider = new EudccDocumentProvider(context);
this.baercodeDocumentProvider = new BaercodeDocumentProvider(context);
Completable.fromAction(() -> this.baercodeDocumentProvider.downloadRequiredFiles())
.subscribeOn(Schedulers.io())
......@@ -227,6 +231,7 @@ public class DocumentManager extends Manager {
.flatMapCompletable(encodedDocuments -> clearDocuments()
.andThen(Observable.fromIterable(encodedDocuments)
.flatMapCompletable(encodedDocument -> parseAndValidateEncodedDocument(encodedDocument)
.doOnSuccess(document -> Timber.d("Re-importing document: %s", document))
.flatMapCompletable(this::addDocument)
.doOnError(throwable -> Timber.w("Unable to re-import document: %s", throwable.toString()))
.onErrorComplete())));
......@@ -260,6 +265,42 @@ public class DocumentManager extends Manager {
.flatMapCompletable(this::persistDocument);
}
/**
* Will emit the {@link DocumentProviderData} with a matching fingerprint or all available data
* if no fingerprint matches.
*/
public Observable<DocumentProviderData> getDocumentProviderData(@NonNull String fingerprint) {
Observable<DocumentProviderData> restoredData = restoreDocumentProviderDataListIfAvailable()
.flatMapObservable(Observable::fromIterable);
Observable<DocumentProviderData> fetchedData = fetchDocumentProviderDataList()
.flatMap(documentProviderData -> persistDocumentProviderDataList(documentProviderData)
.andThen(Single.just(documentProviderData)))
.flatMapObservable(Observable::fromIterable)
.cache();
return getDocumentProviderData(restoredData, fingerprint) // find fingerprint in previously persisted data
.switchIfEmpty(getDocumentProviderData(fetchedData, fingerprint)) // find fingerprint in fetched data
.switchIfEmpty(fetchedData); // fingerprint not found, emit all fetched data
}
public Observable<DocumentProviderData> getDocumentProviderData(Observable<DocumentProviderData> providerData, @NonNull String fingerprint) {
return providerData.filter(documentProviderData -> fingerprint.equals(documentProviderData.getFingerprint()));
}
protected Single<DocumentProviderDataList> fetchDocumentProviderDataList() {
return networkManager.getLucaEndpointsV3()
.flatMap(LucaEndpointsV3::getDocumentProviders);
}
public Maybe<DocumentProviderDataList> restoreDocumentProviderDataListIfAvailable() {
return preferencesManager.restoreIfAvailable(KEY_PROVIDER_DATA, DocumentProviderDataList.class);
}
protected Completable persistDocumentProviderDataList(@NonNull DocumentProviderDataList documentProviderData) {
return preferencesManager.persist(KEY_PROVIDER_DATA, documentProviderData);
}
/**
* @return true if the given url is a document in the <a href="https://app.luca-app.de/webapp/testresult/#eyJ0eXAi...">luca
* style</a>
......
......@@ -51,9 +51,10 @@ public class Appointment extends ProvidedDocument {
document.setTestingTimestamp(Long.parseLong(timestamp));
document.setResultTimestamp(document.getTestingTimestamp());
document.setImportTimestamp(System.currentTimeMillis());
document.setId(UUID.nameUUIDFromBytes(qrCode.getBytes()).toString());
document.setProvider(lab);
document.setEncodedData(url);
document.setHashableEncodedData(qrCode);
document.setId(UUID.nameUUIDFromBytes(qrCode.getBytes()).toString());
}
}
......@@ -32,6 +32,7 @@ public class BaercodeDocument extends ProvidedDocument {
public BaercodeDocument(@NonNull byte[] data) throws DocumentParsingException {
try {
parse(data);
document.setProvider("BärCode");
} catch (IOException e) {
throw new DocumentParsingException("Error while parsing Baercode data", e);
}
......
package de.culture4life.luca.document.provider.eudcc
import android.content.Context
import de.culture4life.luca.R
import de.culture4life.luca.document.DocumentParsingException
import de.culture4life.luca.document.provider.DocumentProvider
import dgca.verifier.app.decoder.DefaultCertificateDecoder
......@@ -12,7 +14,7 @@ import io.reactivex.rxjava3.core.Single
/**
* Provider for the EU Digital COVID Certificate (EUDCC)
*/
class EudccDocumentProvider : DocumentProvider<EudccResult>() {
class EudccDocumentProvider(val context: Context) : DocumentProvider<EudccResult>() {
private val base45Decoder = Base45Decoder()
private val decoder = DefaultCertificateDecoder(base45Decoder)
......@@ -31,6 +33,7 @@ class EudccDocumentProvider : DocumentProvider<EudccResult>() {
override fun parse(encodedData: String): Single<EudccResult> {
return Single.just(EudccResult(encodedData, decoder.decodeCertificate(encodedData)))
.map { it.document.provider = context.getString(R.string.provider_name_eu_dcc); it }
.onErrorResumeNext { throwable ->
if (throwable is DocumentParsingException) {
Single.error<DocumentParsingException>(throwable)
......
......@@ -9,6 +9,7 @@ import java.util.UUID;
import androidx.annotation.NonNull;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.reactivex.rxjava3.core.Maybe;
import timber.log.Timber;
public class OpenTestCheckDocument extends ProvidedDocument {
......@@ -20,6 +21,7 @@ public class OpenTestCheckDocument extends ProvidedDocument {
String r;
String l;
String d;
String f;
String ed;
public OpenTestCheckDocument(@NonNull String encodedJwt) {
......@@ -86,6 +88,7 @@ public class OpenTestCheckDocument extends ProvidedDocument {
if (l.startsWith("DFB") && document.getType() == Document.TYPE_UNKNOWN) {
document.setType(Document.TYPE_GREEN_PASS);
}
document.setProvider(l); // will be replaced with test software provider if available
// lab doctor name
d = claims.get("d", String.class);
......@@ -94,6 +97,9 @@ public class OpenTestCheckDocument extends ProvidedDocument {
}
document.setLabDoctorName(d);
// provider public key fingerprint
f = claims.get("f", String.class);
document.setEncodedData(encodedJwt);
document.setHashableEncodedData(getHeaderAndBody(encodedJwt));
document.setId(UUID.nameUUIDFromBytes(document.getHashableEncodedData().getBytes()).toString());
......@@ -104,4 +110,16 @@ public class OpenTestCheckDocument extends ProvidedDocument {
return parts[0] + "." + parts[1];
}
protected static String getFingerprint(@NonNull String encodedJwt) {
return Maybe.fromCallable(() -> {
String unsignedJwt = OpenTestCheckDocumentProvider.getUnsignedJwt(encodedJwt);
Claims claims = Jwts.parserBuilder()
.build()
.parseClaimsJwt(unsignedJwt)
.getBody();
return claims.get("f", String.class);
}).defaultIfEmpty("").onErrorReturnItem("").blockingGet();
}
}
......@@ -2,17 +2,24 @@ package de.culture4life.luca.document.provider.opentestcheck;
import com.google.android.gms.common.util.Hex;
import android.text.TextUtils;
import android.util.Base64;
import com.nexenio.rxkeystore.RxKeyStore;
import com.nexenio.rxkeystore.provider.hash.RxHashProvider;
import com.nexenio.rxkeystore.provider.hash.Sha256HashProvider;
import de.culture4life.luca.registration.RegistrationData;
import de.culture4life.luca.document.DocumentManager;
import de.culture4life.luca.document.DocumentParsingException;
import de.culture4life.luca.document.DocumentVerificationException;
import de.culture4life.luca.document.provider.DocumentProvider;
import de.culture4life.luca.network.pojo.DocumentProviderData;
import de.culture4life.luca.registration.RegistrationData;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import androidx.annotation.NonNull;
import io.jsonwebtoken.Jwts;
......@@ -21,6 +28,7 @@ import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import timber.log.Timber;
import static de.culture4life.luca.document.DocumentVerificationException.Reason.INVALID_SIGNATURE;
import static de.culture4life.luca.document.DocumentVerificationException.Reason.NAME_MISMATCH;
......@@ -30,24 +38,11 @@ public class OpenTestCheckDocumentProvider extends DocumentProvider<OpenTestChec
private static final String URL_PREFIX_TESTVERIFY_IO = "https://testverify.io/v1#";
protected static final RxHashProvider HASH_PROVIDER = new Sha256HashProvider(new RxKeyStore());
protected static final OpenTestCheckPublicKey[] PUBLIC_KEYS = new OpenTestCheckPublicKey[]{
new OpenTestCheckPublicKey("ticket.io", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqImfTl5rpFHeCM/cjAgeMS8mfhxGxO3+dss+1jidKRJ7ta2fOIQ6k1wPLtqh8U2HHIITXY8Atrlh81s9bSGeJIL9VY/QDeJgqwU147eDLqpO/iF4LvKa13bats+WzN2vXU9vPtk3WNRXh5SktbdMnmK49l20TgjzPac1ES3tv7MSExeF6Bq9zPrG47mUJW+Fm8AH7nID5kBYFosMcsRNVmY4PNYCYM7q17Cc/S/MjFZD+f4mzYLRnMbZs7IjLBGlrood21XHTNt1G6/1f4peA6EWCgKbCgbwdCIHl/wn/ktWOjxoAogX3oRcKOhhCcgt+7ReY8mj2exrTypmN3TscQIDAQAB"),
new OpenTestCheckPublicKey("Soda", "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu7TJ7bLhg3tvgXwnnTx8IFL80/qGDIuctJR/HN0EzooYooMJi0tauRvokbKOqZ40bPahOUMZheeaueKkzSEyfjqPAXOSNzBe9ltgHQcDWjKUuMIaTJW5w2nJncR5UAOFmuqY8aL5IInu/vVgTjP/mXqWN5JKiEGpA3R7UNn0RWFdU6HJ4qq1n52hyUNFF4bRTQHqkw3fnJEMTOqubo4StBX2GMO8F/pCn4UA8+nhzHbtvE1QE4R+ibxs2zbGwDFL5EFDdbqvLULeMfSys5pUDgx7z04oaSzh/MYvtApqOv3dxHi8HkBlCXNUMjUD6EWukcWn4IFHC9MAKzYq4JcfANNjka5ug5LUuL84GcITKqeesZzbSSUU3/OTRk4yri7QbbNKfbv3+E9rUpcQhVTVNdBo3jxqmCm25GjwmYWTLE9++UT4IiUid3Y6kAC+Xeiz2Q+9bXszj6tME1WrhKCAc6dvM6vM3neYul5xnI/K1CSq7lB8KDxCS1//7FT6EIDa4+UVSONkTrVzaVexQgt/fMO3c5TadjS8KN3Gml0DIhzahAXzp+CJ+0xgQqsTcoujRfnVCw2d3YLP6x8JKG4vPikYUhOJ1NEBuAJv1Ict1uFuIoClaw1TkoDeUU4rPcemSOb3JXRKZeIQrRGU9vOzNVfCViDYkiE3X/IbmedhTEUCAwEAAQ=="),
new OpenTestCheckPublicKey("Mein Corona Test", "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqefCWY5KQ6K6KYI4ioALntTWIXOpL5HU4Obh645VIY9q2gFl959I2XzNvKhqMNgvlbj91bY0Y8vQmGVVKyJs9BvKNi9TmjGBU6Jn88TP//jmi4K24QQ+PeqT2O+Jz8wnhQoW/tf9TJa7In31HJcDWkLK7USrLPyRL4vaosOPcBIRTCK0KrjZbEYikXBotiXHUIGNmlRRJzOoWYB8asDVDT+3GTKMXvOxIxRebdvkmRRUl0Wl/+oKlit6fN8XmgGxiJtFVaWMI0dec+afPJwmoGrNSiX9WlvV37x0qefWOc8AGauQ7HxmCq+RK7qz6+cRvo1xEXKcw/GDiXXKwZpOcBUogNee4fDR+WoF8auoCyisRKkqCFqZh09A8goiV3aqAK2JYNxgULoYZ/1BnFX/zldpD6PY+UGcb8RErfXs4Lj88bEBlm/aOs3n4N5yt3866ylOMofLFA2nl27857s/wwFpF0uD8AlBy3GynNFOLxOsCrnZULs/ipZbiBtmX5x71kydOldoQQF+Yrc1XKqZ62+MK9xIQIcvKk/azb7LGG8SWiHW4EbvmPzPSP6QqZslfLRbXcpc8g4ZCRNYHAfNGPwxqjQRNwTKYwPK4lFjqoI3dVrkCVI1hfEpXdBKBQ5TientoC82eeitiFaQIG5yXNnWAmf3gxDX4AltBsVafLsCAwEAAQ=="),
new OpenTestCheckPublicKey("DM", "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2Qg6eu+5MLfs8XZosUUOyHenh51zI4RtaXrHdNhVpHiadTO48rQU9GvDJIM6qoNFWtWMak+kvxp9qYQrCtt7YoL74GagTxhK9g4wOvBV/8AYW/asSRslpFhNi0nlKujiSdf9aKlmUouRSYO200+/8XFz+YRYhL31Srv8xEAuHjLk2wI4/d56FkjMWOxMV7aO4ClicR8WfUZzod779VpCtZY6kkhHLppQ2elLviK2SX5WjPq6srWgCYi3B0aWb/qvM1/gMs7/T/zz9WXJJeMCpElcRWVtbmYsn12wx1hZtn9RdlfpVFnQCbE8WvcvttqPspr2Z0TLgusO+1z7RiayqHVM+ai5uoAOptq4LwkJ6Hls2Aaa9+fx58lrtZ6QCK2J8aSY8z59NFeRJUMfZmTffYNG4gR9rE0hNe+u1Tg+HfFHLpPM62u3D1eCkzw7OlBaRUVlYL7LLir+WT5LP7sL2DaY2WauK6QSI603sO14c3Y8upX3SySLfjryPctzOXv0oH0RG/M2YAbX9KEImw9cYrEJjx7H9tL7QEQ1ufTh2aIeB66uiTV/Ek/lCiVuGefI2Eg0dR3iiuWXfxoqJN8f1KZoaKeIAK6V2Zf+EG15R6ClEqobGryGvcoKfpeJe0KaQSsDQ2RuveGff8Wz6qbPwu9Jvb2i++mfx1Nr22ubjwsCAwEAAQ=="),
new OpenTestCheckPublicKey("TestNow", "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2f7iILA7fFDXXjZSdVknvahoMrmExeB5S06E4KXKUogpkoe6AlkEQJJUhqmUrbIGBsrcSj09rz8WbArXRkGXp3WZELl4kOK/441KW5HAA9hXlXyrgdajCKfofg5CdaXhlvMotL2wBa7JKg/jCy/q1jFS78s8AzrU/CMQ/3w1PCAdRmiOpVLqz66D6+fliAz11Om8GLQUR6S9HN00hI00waxbTGwQ2d8bzXyF2V8hNa2Z3IdYkacvLRnEe4Lr66E0mqzLM0I+H12I23tiC0l7Pw/Nt8KD40zHbwfzuVQ+2K4cazsB3PJInl8HR5n2lqrTxTutm/NQGngtDWxVptnbdqDszrPGsS8OvJmt7cBWJDIV3bMvAmWW9IY3OLj8S707Cx/OETBj2d+nEbyNM2Kb9GLsYwTBIwZHmAo//uYjCyAVXgLnWyN495JZWKPJSsbSpjckqElfOVouJ+wqR+emjCRjOa5o2XH+iU+Aqca8o9c6yO8IxNkKuxTJuZri6rB3hqz3DT9GuRSI7UYHnisYeXAS1OPcEirqZrFP3jqeDaY8tCTtocqNsKD15oLLpFJrhhIG0Y2UwIY2f+WQflh9ySxtjmRGHArmWobmaBiKG0DcycD4nPbJbiNOwWRpWC0JNh6QzreAb2kxsc1rhFOATVMnSOJij3CQPgKi0+GdFmECAwEAAQ=="),
new OpenTestCheckPublicKey("Cosiama", "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArCAGvkp40A1W7g+dYBmdKlv4cozfsCQFRgwtq7FGd2ZN9DxRzKp0Wqv1PXuAoUyJm9PKQJjWvelf3w8ciPLYAsdsICSFFgaVpUeQxdob+D0ls9/q4B4u6nFR9te0EOwVLNJvxnBtXjkwNPOYiS9N6qsJ74gubj+VUpwJC9glSDK5DIi1p99Z/NLLiioxT0MvFr5Cr69N93d2ROemK5o+PbaPQyfrqW1DYawgZAHzx2gMiajS3lUQflwi78d0ZLWIYtSK8ynTZko7/CK3YD+jXoYrX+BIhri4j3P7KEVW2v7k94xsAl4zYeCnV9YWFgj5P9aipWS8kd6BJJkhpzfXdlm9XGOQurOXzcoumARcMxIcKSdm7daCONCFfGBpwfK1pBVHUtymEjGrgO0BLajb3F9WtTyAPCR/8QEXHeHzdi7/SlmpTq75MV0po85JhBz2cw3J4T94ERX76EfqE+F5zODj1AncMK+IiWXf7ToJkioRhPoQRUaeDuQ0RMNsdzoeNCeW+vz9ENtQaOQxKYne2655XQ470c8kBJPT0faW42CPDPA5X0k6F7oAjQxPYsWDp19V4oSYzgiF4zEk45ZcqnLTsjQTOAqNWSIN7H/OFguoadNaZssBD1k5XvRgq391QL7lQyoae47I/523e2/Xc7tLF66gTsK4F6PhD/1gOf8CAwEAAQ=="),
new OpenTestCheckPublicKey("DRK Hannover", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwCFC6kbek4KHCWdBATlZj5ARDXrBW9DLuANucicBTX3LwR4l24L4l/1kei+nBuagVk2KbZpAAVP3ygQ8UehnYCSap1TBLalRn5KAwzGUax+c2TmDmbnFZu957soChtA7cH4Ka1JdaQftcvoL+DghrOSw5jZ47AOJ6LeJ6ZITvU5i9krT3nM6Z9GtmG57QqTRwLVPGNB6PbQg18Ype6cZuCiqmukWtHDVWzi21wA/oZ/2KZUGmI5a46Zc2QqAK4PMfms2DsrIVn+0vcPTUjB5h1wzgvr1S/AD0yMYWmLjyW4QL3/y6uBLW1nTGpFVZ3uNmsGpsFBfrGb9YIBztGK09wIDAQAB"),
new OpenTestCheckPublicKey("Probatix", "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtErrLxCO1CVCYIK0iOSCyxgoqtVd8Dml8SyFt8fMKAf3sr0qhaekncCpJ9NeUEuyyD6lgJGMqka97chZHLDbSsZvahyB7mII694yaiSsRmtWnll9Gw53xT4Mvo9ATMOUq15DSHRNoVyteM+1xjNbthyQigm7/ekPfqzeLPKleszRf0yoMHpZBUH5BrSEeq34u7GpdyMlWztlttM8YYjVvnV2MUF4sIh6lGRnSp1iFDCu3sRNvM7E+UZIQVi0Mz/VxyJ2AHoIToI4Bc0PNoj92W48z/I5cBW9ZfSTljk4i51s8N56A5xiewMqW4loS6Peclyk4O1/pO1F6UjdJia1shdSK7DFh/hQdcP+8wTr/GBWUhwNzvO8WzyC7lvqasHw+E0kUW7+lGJcTsUtQqZdNUzN0CKtfIEf6KXs+jHPXojdc2eHkvc0+5OkfkEWdmLeywoP4q1CMuwntv02K/ydRvCFUsule6oYvfQY+AHb2BIFyJIBC9C67nPE/zWZPa1ExEPXWOTbG/NkL6WMQa8wLD7HFsuLzAtkGK+UmM8+E7Oukrv/0tT8wX/X0U1E3ZvJ3VRNWXiQ36MFsNeLviukUSlcdNBMOsyswwQX2fLFOC9EmX+WHDNuO3Q3RAaG5CBVVPb+R3jXR8Dd5afX86/AJuRXRrRFme/OR8UHfpDY5rsCAwEAAQ=="),
new OpenTestCheckPublicKey("Platform-8", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtd4EHACr6yIN9MAlUBMs6zJnaS0H1gxmE5lFWS/UwWduApvg20ia41Yo5uGRMZyvbPV+jxwfC+7zP6S6JFWaZPPSuVMITe5MxgBtpXfy4RYTUYIox1P4Q/DmUQ6sEzEyMzb6PYGqZgq52mwwPP8i58BdKJ6h/Q9litUcl2T0tOIzUR1mH4dJVC0xw+qPK3lhGSjF0ruCNkNoH7jLmDnLKidoJiKfpPH1QG22Vq0EJMm+ZyBNA9UwHXyGb8mctRmOl5VrI97nkXIkaRe1iYQ7xg2wfLI9JNpoW4ZDoyYCR9oIaO2qf+mfYbJKGmr3yyjoBB1//coiaRdckN6T3LiWbwIDAQAB"),
new OpenTestCheckPublicKey("NoQ", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2EqlyVSnfcss0tGFSRqlaDJs9ZBdevfKa4RkI0YM51xMk9xTNZTDCBimkN/cjJekuBlOa7PXUGd/VGO404et1/vMtLuYtvC/6fA05HZto/et6o4/UgtfBsjKWkvuGbteSYxjdn+s4GWvgkKhgZUx5R9H6E6Q1D+dvUCrJOKqy98TfrJbuZUE5xkN1+2xdfY4pJG83QcgbQdSZ4NmM2jZks8kR3wO3WdvxwPUxLeor5EB14DZY8hJG+zIzRYZ3j1TKcmk/4w4ZCgXyAl1apBxWmqkTDFq+kKUeRgjoLz7sc/kPpPRmldko/WGK4UXkOetzOlWDpLXuhVjXljLX21vZwIDAQAB"),
new OpenTestCheckPublicKey("MeinLaborergebnis/MeinTestergebnis", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3pxpUKBnIMjNSGBZNuUp056Ehpb27DtDQloyeympc/12w1EyrCeYRxHT0lH1G1Pc09K1svJWeEGVt1+4HQTOv5bZ3+Unc1ghGc8nQEDqb6RdOIXK5gT4kPRow9VC0K1wo3hSZTdkHTNY0mYKAjLZCYBmdZDL0QxIdTAMxZqyAED1CH+7pFbuAf59jHPgB93KXHcCc75sLmCnpVie7Ed+IlyI6bmQcsBrXl4wIAsGqc8tbUAr/pTPgGg1a/RnpasXUEmgfSPnxdXdKuVy+ZFyCQ0C2P2MIuhoFeiFshsXA2adVxyh4aivs0MGe5+YBWpYBJQwa/ukoWqpDrsZJtUKBQIDAQAB"),
new OpenTestCheckPublicKey("Einsatz Status", "MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQBj6mH0W1WT1gNKzo1f3PlvrOCzlT9TdxQ7ls9zQDk6xLaOyzNY+RyN84Cy0qE5jx+4vEEu3rQ1ntXxRji7SlAJjAMzytk/GkFdsf/aK6XPYTpoG7gVz3nTDm3BhpcfoivtkbWOmiU+63O9UpaFY+o4ZlTjQDJ6tpocJCakDZ18XIDKjTjFjEQk+jPcRYpeErFQuVNsHvcHT7eDG8ezrvt7a6Ixia5le9g2TVqGxW6QMHu2jPfP/k0PRBKSB+R+xHm6zexRPzKlUsza8SIhXl15jFhD4ZRLmgqea4oSUahNxrn61NaiTG7w8ZfYcwLs5g1qwbYMTgkj5CoQAKKBITfDAgMBAAE="),
new OpenTestCheckPublicKey("Testflow HQ", "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyy/ZHJqqaLuo0rc9/xmqwlnyEmC2CLZzRhw7zMuYb3EB0mHKIOHk7lV1d66osu3MAPv1vj6z6n+DDwqP0A6cc7H0GH3WcWShs3Kbq36OMYkSvEb836NSGdInvJIBW4iFmM2e+6Tau/gE9ad+rsXM5r8JropfUwYRBgyCaVs3kuqVszlZMf33nDTdIBsl+yMrjJ1+sx2lq+YnIsKcxHrHOpQO/UtyJ8C8Gpncbsy8NkZm3glmI9tSjLHFdPh6Spcd+QvSB/mLeAUGCf0Jv8YO5MdVVZYr6wcOaW6yldNJuBg7LzmnWyY0+Tc5qtxojNqBKYfOg3S9NC13r5FyNi6QpYenp/549Hbq0HFgtBL0NbR54HXYATbFMyLeXqEuHdqxxTF/2biOI7nwpHr4ywGAt+c8frHEej/Q6E/pcpe8O4v3Tn8aAGXnUlSwmT1gZ8GQCJzysehyMfXP9FTEpluTjETxVr+/iHFW8oIVJJBHAZ/m+oCF/IkFFoSUa8+wwf9hLnqLWkTwzv/QHrU/LNkV5AkP4ukokzHzzUAy8sQrMk6E5nT3wd/DJMXIIeHADgQOCRhBXqRN3IfJnzjrY26xK+NfcJZhIQCXwOFzEn7hfHy+uKMETqPbAwlog4N6QhcPfIfBTkVytyU2kidHEScjzT5V99cs8Ni7GS2ld5Kh2EsCAwEAAQ=="),
// The following key is invalid and can not be parsed:
// new OpenTestCheckPublicKey("WebCookies", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDDJgLWdKlsv0946gFx0H9iSMj9b3QLeR4HammdjLf9ypE9qJIQZQSUap8RFVcoQpeFpnMyRVC0EHmvKPLd02HJ8m6yLnPG5ADyUWwaBVM7M/geD7uSVhgsoxJ5QA7bwu2oX0ipa3uOp/SljnAQpTEmltrSl8TMLHE4BlbdFJNkVnjWvuXLub9tgR+X6/z5z0kl68eu3XqlRx4hFs1E9fsgpD8H/K/2gb+fQguKFPx/fVxi8MvLrEyT6/Ap37utR4s5SkB2EvieJtk5zFpSMcTp44tCHblucisd4c9ws+/gSNzzWOsz/CarPThXRexqnu/FMdsNfh32b7/fq1h6sj0NgCnJlbLemOWReDramLCWMiWLGMvVkJ+Z4x21ReW2k1xcQo54f0NsUY2bCETrOmnD3g246VynqhpF1yLZevZGe02rqOubBX+vQDaN65IA6fczg2COYoOvgOdkrcCnlRLHwWw0croXPnZj7z/yocYbbxJjJCEecJ1W64qISouXubU="),
};
private final DocumentManager documentManager;
public OpenTestCheckDocumentProvider(@NonNull DocumentManager documentManager) {
this.documentManager = documentManager;
}
@Override
public Single<Boolean> canParse(@NonNull String encodedData) {
......@@ -60,6 +55,11 @@ public class OpenTestCheckDocumentProvider extends DocumentProvider<OpenTestChec
public Single<OpenTestCheckDocument> parse(@NonNull String encodedData) {
return getEncodedJwtWithoutUrl(encodedData)
.map(OpenTestCheckDocument::new)
.flatMap(openTestCheckDocument -> getProviderName(openTestCheckDocument)
.map(providerName -> {
openTestCheckDocument.getDocument().setProvider(providerName);
return openTestCheckDocument;
}))
.onErrorResumeNext(throwable -> Single.error(new DocumentParsingException(throwable)));
}
......@@ -70,13 +70,15 @@ public class OpenTestCheckDocumentProvider extends DocumentProvider<OpenTestChec
@Override
public Completable verify(@NonNull String encodedData) {
return Maybe.mergeDelayError(getPublicKeys()
.map(publicKey -> getEncodedJwtWithoutUrl(encodedData)
.flatMapMaybe(jwt -> verifyJwt(jwt, publicKey)
.andThen(Maybe.just(publicKey))
.onErrorResumeWith(Maybe.empty())))
return Maybe.mergeDelayError(getDocumentProviderData(encodedData)
.flatMapSingle(documentProviderData -> decodePublicKey(documentProviderData.getPublicKey())
.map(publicKey -> getEncodedJwtWithoutUrl(encodedData)
.flatMapMaybe(jwt -> verifyJwt(jwt, publicKey)
.andThen(Maybe.just(documentProviderData))
.onErrorResumeWith(Maybe.empty()))))
.toFlowable(BackpressureStrategy.BUFFER))
.firstOrError()
.doOnSuccess(documentProviderData -> Timber.d("Verified using public key from %s", documentProviderData.getName()))
.ignoreElement()
.onErrorResumeNext(throwable -> Completable.error(new DocumentVerificationException(INVALID_SIGNATURE, throwable)));
}
......@@ -117,9 +119,30 @@ public class OpenTestCheckDocumentProvider extends DocumentProvider<OpenTestChec
});
}
private Observable<RSAPublicKey> getPublicKeys() {
return Observable.fromArray(PUBLIC_KEYS)
.map(OpenTestCheckPublicKey::getPublicKey);
private Single<String> getProviderName(@NonNull OpenTestCheckDocument openTestCheckDocument) {
return Maybe.defer(() -> {
if (TextUtils.isEmpty(openTestCheckDocument.f)) {
return Maybe.empty();
}
return documentManager.restoreDocumentProviderDataListIfAvailable()
.flatMapObservable(Observable::fromIterable)
.filter(documentProviderData -> documentProviderData.getFingerprint().equals(openTestCheckDocument.f))
.firstElement()
.map(DocumentProviderData::getName);
}).defaultIfEmpty(openTestCheckDocument.l).onErrorReturnItem(openTestCheckDocument.l);
}
private Observable<DocumentProviderData> getDocumentProviderData(@NonNull String encodedJwt) {
return Single.fromCallable(() -> OpenTestCheckDocument.getFingerprint(encodedJwt))
.flatMapObservable(documentManager::getDocumentProviderData);
}
private static Single<RSAPublicKey> decodePublicKey(@NonNull String encodedPublicKey) {
return Single.fromCallable(() -> {
byte[] encoded = Base64.decode(encodedPublicKey, Base64.NO_WRAP);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(encoded));