From 8f03091d528904e1680d97d182c66075bfa84e04 Mon Sep 17 00:00:00 2001 From: Chilja Gossow <49635654+chiljamgossow@users.noreply.github.com> Date: Thu, 17 Jun 2021 13:44:17 +0200 Subject: [PATCH] Dcc QR code extractor (incl. recovery) (EXPOSUREAPP-7676) (#3444) * interface recovery * interface recovery * merge dcc extractors * fix tests * detekt * tests and error handling * detekt * fix faq exception * fix faq exception * remove todo * klint * clean up * clean up * use dedicated class for each type * klint * fix tests * fix tests * fix comment * change naming * change naming * klint * simplify * comments --- .../ui/CovidCertificateDetailsFragmentTest.kt | 2 - .../bugreporting/BugReportingSharedModule.kt | 4 +- ...cateQrCodeCensor.kt => DccQrCodeCensor.kt} | 29 +-- .../qrcode/CoronaTestQRCodeValidator.kt | 10 +- .../coronatest/qrcode/PcrQrCodeExtractor.kt | 2 +- .../qrcode/RapidAntigenQrCodeExtractor.kt | 2 +- .../CertificatePersonIdentifier.kt | 10 +- .../common/certificate/Dcc.kt | 75 ------ .../common/certificate/DccData.kt | 2 +- .../common/certificate/DccQrCodeExtractor.kt | 225 ++++++++++++++++++ .../common/certificate/DccV1.kt | 195 +++++++++++++++ .../common/certificate/DccV1Parser.kt | 132 ++++++++++ .../InvalidHealthCertificateException.kt | 38 ++- .../InvalidRecoveryCertificateException.kt | 14 ++ .../InvalidTestCertificateException.kt | 4 +- .../InvalidVaccinationCertificateException.kt | 30 +-- .../common/qrcode/DccQrCode.kt | 7 +- .../person/core/PersonCertificatesProvider.kt | 4 +- .../core/RecoveryCertificateRepository.kt | 4 +- .../core/certificate/RecoveryDccParser.kt | 14 -- .../core/certificate/RecoveryDccV1.kt | 37 --- .../core/qrcode/RecoveryCertificateQRCode.kt | 7 +- .../RecoveryCertificateQRCodeExtractor.kt | 28 --- .../storage/RecoveryCertificateContainer.kt | 21 +- .../test/core/TestCertificate.kt | 3 +- .../test/core/TestCertificateRepository.kt | 7 +- .../test/core/certificate/TestDccParser.kt | 69 ------ .../test/core/certificate/TestDccV1.kt | 45 ---- .../test/core/qrcode/TestCertificateQRCode.kt | 7 +- .../qrcode/TestCertificateQRCodeExtractor.kt | 133 ----------- .../core/storage/TestCertificateContainer.kt | 29 ++- .../core/storage/TestCertificateProcessor.kt | 6 +- .../core/certificate/VaccinationDccV1.kt | 42 ---- .../certificate/VaccinationDccV1Parser.kt | 77 ------ .../core/qrcode/DccQrCodeValidator.kt | 30 +++ .../qrcode/VaccinationCertificateQRCode.kt | 7 +- .../core/qrcode/VaccinationQRCodeExtractor.kt | 83 ------- .../core/qrcode/VaccinationQRCodeValidator.kt | 28 --- .../core/repository/VaccinationRepository.kt | 10 +- .../storage/ContainerPostProcessor.kt | 4 +- .../storage/VaccinationContainer.kt | 23 +- .../ui/scan/VaccinationQrCodeScanFragment.kt | 8 +- .../ui/scan/VaccinationQrCodeScanViewModel.kt | 10 +- .../res/values-bg/vaccination_strings.xml | 6 +- .../res/values-de/vaccination_strings.xml | 4 +- .../res/values-en/vaccination_strings.xml | 6 +- .../res/values-pl/vaccination_strings.xml | 6 +- .../res/values-ro/vaccination_strings.xml | 6 +- .../res/values-tr/vaccination_strings.xml | 6 +- .../main/res/values/vaccination_strings.xml | 4 +- ...deCensorTest.kt => DccQrCodeCensorTest.kt} | 59 ++--- .../qrcode/CoronaTestQrCodeValidatorTest.kt | 5 +- .../qrcode/PcrQrCodeExtractorTest.kt | 12 +- .../qrcode/RapidAntigenQrCodeExtractorTest.kt | 11 +- .../common/CertificatePersonIdentifierTest.kt | 10 +- .../test/TestCertificateRepositoryTest.kt | 8 +- .../test/TestCertificateTestData.kt | 4 +- .../qrcode/TestCertificateDccParserTest.kt | 8 +- .../TestCertificateQRCodeExtractorTest.kt | 26 +- .../execution/TestCertificateProcessorTest.kt | 11 +- .../core/VaccinationQrCodeTestData.java | 2 + .../core/VaccinationTestComponent.kt | 8 +- .../vaccination/core/VaccinationTestData.kt | 110 +++++---- ...actorTest.kt => DccQrCodeExtractorTest.kt} | 86 +++++-- .../core/qrcode/DccQrCodeValidatorTest.kt | 46 ++++ .../core/qrcode/RecoveryQrCodeTestData.java | 5 + .../qrcode/VaccinationQrCodeValidatorTest.kt | 34 --- .../repository/VaccinationRepositoryTest.kt | 10 +- .../storage/VaccinationContainerTest.kt | 17 +- 69 files changed, 1055 insertions(+), 972 deletions(-) rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/{CertificateQrCodeCensor.kt => DccQrCodeCensor.kt} (82%) delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/Dcc.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccQrCodeExtractor.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1Parser.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidRecoveryCertificateException.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccParser.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccV1.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCodeExtractor.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccParser.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccV1.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractor.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidator.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/{CertificateQrCodeCensorTest.kt => DccQrCodeCensorTest.kt} (65%) rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/{VaccinationQRCodeExtractorTest.kt => DccQrCodeExtractorTest.kt} (76%) create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidatorTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/RecoveryQrCodeTestData.java delete mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt index 5411c5a5d..124ba8e74 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt @@ -98,8 +98,6 @@ class VaccinationDetailsFragmentTest : BaseUITest() { get() = "Xup" override val sampleCollectedAt: Instant get() = testDate - override val testResultAt: Instant - get() = testDate override val testCenter: String get() = "AB123" override val registeredAt: Instant diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt index 5d9d4d09f..b78711347 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt @@ -17,7 +17,7 @@ import de.rki.coronawarnapp.bugreporting.censors.submission.PcrTeleTanCensor import de.rki.coronawarnapp.bugreporting.censors.submission.RACoronaTestCensor import de.rki.coronawarnapp.bugreporting.censors.submission.RatProfileCensor import de.rki.coronawarnapp.bugreporting.censors.submission.RatQrCodeCensor -import de.rki.coronawarnapp.bugreporting.censors.vaccination.CertificateQrCodeCensor +import de.rki.coronawarnapp.bugreporting.censors.vaccination.DccQrCodeCensor import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebugLoggerScope import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadApiV1 @@ -126,5 +126,5 @@ class BugReportingSharedModule { @Provides @IntoSet - fun certificateQrCodeCensor(censor: CertificateQrCodeCensor): BugCensor = censor + fun certificateQrCodeCensor(censor: DccQrCodeCensor): BugCensor = censor } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensor.kt similarity index 82% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensor.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensor.kt index 8ea9f61d7..d97b5e1ec 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensor.kt @@ -3,14 +3,14 @@ package de.rki.coronawarnapp.bugreporting.censors.vaccination import dagger.Reusable import de.rki.coronawarnapp.bugreporting.censors.BugCensor import de.rki.coronawarnapp.bugreporting.censors.BugCensor.CensorContainer -import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData -import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1 +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1 +import de.rki.coronawarnapp.covidcertificate.common.certificate.VaccinationDccV1 import java.util.LinkedList import javax.inject.Inject @Reusable -class CertificateQrCodeCensor @Inject constructor() : BugCensor { +class DccQrCodeCensor @Inject constructor() : BugCensor { override suspend fun checkLog(message: String): CensorContainer? { var newMessage = CensorContainer(message) @@ -25,20 +25,15 @@ class CertificateQrCodeCensor @Inject constructor() : BugCensor { newMessage = newMessage.censor( dobFormatted, - "vaccinationCertificate/dateOfBirth" + "covidCertificate/dateOfBirth" ) - if (dobFormatted != dob) { - newMessage = newMessage.censor( - dob, - "vaccinationCertificate/dob" - ) - } newMessage = censorNameData(nameData, newMessage) - payload.let { data -> - newMessage = censorVaccinationData(data, newMessage) + (it.certificate as? VaccinationDccV1)?.let { + newMessage = censorVaccinationData(it.vaccination, newMessage) } + // TODO test and recovery ? } } @@ -46,7 +41,7 @@ class CertificateQrCodeCensor @Inject constructor() : BugCensor { } private fun censorVaccinationData( - vaccinationData: VaccinationDccV1.VaccinationData, + vaccinationData: DccV1.VaccinationData, message: CensorContainer ): CensorContainer { var newMessage = message @@ -101,7 +96,7 @@ class CertificateQrCodeCensor @Inject constructor() : BugCensor { return newMessage } - private fun censorNameData(nameData: Dcc.NameData, message: CensorContainer): CensorContainer { + private fun censorNameData(nameData: DccV1.NameData, message: CensorContainer): CensorContainer { var newMessage = message nameData.familyName?.let { fName -> @@ -147,8 +142,8 @@ class CertificateQrCodeCensor @Inject constructor() : BugCensor { fun clearQRCodeStringToCensor() = synchronized(qrCodeStringsToCensor) { qrCodeStringsToCensor.clear() } - private val certsToCensor = LinkedList<DccData<VaccinationDccV1>>() - fun addCertificateToCensor(cert: DccData<VaccinationDccV1>) = synchronized(certsToCensor) { + private val certsToCensor = LinkedList<DccData<out DccV1.MetaData>>() + fun addCertificateToCensor(cert: DccData<out DccV1.MetaData>) = synchronized(certsToCensor) { certsToCensor.apply { if (contains(cert)) return@apply addFirst(cert) @@ -159,6 +154,6 @@ class CertificateQrCodeCensor @Inject constructor() : BugCensor { fun clearCertificateToCensor() = synchronized(certsToCensor) { certsToCensor.clear() } - private const val PLACEHOLDER = "########-####-####-####-########" + private const val PLACEHOLDER = "###" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt index 4f12913e4..7b31a9773 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt @@ -13,7 +13,7 @@ class CoronaTestQrCodeValidator @Inject constructor( fun validate(rawString: String): CoronaTestQRCode { return findExtractor(rawString) - ?.extract(rawString, mode = QrCodeExtractor.Mode.TEST_STRICT) + ?.extract(rawString) ?.also { Timber.i("Extracted data from QR code is %s", it) } ?: throw InvalidQRCodeException() } @@ -25,11 +25,5 @@ class CoronaTestQrCodeValidator @Inject constructor( interface QrCodeExtractor<T> { fun canHandle(rawString: String): Boolean - fun extract(rawString: String, mode: Mode): T - - enum class Mode { - TEST_STRICT, - CERT_VAC_STRICT, - CERT_VAC_LENIENT - } + fun extract(rawString: String): T } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt index c94eb873f..49633e3de 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt @@ -8,7 +8,7 @@ class PcrQrCodeExtractor @Inject constructor() : QrCodeExtractor<CoronaTestQRCod override fun canHandle(rawString: String): Boolean = rawString.startsWith(prefix, ignoreCase = true) - override fun extract(rawString: String, mode: QrCodeExtractor.Mode): CoronaTestQRCode.PCR { + override fun extract(rawString: String): CoronaTestQRCode.PCR { val guid = extractGUID(rawString) PcrQrCodeCensor.lastGUID = guid return CoronaTestQRCode.PCR(guid) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt index 9175decb7..2265512e0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt @@ -19,7 +19,7 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona return rawString.startsWith(PREFIX1, ignoreCase = true) || rawString.startsWith(PREFIX2, ignoreCase = true) } - override fun extract(rawString: String, mode: QrCodeExtractor.Mode): CoronaTestQRCode.RapidAntigen { + override fun extract(rawString: String): CoronaTestQRCode.RapidAntigen { Timber.v("extract(rawString=%s)", rawString) val payload = CleanPayload(extractData(rawString)) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/CertificatePersonIdentifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/CertificatePersonIdentifier.kt index 1e71889eb..440f44388 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/CertificatePersonIdentifier.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/CertificatePersonIdentifier.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.covidcertificate.common.certificate -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.DOB_MISMATCH +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NAME_MISMATCH import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.util.HashExtensions.toSHA256 import org.joda.time.LocalDate @@ -34,15 +34,15 @@ data class CertificatePersonIdentifier( fun requireMatch(other: CertificatePersonIdentifier) { if (lastNameStandardized != other.lastNameStandardized) { Timber.d("Family name does not match, got ${other.lastNameStandardized}, expected $lastNameStandardized") - throw InvalidVaccinationCertificateException(VC_NAME_MISMATCH) + throw InvalidVaccinationCertificateException(NAME_MISMATCH) } if (firstNameStandardized != other.firstNameStandardized) { Timber.d("Given name does not match, got ${other.firstNameStandardized}, expected $firstNameStandardized") - throw InvalidVaccinationCertificateException(VC_NAME_MISMATCH) + throw InvalidVaccinationCertificateException(NAME_MISMATCH) } if (dateOfBirth != other.dateOfBirth) { Timber.d("Date of birth does not match, got ${other.dateOfBirth}, expected $dateOfBirth") - throw InvalidVaccinationCertificateException(VC_DOB_MISMATCH) + throw InvalidVaccinationCertificateException(DOB_MISMATCH) } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/Dcc.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/Dcc.kt deleted file mode 100644 index 52d13dd08..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/Dcc.kt +++ /dev/null @@ -1,75 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.common.certificate - -import com.google.gson.annotations.SerializedName -import org.joda.time.DateTime -import org.joda.time.LocalDate -import org.joda.time.format.DateTimeFormat -import org.joda.time.format.DateTimeFormatterBuilder -import org.joda.time.format.ISODateTimeFormat -import timber.log.Timber - -abstract class Dcc<PayloadType : Dcc.Payload> { - data class NameData( - @SerializedName("fn") internal val familyName: String?, - @SerializedName("fnt") internal val familyNameStandardized: String, - @SerializedName("gn") internal val givenName: String?, - @SerializedName("gnt") internal val givenNameStandardized: String?, - ) { - val firstName: String? - get() = if (givenName.isNullOrBlank()) givenNameStandardized else givenName - - val lastName: String - get() = if (familyName.isNullOrBlank()) familyNameStandardized else familyName - - val fullName: String - get() = when { - firstName.isNullOrBlank() -> lastName - else -> "$firstName $lastName" - } - } - - abstract val version: String - abstract val nameData: NameData - abstract val dob: String - - // Can't use lazy because GSON will NULL it, as we have no no-args constructor - private var dateOfBirthCache: LocalDate? = null - val dateOfBirth: LocalDate - get() = dateOfBirthCache ?: dob.toLocalDateLeniently().also { dateOfBirthCache = it } - - abstract val payloads: List<PayloadType> - val payload: PayloadType - get() = payloads.single() - - val personIdentifier: CertificatePersonIdentifier - get() = CertificatePersonIdentifier( - dateOfBirth = dateOfBirth, - lastNameStandardized = nameData.familyNameStandardized, - firstNameStandardized = nameData.givenNameStandardized - ) - - interface Payload { - val targetId: String - val certificateCountry: String - val certificateIssuer: String - val uniqueCertificateIdentifier: String - } -} - -internal fun String.toLocalDateLeniently(): LocalDate = try { - LocalDate.parse(this, DateTimeFormat.forPattern("yyyy-MM-dd")) -} catch (e: Exception) { - Timber.w("Irregular date string: %s", this) - try { - DateTime.parse( - this, - DateTimeFormatterBuilder() - .append(ISODateTimeFormat.date()) - .append(ISODateTimeFormat.timeParser().withOffsetParsed()) - .toFormatter() - ).toLocalDate() - } catch (giveUp: Exception) { - Timber.e("Invalid date string: %s", this) - throw giveUp - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccData.kt index 89a2aac6b..e0e83438d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccData.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.covidcertificate.common.certificate -data class DccData<CertT : Dcc<*>>( +data class DccData<CertT : DccV1.MetaData>( val header: DccHeader, val certificate: CertT, ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccQrCodeExtractor.kt new file mode 100644 index 000000000..03d32a9a3 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccQrCodeExtractor.kt @@ -0,0 +1,225 @@ +package de.rki.coronawarnapp.covidcertificate.common.certificate + +import de.rki.coronawarnapp.bugreporting.censors.vaccination.DccQrCodeCensor +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode.CERT_REC_STRICT +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode.CERT_SINGLE_STRICT +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode.CERT_TEST_STRICT +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode.CERT_VAC_LENIENT +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode.CERT_VAC_STRICT +import de.rki.coronawarnapp.covidcertificate.common.decoder.DccCoseDecoder +import de.rki.coronawarnapp.covidcertificate.common.decoder.DccHeaderParser +import de.rki.coronawarnapp.covidcertificate.common.decoder.RawCOSEObject +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_ENCODING_FAILED +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_COMPRESSION_FAILED +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_RECOVERY_ENTRY +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_VACCINATION_ENTRY +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidRecoveryCertificateException +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException +import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode +import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode +import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode +import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode +import de.rki.coronawarnapp.util.compression.deflate +import de.rki.coronawarnapp.util.compression.inflate +import de.rki.coronawarnapp.util.encoding.Base45Decoder +import timber.log.Timber +import javax.inject.Inject + +class DccQrCodeExtractor @Inject constructor( + private val coseDecoder: DccCoseDecoder, + private val headerParser: DccHeaderParser, + private val bodyParser: DccV1Parser, +) : QrCodeExtractor<DccQrCode> { + + override fun canHandle(rawString: String): Boolean = rawString.startsWith(PREFIX) + + /** + * May throw an **[InvalidHealthCertificateException]** + */ + override fun extract(rawString: String): DccQrCode = extract(rawString, CERT_SINGLE_STRICT) + + /** + * May throw an **[InvalidHealthCertificateException]** + */ + fun extractEncrypted( + decryptionKey: ByteArray, + rawCoseObjectEncrypted: ByteArray, + ): DccQrCode { + val qrCodeString = rawCoseObjectEncrypted.decrypt(decryptionKey).encode() + return extract(qrCodeString) + } + + /** + * May throw an **[InvalidHealthCertificateException]** + */ + fun extract(rawString: String, mode: DccV1Parser.Mode): DccQrCode { + DccQrCodeCensor.addQRCodeStringToCensor(rawString) + + return try { + val parsedData = rawString + .removePrefix(PREFIX) + .decodeBase45() + .decompress() + .parse(mode) + + toDccQrCode(rawString, parsedData).also { + when (mode) { + CERT_VAC_STRICT, CERT_VAC_LENIENT -> if (it !is VaccinationCertificateQRCode) + throw InvalidVaccinationCertificateException(NO_VACCINATION_ENTRY) + CERT_REC_STRICT -> if (it !is RecoveryCertificateQRCode) + throw InvalidRecoveryCertificateException(NO_RECOVERY_ENTRY) + CERT_TEST_STRICT -> if (it !is TestCertificateQRCode) + throw InvalidTestCertificateException(NO_TEST_ENTRY) + else -> { /*anything goes*/ + } + } + } + } catch (e: InvalidHealthCertificateException) { + when (mode) { + CERT_VAC_STRICT, CERT_VAC_LENIENT -> + throw InvalidVaccinationCertificateException(e.errorCode) + CERT_REC_STRICT -> + throw InvalidRecoveryCertificateException(e.errorCode) + CERT_TEST_STRICT -> + throw InvalidTestCertificateException(e.errorCode) + CERT_SINGLE_STRICT -> throw e + } + } + } + + private fun RawCOSEObject.decrypt(decryptionKey: ByteArray): RawCOSEObject = try { + coseDecoder.decryptMessage( + input = this, + decryptionKey = decryptionKey + ) + } catch (e: InvalidHealthCertificateException) { + throw e + } catch (e: Throwable) { + Timber.e(e, HC_COSE_MESSAGE_INVALID.toString()) + throw InvalidHealthCertificateException(HC_COSE_MESSAGE_INVALID) + } + + private fun RawCOSEObject.encode(): String { + return PREFIX + compress().encodeBase45() + } + + private fun ByteArray.encodeBase45(): String = try { + Base45Decoder.encode(this) + } catch (e: Throwable) { + Timber.e(e, HC_BASE45_ENCODING_FAILED.toString()) + throw InvalidHealthCertificateException(HC_BASE45_ENCODING_FAILED) + } + + private fun RawCOSEObject.compress(): ByteArray = try { + this.deflate() + } catch (e: Throwable) { + Timber.e(e, HC_ZLIB_COMPRESSION_FAILED.toString()) + throw InvalidTestCertificateException(HC_ZLIB_COMPRESSION_FAILED) + } + + private fun toDccQrCode(rawString: String, parsedData: DccData<DccV1.MetaData>): DccQrCode = + when (parsedData.certificate) { + is VaccinationDccV1 -> VaccinationCertificateQRCode( + qrCode = rawString, + data = DccData( + parsedData.header, + parsedData.certificate + ), + ) + is TestDccV1 -> TestCertificateQRCode( + qrCode = rawString, + data = DccData( + parsedData.header, + parsedData.certificate + ), + ) + is RecoveryDccV1 -> RecoveryCertificateQRCode( + qrCode = rawString, + data = DccData( + parsedData.header, + parsedData.certificate + ), + ) + else -> throw InvalidHealthCertificateException(JSON_SCHEMA_INVALID) + } + + private fun String.decodeBase45(): ByteArray = try { + Base45Decoder.decode(this) + } catch (e: Throwable) { + Timber.e(e) + throw InvalidHealthCertificateException(HC_BASE45_DECODING_FAILED) + } + + private fun ByteArray.decompress(): RawCOSEObject = try { + this.inflate(sizeLimit = DEFAULT_SIZE_LIMIT) + } catch (e: Throwable) { + Timber.e(e) + throw InvalidHealthCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) + } + + fun RawCOSEObject.parse(mode: DccV1Parser.Mode): DccData<DccV1.MetaData> = try { + Timber.v("Parsing COSE for covid certificate.") + val cbor = coseDecoder.decode(this) + DccData( + header = headerParser.parse(cbor), + certificate = bodyParser.parse(cbor, mode).toCertificate + ).also { + DccQrCodeCensor.addCertificateToCensor(it) + }.also { + Timber.v("Parsed covid certificate for %s", it.certificate.nameData.familyNameStandardized) + } + } catch (e: InvalidHealthCertificateException) { + throw e + } catch (e: Throwable) { + Timber.e(e) + throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED) + } + + private val DccV1.isVaccinationCertificate: Boolean + get() = this.vaccinations?.isNotEmpty() == true + + private val DccV1.isTestCertificate: Boolean + get() = this.tests?.isNotEmpty() == true + + private val DccV1.isRecoveryCertificate: Boolean + get() = this.recoveries?.isNotEmpty() == true + + private val DccV1.toCertificate: DccV1.MetaData + get() = when { + isVaccinationCertificate -> VaccinationDccV1( + version = version, + nameData = nameData, + dateOfBirth = dateOfBirth, + personIdentifier = personIdentifier, + vaccination = vaccinations!!.first() + ) + isTestCertificate -> TestDccV1( + version = version, + nameData = nameData, + dateOfBirth = dateOfBirth, + personIdentifier = personIdentifier, + test = tests!!.first() + ) + isRecoveryCertificate -> RecoveryDccV1( + version = version, + nameData = nameData, + dateOfBirth = dateOfBirth, + personIdentifier = personIdentifier, + recovery = recoveries!!.first() + ) + else -> throw InvalidHealthCertificateException(JSON_SCHEMA_INVALID) + } +} + +private const val PREFIX = "HC1:" +// Zip bomb +private const val DEFAULT_SIZE_LIMIT = 1024L * 1024 * 10L // 10 MB diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1.kt new file mode 100644 index 000000000..8752cfda2 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1.kt @@ -0,0 +1,195 @@ +package de.rki.coronawarnapp.covidcertificate.common.certificate + +import com.google.gson.annotations.SerializedName +import org.joda.time.DateTime +import org.joda.time.Instant +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat +import org.joda.time.format.DateTimeFormatterBuilder +import org.joda.time.format.ISODateTimeFormat +import timber.log.Timber + +data class DccV1( + @SerializedName("ver") val version: String, + @SerializedName("nam") val nameData: NameData, + @SerializedName("dob") val dob: String, + @SerializedName("v") val vaccinations: List<VaccinationData>? = null, + @SerializedName("t") val tests: List<TestCertificateData>? = null, + @SerializedName("r") val recoveries: List<RecoveryCertificateData>? = null, +) { + data class NameData( + @SerializedName("fn") internal val familyName: String?, + @SerializedName("fnt") internal val familyNameStandardized: String, + @SerializedName("gn") internal val givenName: String?, + @SerializedName("gnt") internal val givenNameStandardized: String?, + ) { + val firstName: String? + get() = if (givenName.isNullOrBlank()) givenNameStandardized else givenName + + val lastName: String + get() = if (familyName.isNullOrBlank()) familyNameStandardized else familyName + + val fullName: String + get() = when { + firstName.isNullOrBlank() -> lastName + else -> "$firstName $lastName" + } + } + + // Can't use lazy because GSON will NULL it, as we have no no-args constructor + private var dateOfBirthCache: LocalDate? = null + val dateOfBirth: LocalDate + get() = dateOfBirthCache ?: dob.toLocalDateLeniently().also { dateOfBirthCache = it } + + val personIdentifier: CertificatePersonIdentifier + get() = CertificatePersonIdentifier( + dateOfBirth = dateOfBirth, + lastNameStandardized = nameData.familyNameStandardized, + firstNameStandardized = nameData.givenNameStandardized + ) + + interface MetaData { + val version: String + val nameData: NameData + val dateOfBirth: LocalDate + val payload: Payload + val personIdentifier: CertificatePersonIdentifier + } + + interface Payload { + val targetId: String + val certificateCountry: String + val certificateIssuer: String + val uniqueCertificateIdentifier: String + } + + data class RecoveryCertificateData( + // Disease or agent targeted, e.g. "tg": "840539006" + @SerializedName("tg") override val targetId: String, + // Date of First Positive NAA Test Result (required) e.g. "2021-04-21" + @SerializedName("fr") val fr: String, + // Certificate Valid From (required) e.g. "2021-05-01" + @SerializedName("df") val df: String, + // Certificate Valid Until (required) e.g. "2021-10-21" + @SerializedName("du") val du: String, + // Country of Test (required) + @SerializedName("co") override val certificateCountry: String, + // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport", + @SerializedName("is") override val certificateIssuer: String, + // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" + @SerializedName("ci") override val uniqueCertificateIdentifier: String + ) : Payload { + val testedPositiveOn: LocalDate + get() = LocalDate.parse(fr) + val validFrom: LocalDate + get() = LocalDate.parse(df) + val validUntil: LocalDate + get() = LocalDate.parse(du) + } + + data class VaccinationData( + // Disease or agent targeted, e.g. "tg": "840539006" + @SerializedName("tg") override val targetId: String, + // Vaccine or prophylaxis, e.g. "vp": "1119349007" + @SerializedName("vp") val vaccineId: String, + // Vaccine medicinal product,e.g. "mp": "EU/1/20/1528", + @SerializedName("mp") val medicalProductId: String, + // Marketing Authorization Holder, e.g. "ma": "ORG-100030215", + @SerializedName("ma") val marketAuthorizationHolderId: String, + // Dose Number, e.g. "dn": 2 + @SerializedName("dn") val doseNumber: Int, + // Total Series of Doses, e.g. "sd": 2, + @SerializedName("sd") val totalSeriesOfDoses: Int, + // Date of Vaccination, e.g. "dt" : "2021-04-21" + @SerializedName("dt") val dt: String, + // Country of Vaccination, e.g. "co": "NL" + @SerializedName("co") override val certificateCountry: String, + // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport", + @SerializedName("is") override val certificateIssuer: String, + // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" + @SerializedName("ci") override val uniqueCertificateIdentifier: String + ) : Payload { + // Can't use lazy because GSON will NULL it, as we have no no-args constructor + private var vaccinatedAtCache: LocalDate? = null + val vaccinatedAt: LocalDate + get() = vaccinatedAtCache ?: dt.toLocalDateLeniently().also { vaccinatedAtCache = it } + } + + data class TestCertificateData( + // Disease or agent targeted, e.g. "tg": "840539006" + @SerializedName("tg") override val targetId: String, + // Type of Test (required) eg "LP217198-3" + @SerializedName("tt") val testType: String, + // Test Result (required) e. g. "tr": "260415000" + @SerializedName("tr") val testResult: String, + // NAA Test Name (only for PCR tests, but not required) "nm": "Roche LightCycler qPCR", + @SerializedName("nm") val testName: String? = null, + // RAT Test name and manufacturer (only for RAT tests, but not required) + @SerializedName("ma") val testNameAndManufactor: String? = null, + // Date/Time of Sample Collection (required) "sc": "2021-04-13T14:20:00+00:00" + @SerializedName("sc") val sc: String, + // Testing Center (required) "tc": "GGD Fryslân, L-Heliconweg", + @SerializedName("tc") val testCenter: String?, + // Country of Test (required) + @SerializedName("co") override val certificateCountry: String, + // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport", + @SerializedName("is") override val certificateIssuer: String, + // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" + @SerializedName("ci") override val uniqueCertificateIdentifier: String + ) : Payload { + + val sampleCollectedAt: Instant + get() = Instant.parse(sc) + } +} + +internal fun String.toLocalDateLeniently(): LocalDate = try { + LocalDate.parse(this, DateTimeFormat.forPattern("yyyy-MM-dd")) +} catch (e: Exception) { + Timber.w("Irregular date string: %s", this) + try { + DateTime.parse( + this, + DateTimeFormatterBuilder() + .append(ISODateTimeFormat.date()) + .append(ISODateTimeFormat.timeParser().withOffsetParsed()) + .toFormatter() + ).toLocalDate() + } catch (giveUp: Exception) { + Timber.e("Invalid date string: %s", this) + throw giveUp + } +} + +data class VaccinationDccV1( + override val version: String, + override val nameData: DccV1.NameData, + override val dateOfBirth: LocalDate, + override val personIdentifier: CertificatePersonIdentifier, + val vaccination: DccV1.VaccinationData +) : DccV1.MetaData { + override val payload: DccV1.Payload + get() = vaccination +} + +data class TestDccV1( + override val version: String, + override val nameData: DccV1.NameData, + override val dateOfBirth: LocalDate, + override val personIdentifier: CertificatePersonIdentifier, + val test: DccV1.TestCertificateData +) : DccV1.MetaData { + override val payload: DccV1.Payload + get() = test +} + +data class RecoveryDccV1( + override val version: String, + override val nameData: DccV1.NameData, + override val dateOfBirth: LocalDate, + override val personIdentifier: CertificatePersonIdentifier, + val recovery: DccV1.RecoveryCertificateData +) : DccV1.MetaData { + override val payload: DccV1.Payload + get() = recovery +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1Parser.kt new file mode 100644 index 000000000..702d1ef93 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1Parser.kt @@ -0,0 +1,132 @@ +package de.rki.coronawarnapp.covidcertificate.common.certificate + +import com.google.gson.Gson +import com.upokecenter.cbor.CBORObject +import dagger.Reusable +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException +import de.rki.coronawarnapp.util.serialization.BaseGson +import de.rki.coronawarnapp.util.serialization.fromJson +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class DccV1Parser @Inject constructor( + @BaseGson private val gson: Gson +) { + fun parse(map: CBORObject, mode: Mode): DccV1 = try { + map[keyHCert]?.run { + this[keyEuDgcV1]?.run { + this.toCertificate().toValidated(mode) + } ?: throw InvalidVaccinationCertificateException(ErrorCode.HC_CWT_NO_DGC) + } ?: throw InvalidVaccinationCertificateException(ErrorCode.HC_CWT_NO_HCERT) + } catch (e: InvalidHealthCertificateException) { + throw e + } catch (e: Throwable) { + throw InvalidHealthCertificateException(ErrorCode.HC_CBOR_DECODING_FAILED, cause = e) + } + + private fun CBORObject.toCertificate(): DccV1 = try { + val json = ToJSONString() + gson.fromJson(json) + } catch (e: InvalidHealthCertificateException) { + throw e + } catch (e: Throwable) { + throw InvalidHealthCertificateException(ErrorCode.JSON_SCHEMA_INVALID) + } + + private fun DccV1.toValidated(mode: Mode): DccV1 = try { + checkModeRestrictions(mode) + .apply { + // Apply otherwise we risk accidentally accessing the original obj in the outer scope + require(isSingleCertificate()) + checkFields() + } + } catch (e: InvalidHealthCertificateException) { + throw e + } catch (e: Throwable) { + throw InvalidHealthCertificateException(ErrorCode.JSON_SCHEMA_INVALID) + } + + private fun DccV1.checkModeRestrictions(mode: Mode) = when (mode) { + Mode.CERT_VAC_STRICT -> + if (vaccinations?.size != 1) + throw InvalidVaccinationCertificateException( + if (vaccinations.isNullOrEmpty()) ErrorCode.NO_VACCINATION_ENTRY + else ErrorCode.MULTIPLE_VACCINATION_ENTRIES + ) + else this + Mode.CERT_VAC_LENIENT -> { + if (vaccinations.isNullOrEmpty()) + throw InvalidVaccinationCertificateException(ErrorCode.NO_VACCINATION_ENTRY) + Timber.w("Lenient: Vaccination data contained multiple entries.") + copy(vaccinations = listOf(vaccinations.maxByOrNull { it.vaccinatedAt }!!)) + } + Mode.CERT_REC_STRICT -> + if (recoveries?.size != 1) + throw InvalidVaccinationCertificateException( + if (recoveries.isNullOrEmpty()) ErrorCode.NO_RECOVERY_ENTRY + else ErrorCode.MULTIPLE_RECOVERY_ENTRIES + ) + else this + Mode.CERT_TEST_STRICT -> + if (tests?.size != 1) + throw InvalidVaccinationCertificateException( + if (tests.isNullOrEmpty()) ErrorCode.NO_TEST_ENTRY + else ErrorCode.MULTIPLE_TEST_ENTRIES + ) + else this + else -> this + } + + private fun DccV1.isSingleCertificate(): Boolean { + return (vaccinations?.size ?: 0) + (tests?.size ?: 0) + (recoveries?.size ?: 0) == 1 + } + + private fun DccV1.checkFields() { + // check for non null (Gson does not enforce it) + not blank & force date parsing + require(version.isNotBlank()) + require(nameData.familyNameStandardized.isNotBlank()) + dateOfBirth + vaccinations?.forEach { + it.vaccinatedAt + require(it.certificateIssuer.isNotBlank()) + require(it.certificateCountry.isNotBlank()) + require(it.marketAuthorizationHolderId.isNotBlank()) + require(it.medicalProductId.isNotBlank()) + require(it.targetId.isNotBlank()) + require(it.doseNumber > 0) + require(it.totalSeriesOfDoses > 0) + } + tests?.forEach { + it.sampleCollectedAt + require(it.certificateIssuer.isNotBlank()) + require(it.certificateCountry.isNotBlank()) + require(it.targetId.isNotBlank()) + require(it.testResult.isNotBlank()) + require(it.testType.isNotBlank()) + } + recoveries?.forEach { + it.testedPositiveOn + it.validFrom + it.validUntil + require(it.certificateIssuer.isNotBlank()) + require(it.certificateCountry.isNotBlank()) + require(it.targetId.isNotBlank()) + } + } + + enum class Mode { + CERT_VAC_STRICT, // exactly one vaccination certificate allowed + CERT_VAC_LENIENT, // multiple vaccination certificates allowed + CERT_REC_STRICT, // exactly one recovery certificate allowed + CERT_TEST_STRICT, // exactly one test certificate allowed + CERT_SINGLE_STRICT; // exactly one certificate allowed + } + + companion object { + private val keyEuDgcV1 = CBORObject.FromObject(1) + private val keyHCert = CBORObject.FromObject(-260) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt index 401f396e2..d1c774e5b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.covidcertificate.common.exception import android.content.Context +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException import de.rki.coronawarnapp.util.HasHumanReadableError import de.rki.coronawarnapp.util.HumanReadableError @@ -22,15 +23,18 @@ open class InvalidHealthCertificateException( HC_COSE_TAG_INVALID("COSE tag invalid."), HC_COSE_MESSAGE_INVALID("COSE message invalid."), HC_CBOR_DECODING_FAILED("CBOR decoding failed."), - VC_NO_VACCINATION_ENTRY("Vaccination certificate missing."), - VC_MULTIPLE_VACCINATION_ENTRIES("Multiple vaccination certificates."), + NO_VACCINATION_ENTRY("Vaccination certificate missing."), + MULTIPLE_VACCINATION_ENTRIES("Multiple vaccination certificates."), + MULTIPLE_TEST_ENTRIES("Multiple test certificates."), + MULTIPLE_RECOVERY_ENTRIES("Multiple recovery certificates."), NO_TEST_ENTRY("Test certificate missing."), - VC_PREFIX_INVALID("Prefix invalid."), - VC_STORING_FAILED("Storing failed."), + NO_RECOVERY_ENTRY("Recovery certificate missing."), + PREFIX_INVALID("Prefix invalid."), + STORING_FAILED("Storing failed."), JSON_SCHEMA_INVALID("Json schema invalid."), - VC_NAME_MISMATCH("Name does not match."), - VC_ALREADY_REGISTERED("Certificate already registered."), - VC_DOB_MISMATCH("Date of birth does not match."), + NAME_MISMATCH("Name does not match."), + ALREADY_REGISTERED("Certificate already registered."), + DOB_MISMATCH("Date of birth does not match."), HC_CWT_NO_DGC("Dgc missing."), HC_CWT_NO_EXP("Expiration date missing."), HC_CWT_NO_HCERT("Health certificate missing."), @@ -41,9 +45,22 @@ open class InvalidHealthCertificateException( RSA_KP_GENERATION_FAILED("RSA key pair generation failed."), } + open val showFaqButton: Boolean = false + open val faqButtonText: Int = 0 + open val faqLink: Int = 0 + open val errorMessage: LazyString - get() = CachedString { context -> - context.getString(ERROR_MESSAGE_GENERIC) + get() = when (errorCode) { + ErrorCode.STORING_FAILED -> CachedString { context -> + context.getString(ERROR_MESSAGE_SCAN_AGAIN) + } + + ErrorCode.ALREADY_REGISTERED -> CachedString { context -> + context.getString(ERROR_MESSAGE_ALREADY_REGISTERED) + } + else -> CachedString { context -> + context.getString(ERROR_MESSAGE_GENERIC) + } } override fun toHumanReadableError(context: Context): HumanReadableError { @@ -52,3 +69,6 @@ open class InvalidHealthCertificateException( ) } } + +private const val ERROR_MESSAGE_SCAN_AGAIN = R.string.error_dcc_scan_again +private const val ERROR_MESSAGE_ALREADY_REGISTERED = R.string.error_dcc_already_registered diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidRecoveryCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidRecoveryCertificateException.kt new file mode 100644 index 000000000..2b67a1f0d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidRecoveryCertificateException.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.covidcertificate.common.exception + +import android.content.Context +import de.rki.coronawarnapp.util.HumanReadableError + +class InvalidRecoveryCertificateException(errorCode: ErrorCode) : InvalidHealthCertificateException(errorCode) { + override fun toHumanReadableError(context: Context): HumanReadableError { + return HumanReadableError( + description = errorMessage.get(context) + " ($PREFIX$errorCode)" + ) + } +} + +private const val PREFIX = "RC_" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidTestCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidTestCertificateException.kt index 9bdfc992a..7ffb6f2ee 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidTestCertificateException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidTestCertificateException.kt @@ -8,7 +8,7 @@ import de.rki.coronawarnapp.util.ui.LazyString class InvalidTestCertificateException(errorCode: ErrorCode) : InvalidHealthCertificateException(errorCode) { override fun toHumanReadableError(context: Context): HumanReadableError { return HumanReadableError( - description = errorMessage.get(context) + " ($errorCode)" + description = errorMessage.get(context) + " ($PREFIX$errorCode)" ) } @@ -35,3 +35,5 @@ class InvalidTestCertificateException(errorCode: ErrorCode) : InvalidHealthCerti else -> super.errorMessage } } + +private const val PREFIX = "TC_" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt index 63fc7f672..0004db62f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt @@ -11,15 +11,15 @@ class InvalidVaccinationCertificateException( cause: Throwable? = null, ) : InvalidHealthCertificateException(errorCode, cause) { override fun toHumanReadableError(context: Context): HumanReadableError { - var errorCodeString = errorCode.toString() - errorCodeString = if (errorCodeString.startsWith(PREFIX_VC)) errorCodeString else PREFIX_VC + errorCodeString return HumanReadableError( - description = errorMessage.get(context) + " ($errorCodeString)" + description = errorMessage.get(context) + " ($PREFIX$errorCode)" ) } - val showFaqButton: Boolean + override val showFaqButton: Boolean get() = errorCode in codesVcInvalid + override val faqButtonText: Int = R.string.error_button_vc_faq + override val faqLink: Int = R.string.error_button_vc_faq_link private val codesVcInvalid = listOf( ErrorCode.HC_BASE45_DECODING_FAILED, @@ -27,7 +27,7 @@ class InvalidVaccinationCertificateException( ErrorCode.HC_COSE_MESSAGE_INVALID, ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED, ErrorCode.HC_COSE_TAG_INVALID, - ErrorCode.VC_PREFIX_INVALID, + ErrorCode.PREFIX_INVALID, ErrorCode.HC_CWT_NO_DGC, ErrorCode.HC_CWT_NO_EXP, ErrorCode.HC_CWT_NO_HCERT, @@ -41,34 +41,24 @@ class InvalidVaccinationCertificateException( context.getString(ERROR_MESSAGE_VC_INVALID) } - ErrorCode.VC_NO_VACCINATION_ENTRY -> CachedString { context -> + ErrorCode.NO_VACCINATION_ENTRY -> CachedString { context -> context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) } - ErrorCode.VC_MULTIPLE_VACCINATION_ENTRIES -> CachedString { context -> + ErrorCode.MULTIPLE_VACCINATION_ENTRIES -> CachedString { context -> context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) } - ErrorCode.VC_STORING_FAILED -> CachedString { context -> - context.getString(ERROR_MESSAGE_VC_SCAN_AGAIN) - } - - ErrorCode.VC_NAME_MISMATCH, - ErrorCode.VC_DOB_MISMATCH -> CachedString { context -> + ErrorCode.NAME_MISMATCH, + ErrorCode.DOB_MISMATCH -> CachedString { context -> context.getString(ERROR_MESSAGE_VC_DIFFERENT_PERSON) } - - ErrorCode.VC_ALREADY_REGISTERED -> CachedString { context -> - context.getString(ERROR_MESSAGE_VC_ALREADY_REGISTERED) - } else -> super.errorMessage } } -private const val PREFIX_VC = "VC_" +private const val PREFIX = "VC_" private const val ERROR_MESSAGE_VC_INVALID = R.string.error_vc_invalid private const val ERROR_MESSAGE_VC_NOT_YET_SUPPORTED = R.string.error_vc_not_yet_supported -private const val ERROR_MESSAGE_VC_SCAN_AGAIN = R.string.error_vc_scan_again private const val ERROR_MESSAGE_VC_DIFFERENT_PERSON = R.string.error_vc_different_person -private const val ERROR_MESSAGE_VC_ALREADY_REGISTERED = R.string.error_vc_already_registered diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/qrcode/DccQrCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/qrcode/DccQrCode.kt index 9d1527dd0..0fd67dd25 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/qrcode/DccQrCode.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/qrcode/DccQrCode.kt @@ -1,16 +1,15 @@ package de.rki.coronawarnapp.covidcertificate.common.qrcode import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier -import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1 -interface DccQrCode<DccT : Dcc<*>> { +interface DccQrCode { val qrCode: QrCodeString - val data: DccData<DccT> + val data: DccData<out DccV1.MetaData> val personIdentifier: CertificatePersonIdentifier get() = data.certificate.personIdentifier val uniqueCertificateIdentifier: String - get() = data.certificate.payload.uniqueCertificateIdentifier } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/core/PersonCertificatesProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/core/PersonCertificatesProvider.kt index acee0e0a8..f8060e58c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/core/PersonCertificatesProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/core/PersonCertificatesProvider.kt @@ -90,9 +90,7 @@ class PersonCertificatesProvider @Inject constructor( get() = "testNameAndManufacturer" override val sampleCollectedAt: Instant get() = Instant.now() - override val testResultAt: Instant? - get() = Instant.now() - override val testCenter: String + override val testCenter: String? get() = "testCenter" override val registeredAt: Instant get() = Instant.now() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt index f3e9f6ba8..4f9aab5a4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.covidcertificate.recovery.core +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode -import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCodeExtractor import de.rki.coronawarnapp.covidcertificate.recovery.core.storage.RecoveryCertificateContainer import de.rki.coronawarnapp.covidcertificate.recovery.core.storage.RecoveryCertificateIdentifier import de.rki.coronawarnapp.covidcertificate.valueset.ValueSetsRepository @@ -18,7 +18,7 @@ import javax.inject.Singleton class RecoveryCertificateRepository @Inject constructor( @AppScope private val appScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, - private val qrCodeExtractor: RecoveryCertificateQRCodeExtractor, + private val qrCodeExtractor: DccQrCodeExtractor, valueSetsRepository: ValueSetsRepository, ) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccParser.kt deleted file mode 100644 index 5118c4384..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccParser.kt +++ /dev/null @@ -1,14 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.recovery.core.certificate - -import com.google.gson.Gson -import com.upokecenter.cbor.CBORObject -import dagger.Reusable -import de.rki.coronawarnapp.util.serialization.BaseGson -import javax.inject.Inject - -@Reusable -class RecoveryDccParser @Inject constructor( - @BaseGson private val gson: Gson, -) { - fun parse(map: CBORObject): RecoveryDccV1 = throw NotImplementedError() -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccV1.kt deleted file mode 100644 index 0038ed3a7..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccV1.kt +++ /dev/null @@ -1,37 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.recovery.core.certificate - -import com.google.gson.annotations.SerializedName -import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc -import org.joda.time.LocalDate - -data class RecoveryDccV1( - @SerializedName("ver") override val version: String, - @SerializedName("nam") override val nameData: Dcc.NameData, - @SerializedName("dob") override val dob: String, - @SerializedName("t") override val payloads: List<RecoveryCertificateData>, -) : Dcc<RecoveryDccV1.RecoveryCertificateData>() { - - data class RecoveryCertificateData( - // Disease or agent targeted, e.g. "tg": "840539006" - @SerializedName("tg") override val targetId: String, - // Date of First Positive NAA Test Result (required) e.g. "2021-04-21" - @SerializedName("fr") val fr: String, - // Certificate Valid From (required) e.g. "2021-05-01" - @SerializedName("df") val df: String, - // Certificate Valid Until (required) e.g. "2021-10-21" - @SerializedName("du") val du: String, - // Country of Test (required) - @SerializedName("co") override val certificateCountry: String, - // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport", - @SerializedName("is") override val certificateIssuer: String, - // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" - @SerializedName("ci") override val uniqueCertificateIdentifier: String - ) : Dcc.Payload { - val testedPositiveOn: LocalDate - get() = LocalDate.parse(fr) - val validFrom: LocalDate - get() = LocalDate.parse(df) - val validUntil: LocalDate - get() = LocalDate.parse(du) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCode.kt index cb9f4174d..f51885985 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCode.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCode.kt @@ -1,11 +1,14 @@ package de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData +import de.rki.coronawarnapp.covidcertificate.common.certificate.RecoveryDccV1 import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString -import de.rki.coronawarnapp.covidcertificate.recovery.core.certificate.RecoveryDccV1 data class RecoveryCertificateQRCode( override val qrCode: QrCodeString, override val data: DccData<RecoveryDccV1>, -) : DccQrCode<RecoveryDccV1> +) : DccQrCode { + override val uniqueCertificateIdentifier: String + get() = data.certificate.recovery.uniqueCertificateIdentifier +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCodeExtractor.kt deleted file mode 100644 index c8fccba45..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCodeExtractor.kt +++ /dev/null @@ -1,28 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode - -import dagger.Reusable -import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData -import de.rki.coronawarnapp.covidcertificate.common.decoder.DccCoseDecoder -import de.rki.coronawarnapp.covidcertificate.common.decoder.DccHeaderParser -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException -import de.rki.coronawarnapp.covidcertificate.recovery.core.certificate.RecoveryDccParser -import de.rki.coronawarnapp.covidcertificate.recovery.core.certificate.RecoveryDccV1 -import javax.inject.Inject - -@Reusable -class RecoveryCertificateQRCodeExtractor @Inject constructor( - private val coseDecoder: DccCoseDecoder, - private val headerParser: DccHeaderParser, - private val bodyParser: RecoveryDccParser, -) { - - /** - * May throw an **[InvalidTestCertificateException]** - */ - fun extract(qrCode: String) = RecoveryCertificateQRCode( - data = qrCode.extract(), - qrCode = qrCode - ) - - private fun String.extract(): DccData<RecoveryDccV1> = throw NotImplementedError() -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt index 65209c919..fc7326f6f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt @@ -2,10 +2,12 @@ package de.rki.coronawarnapp.covidcertificate.recovery.core.storage import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode +import de.rki.coronawarnapp.covidcertificate.common.certificate.RecoveryDccV1 import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString import de.rki.coronawarnapp.covidcertificate.recovery.core.RecoveryCertificate -import de.rki.coronawarnapp.covidcertificate.recovery.core.certificate.RecoveryDccV1 -import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCodeExtractor +import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.TestCertificateValueSets import org.joda.time.Instant import org.joda.time.LocalDate @@ -13,17 +15,24 @@ import java.util.Locale data class RecoveryCertificateContainer( internal val data: StoredRecoveryCertificateData, - private val qrCodeExtractor: RecoveryCertificateQRCodeExtractor, + private val qrCodeExtractor: DccQrCodeExtractor, val isUpdatingData: Boolean = false, ) : StoredRecoveryCertificate by data { @delegate:Transient private val certificateData: DccData<RecoveryDccV1> by lazy { - data.recoveryCertificateQrCode!!.let { qrCodeExtractor.extract(it).data } + data.recoveryCertificateQrCode!!.let { + ( + qrCodeExtractor.extract( + it, + mode = Mode.CERT_REC_STRICT + ) as RecoveryCertificateQRCode + ).data + } } val certificateId: String - get() = certificateData.certificate.payload.uniqueCertificateIdentifier + get() = certificateData.certificate.recovery.uniqueCertificateIdentifier fun toRecoveryCertificate( valueSet: TestCertificateValueSets?, @@ -31,7 +40,7 @@ data class RecoveryCertificateContainer( ): RecoveryCertificate { val header = certificateData.header val certificate = certificateData.certificate - val recoveryCertificate = certificate.payload + val recoveryCertificate = certificate.recovery return object : RecoveryCertificate { override val personIdentifier: CertificatePersonIdentifier diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificate.kt index 82da1f8c4..aa7b70f2b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificate.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificate.kt @@ -22,8 +22,7 @@ interface TestCertificate : CwaCovidCertificate { */ val testNameAndManufacturer: String? val sampleCollectedAt: Instant - val testResultAt: Instant? - val testCenter: String + val testCenter: String? val registeredAt: Instant val isUpdatingData: Boolean val isCertificateRetrievalPending: Boolean diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt index 658d7b433..955f493fb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt @@ -2,8 +2,9 @@ package de.rki.coronawarnapp.covidcertificate.test.core import de.rki.coronawarnapp.bugreporting.reportProblem import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException -import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException.ErrorCode.DCC_NOT_SUPPORTED_BY_LAB import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData import de.rki.coronawarnapp.covidcertificate.test.core.storage.RACertificateData import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateContainer @@ -36,7 +37,7 @@ class TestCertificateRepository @Inject constructor( @AppScope private val appScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, private val storage: TestCertificateStorage, - private val qrCodeExtractor: TestCertificateQRCodeExtractor, + private val qrCodeExtractor: DccQrCodeExtractor, private val processor: TestCertificateProcessor, valueSetsRepository: ValueSetsRepository, ) { @@ -187,7 +188,7 @@ class TestCertificateRepository @Inject constructor( RefreshResult( cert, TestCertificateServerException( - TestCertificateServerException.ErrorCode.DCC_NOT_SUPPORTED_BY_LAB + DCC_NOT_SUPPORTED_BY_LAB ) ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccParser.kt deleted file mode 100644 index f30135698..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccParser.kt +++ /dev/null @@ -1,69 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.test.core.certificate - -import com.google.gson.Gson -import com.upokecenter.cbor.CBORObject -import dagger.Reusable -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_DGC -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_HCERT -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException -import de.rki.coronawarnapp.util.serialization.BaseGson -import de.rki.coronawarnapp.util.serialization.fromJson -import timber.log.Timber -import javax.inject.Inject - -@Reusable -class TestDccParser @Inject constructor( - @BaseGson private val gson: Gson, -) { - fun parse(map: CBORObject): TestDccV1 = try { - map[keyHCert]?.run { - this[keyEuDgcV1]?.run { - toCertificate() - } ?: throw InvalidTestCertificateException(HC_CWT_NO_DGC) - } ?: throw InvalidTestCertificateException(HC_CWT_NO_HCERT) - } catch (e: InvalidTestCertificateException) { - throw e - } catch (e: Throwable) { - throw InvalidTestCertificateException(HC_CBOR_DECODING_FAILED) - } - - @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") - private fun TestDccV1.validate(): TestDccV1 { - if (payloads.isNullOrEmpty()) { - throw InvalidTestCertificateException(NO_TEST_ENTRY) - } - // check for non null (Gson does not enforce it) & force date parsing - require(version.isNotBlank()) - require(nameData.familyNameStandardized.isNotBlank()) - dateOfBirth - payload.let { - it.testResultAt - it.sampleCollectedAt - require(it.certificateIssuer.isNotBlank()) - require(it.certificateCountry.isNotBlank()) - require(it.targetId.isNotBlank()) - require(it.testCenter.isNotBlank()) - require(it.testResult.isNotBlank()) - require(it.testType.isNotBlank()) - } - return this - } - - private fun CBORObject.toCertificate() = try { - val json = ToJSONString() - gson.fromJson<TestDccV1>(json).validate() - } catch (e: InvalidTestCertificateException) { - throw e - } catch (e: Throwable) { - Timber.e(e) - throw InvalidTestCertificateException(JSON_SCHEMA_INVALID) - } - - companion object { - private val keyEuDgcV1 = CBORObject.FromObject(1) - private val keyHCert = CBORObject.FromObject(-260) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccV1.kt deleted file mode 100644 index 3ebb7e072..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccV1.kt +++ /dev/null @@ -1,45 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.test.core.certificate - -import com.google.gson.annotations.SerializedName -import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc -import org.joda.time.Instant - -data class TestDccV1( - @SerializedName("ver") override val version: String, - @SerializedName("nam") override val nameData: NameData, - @SerializedName("dob") override val dob: String, - @SerializedName("t") override val payloads: List<TestCertificateData>, -) : Dcc<TestDccV1.TestCertificateData>() { - - data class TestCertificateData( - // Disease or agent targeted, e.g. "tg": "840539006" - @SerializedName("tg") override val targetId: String, - // Type of Test (required) eg "LP217198-3" - @SerializedName("tt") val testType: String, - // Test Result (required) e. g. "tr": "260415000" - @SerializedName("tr") val testResult: String, - // NAA Test Name (only for PCR tests, but not required) "nm": "Roche LightCycler qPCR", - @SerializedName("nm") val testName: String? = null, - // RAT Test name and manufacturer (only for RAT tests, but not required) - @SerializedName("ma") val testNameAndManufactor: String? = null, - // Date/Time of Sample Collection (required) "sc": "2021-04-13T14:20:00+00:00" - @SerializedName("sc") val sc: String, - // Date/Time of Test Result "dr": "2021-04-13T14:40:01+00:00", - @SerializedName("dr") val dr: String? = null, - // Testing Center (required) "tc": "GGD Fryslân, L-Heliconweg", - @SerializedName("tc") val testCenter: String, - // Country of Test (required) - @SerializedName("co") override val certificateCountry: String, - // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport", - @SerializedName("is") override val certificateIssuer: String, - // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" - @SerializedName("ci") override val uniqueCertificateIdentifier: String - ) : Payload { - - val testResultAt: Instant? - get() = dr?.let { Instant.parse(it) } - - val sampleCollectedAt: Instant - get() = Instant.parse(sc) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCode.kt index 5e2063068..8fe8102b7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCode.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCode.kt @@ -1,11 +1,14 @@ package de.rki.coronawarnapp.covidcertificate.test.core.qrcode import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData +import de.rki.coronawarnapp.covidcertificate.common.certificate.TestDccV1 import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString -import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccV1 data class TestCertificateQRCode( override val qrCode: QrCodeString, override val data: DccData<TestDccV1>, -) : DccQrCode<TestDccV1> +) : DccQrCode { + override val uniqueCertificateIdentifier: String + get() = data.certificate.test.uniqueCertificateIdentifier +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractor.kt deleted file mode 100644 index d466e3636..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractor.kt +++ /dev/null @@ -1,133 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.test.core.qrcode - -import com.upokecenter.cbor.CBORObject -import dagger.Reusable -import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData -import de.rki.coronawarnapp.covidcertificate.common.decoder.DccCoseDecoder -import de.rki.coronawarnapp.covidcertificate.common.decoder.DccHeaderParser -import de.rki.coronawarnapp.covidcertificate.common.decoder.RawCOSEObject -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_ENCODING_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_COMPRESSION_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException -import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccParser -import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccV1 -import de.rki.coronawarnapp.util.compression.deflate -import de.rki.coronawarnapp.util.compression.inflate -import de.rki.coronawarnapp.util.encoding.Base45Decoder -import timber.log.Timber -import javax.inject.Inject - -@Reusable -class TestCertificateQRCodeExtractor @Inject constructor( - private val coseDecoder: DccCoseDecoder, - private val headerParser: DccHeaderParser, - private val bodyParser: TestDccParser, -) { - - /** - * May throw an **[InvalidTestCertificateException]** - */ - fun extract( - decryptionKey: ByteArray, - rawCoseObjectEncrypted: ByteArray, - ): TestCertificateQRCode { - val rawCoseObject = rawCoseObjectEncrypted.decrypt(decryptionKey) - return TestCertificateQRCode( - data = rawCoseObject.decode(), - qrCode = rawCoseObject.encode() - ) - } - - /** - * May throw an **[InvalidTestCertificateException]** - */ - fun extract(qrCode: String) = TestCertificateQRCode( - data = qrCode.extract(), - qrCode = qrCode - ) - - private fun RawCOSEObject.decrypt(decryptionKey: ByteArray): RawCOSEObject = try { - coseDecoder.decryptMessage( - input = this, - decryptionKey = decryptionKey - ) - } catch (e: InvalidHealthCertificateException) { - throw InvalidTestCertificateException(e.errorCode) - } catch (e: Throwable) { - Timber.e(e, HC_COSE_MESSAGE_INVALID.toString()) - throw InvalidTestCertificateException(HC_COSE_MESSAGE_INVALID) - } - - private fun String.extract(): DccData<TestDccV1> = - removePrefix(PREFIX) - .decodeBase45() - .decompress() - .decode() - - private fun RawCOSEObject.encode(): String { - return PREFIX + compress().encodeBase45() - } - - private fun RawCOSEObject.decode(): DccData<TestDccV1> = try { - coseDecoder.decode(this).parse() - } catch (e: InvalidHealthCertificateException) { - throw InvalidTestCertificateException(e.errorCode) - } catch (e: Throwable) { - Timber.e(e, HC_COSE_MESSAGE_INVALID.toString()) - throw InvalidTestCertificateException(HC_COSE_MESSAGE_INVALID) - } - - private fun CBORObject.parse(): DccData<TestDccV1> = try { - DccData( - header = headerParser.parse(this), - certificate = bodyParser.parse(this) - ).also { - Timber.v("Parsed test certificate for %s", it.certificate.nameData.givenNameStandardized) - } - } catch (e: InvalidHealthCertificateException) { - throw InvalidTestCertificateException(e.errorCode) - } catch (e: Throwable) { - Timber.e(e, HC_CBOR_DECODING_FAILED.toString()) - throw InvalidTestCertificateException(HC_CBOR_DECODING_FAILED) - } - - private fun String.decodeBase45(): ByteArray = try { - Base45Decoder.decode(this) - } catch (e: Throwable) { - Timber.e(e, HC_BASE45_DECODING_FAILED.toString()) - throw InvalidTestCertificateException(HC_BASE45_DECODING_FAILED) - } - - private fun ByteArray.encodeBase45(): String = try { - Base45Decoder.encode(this) - } catch (e: Throwable) { - Timber.e(e, HC_BASE45_ENCODING_FAILED.toString()) - throw InvalidTestCertificateException(HC_BASE45_ENCODING_FAILED) - } - - private fun RawCOSEObject.compress(): ByteArray = try { - this.deflate() - } catch (e: Throwable) { - Timber.e(e, HC_ZLIB_COMPRESSION_FAILED.toString()) - throw InvalidTestCertificateException(HC_ZLIB_COMPRESSION_FAILED) - } - - private fun ByteArray.decompress(): RawCOSEObject = try { - this.inflate(sizeLimit = DEFAULT_SIZE_LIMIT) - } catch (e: Throwable) { - Timber.e(e, HC_ZLIB_DECOMPRESSION_FAILED.toString()) - throw InvalidTestCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) - } - - companion object { - private const val PREFIX = "HC1:" - - // Zip bomb - private const val DEFAULT_SIZE_LIMIT = 1024L * 1024 * 10L // 10 MB - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt index 216b2e326..3afa51c3c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt @@ -1,11 +1,11 @@ package de.rki.coronawarnapp.covidcertificate.test.core.storage import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier -import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificate -import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccV1 -import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor +import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.TestCertificateValueSets import org.joda.time.Instant import org.joda.time.LocalDate @@ -13,13 +13,18 @@ import java.util.Locale data class TestCertificateContainer( internal val data: StoredTestCertificateData, - private val qrCodeExtractor: TestCertificateQRCodeExtractor, + private val qrCodeExtractor: DccQrCodeExtractor, val isUpdatingData: Boolean = false, ) : StoredTestCertificateData by data { @delegate:Transient - private val certificateData: DccData<TestDccV1> by lazy { - data.testCertificateQrCode!!.let { qrCodeExtractor.extract(it).data } + private val testCertificateQRCode: TestCertificateQRCode by lazy { + data.testCertificateQrCode!!.let { + qrCodeExtractor.extract( + it, + DccV1Parser.Mode.CERT_TEST_STRICT + ) as TestCertificateQRCode + } } val isPublicKeyRegistered: Boolean @@ -31,7 +36,7 @@ data class TestCertificateContainer( val certificateId: String? get() { if (isCertificateRetrievalPending) return null - return certificateData.certificate.payload.uniqueCertificateIdentifier + return testCertificateQRCode.uniqueCertificateIdentifier } fun toTestCertificate( @@ -40,9 +45,9 @@ data class TestCertificateContainer( ): TestCertificate? { if (isCertificateRetrievalPending) return null - val header = certificateData.header - val certificate = certificateData.certificate - val testCertificate = certificate.payload + val header = testCertificateQRCode.data.header + val certificate = testCertificateQRCode.data.certificate + val testCertificate = certificate.test return object : TestCertificate { override val personIdentifier: CertificatePersonIdentifier @@ -72,9 +77,7 @@ data class TestCertificateContainer( get() = testCertificate.testNameAndManufactor?.let { valueSet?.getDisplayText(it) ?: it } override val sampleCollectedAt: Instant get() = testCertificate.sampleCollectedAt - override val testResultAt: Instant? - get() = testCertificate.testResultAt - override val testCenter: String + override val testCenter: String? get() = testCertificate.testCenter override val isUpdatingData: Boolean diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt index f929e89fb..fe9960c7c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt @@ -3,10 +3,10 @@ package de.rki.coronawarnapp.covidcertificate.test.core.storage import dagger.Reusable import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException -import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateComponents import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateServer import de.rki.coronawarnapp.util.TimeStamper @@ -26,7 +26,7 @@ class TestCertificateProcessor @Inject constructor( private val rsaKeyPairGenerator: RSAKeyPairGenerator, private val rsaCryptography: RSACryptography, private val appConfigProvider: AppConfigProvider, - private val qrCodeExtractor: TestCertificateQRCodeExtractor, + private val qrCodeExtractor: DccQrCodeExtractor, ) { /** @@ -131,7 +131,7 @@ class TestCertificateProcessor @Inject constructor( throw InvalidTestCertificateException(InvalidHealthCertificateException.ErrorCode.RSA_DECRYPTION_FAILED) } - val extractedData = qrCodeExtractor.extract( + val extractedData = qrCodeExtractor.extractEncrypted( decryptionKey = encryptionKey.toByteArray(), rawCoseObjectEncrypted = components.encryptedCoseTestCertificateBase64.decodeBase64()!!.toByteArray() ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt deleted file mode 100644 index 5a163335c..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt +++ /dev/null @@ -1,42 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate - -import com.google.gson.annotations.SerializedName -import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc -import de.rki.coronawarnapp.covidcertificate.common.certificate.toLocalDateLeniently -import org.joda.time.LocalDate - -data class VaccinationDccV1( - @SerializedName("ver") override val version: String, - @SerializedName("nam") override val nameData: Dcc.NameData, - @SerializedName("dob") override val dob: String, - @SerializedName("v") override val payloads: List<VaccinationData>, -) : Dcc<VaccinationDccV1.VaccinationData>() { - - data class VaccinationData( - // Disease or agent targeted, e.g. "tg": "840539006" - @SerializedName("tg") override val targetId: String, - // Vaccine or prophylaxis, e.g. "vp": "1119349007" - @SerializedName("vp") val vaccineId: String, - // Vaccine medicinal product,e.g. "mp": "EU/1/20/1528", - @SerializedName("mp") val medicalProductId: String, - // Marketing Authorization Holder, e.g. "ma": "ORG-100030215", - @SerializedName("ma") val marketAuthorizationHolderId: String, - // Dose Number, e.g. "dn": 2 - @SerializedName("dn") val doseNumber: Int, - // Total Series of Doses, e.g. "sd": 2, - @SerializedName("sd") val totalSeriesOfDoses: Int, - // Date of Vaccination, e.g. "dt" : "2021-04-21" - @SerializedName("dt") val dt: String, - // Country of Vaccination, e.g. "co": "NL" - @SerializedName("co") override val certificateCountry: String, - // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport", - @SerializedName("is") override val certificateIssuer: String, - // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" - @SerializedName("ci") override val uniqueCertificateIdentifier: String - ) : Payload { - // Can't use lazy because GSON will NULL it, as we have no no-args constructor - private var vaccinatedAtCache: LocalDate? = null - val vaccinatedAt: LocalDate - get() = vaccinatedAtCache ?: dt.toLocalDateLeniently().also { vaccinatedAtCache = it } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt deleted file mode 100644 index 725d2082d..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt +++ /dev/null @@ -1,77 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate - -import com.google.gson.Gson -import com.upokecenter.cbor.CBORObject -import dagger.Reusable -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException -import de.rki.coronawarnapp.util.serialization.BaseGson -import de.rki.coronawarnapp.util.serialization.fromJson -import timber.log.Timber -import javax.inject.Inject - -@Reusable -class VaccinationDccV1Parser @Inject constructor( - @BaseGson private val gson: Gson -) { - - fun parse(map: CBORObject, lenient: Boolean): VaccinationDccV1 = try { - map[keyHCert]?.run { - this[keyEuDgcV1]?.run { - this.toCertificate(lenient = lenient) - } ?: throw InvalidVaccinationCertificateException(ErrorCode.HC_CWT_NO_DGC) - } ?: throw InvalidVaccinationCertificateException(ErrorCode.HC_CWT_NO_HCERT) - } catch (e: InvalidHealthCertificateException) { - throw e - } catch (e: Throwable) { - throw InvalidVaccinationCertificateException(ErrorCode.HC_CBOR_DECODING_FAILED, cause = e) - } - - @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") - private fun VaccinationDccV1.toValidated(lenient: Boolean): VaccinationDccV1 = this - .run { - if (payloads.isEmpty()) throw InvalidVaccinationCertificateException(ErrorCode.VC_NO_VACCINATION_ENTRY) - - if (payloads.size == 1) return@run this - - if (lenient) { - Timber.w("Lenient: Vaccination data contained multiple entries.") - copy(payloads = listOf(payloads.maxByOrNull { it.vaccinatedAt }!!)) - } else { - throw InvalidVaccinationCertificateException(ErrorCode.VC_MULTIPLE_VACCINATION_ENTRIES) - } - } - .apply { - // Apply otherwise we risk accidentally accessing the original obj in the outer scope - // Force date parsing - // check for non null (Gson does not enforce it) & force date parsing - require(version.isNotBlank()) - require(nameData.familyNameStandardized.isNotBlank()) - dateOfBirth - payload.let { - it.vaccinatedAt - require(it.certificateIssuer.isNotBlank()) - require(it.certificateCountry.isNotBlank()) - require(it.marketAuthorizationHolderId.isNotBlank()) - require(it.medicalProductId.isNotBlank()) - require(it.targetId.isNotBlank()) - require(it.doseNumber > 0) - require(it.totalSeriesOfDoses > 0) - } - } - - private fun CBORObject.toCertificate(lenient: Boolean): VaccinationDccV1 = try { - val json = ToJSONString() - gson.fromJson<VaccinationDccV1>(json).toValidated(lenient = lenient) - } catch (e: InvalidVaccinationCertificateException) { - throw e - } catch (e: Throwable) { - throw InvalidVaccinationCertificateException(ErrorCode.JSON_SCHEMA_INVALID) - } - - companion object { - private val keyEuDgcV1 = CBORObject.FromObject(1) - private val keyHCert = CBORObject.FromObject(-260) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidator.kt new file mode 100644 index 000000000..475d6ee81 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidator.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode + +import dagger.Reusable +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.PREFIX_INVALID +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException +import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class DccQrCodeValidator @Inject constructor( + dccQrCodeExtractor: DccQrCodeExtractor +) { + private val extractors = setOf(dccQrCodeExtractor) + + fun validate(rawString: String): DccQrCode { + // If there is more than one "extractor" in the future, check censoring again. + // CertificateQrCodeCensor.addQRCodeStringToCensor(rawString) + return findExtractor(rawString) + ?.extract(rawString, mode = DccV1Parser.Mode.CERT_SINGLE_STRICT) + ?.also { Timber.i("Extracted data from QR code is %s", it) } + ?: throw InvalidVaccinationCertificateException(PREFIX_INVALID) + } + + private fun findExtractor(rawString: String): DccQrCodeExtractor? { + return extractors.find { it.canHandle(rawString) } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationCertificateQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationCertificateQRCode.kt index ad88fb591..a84cabaaf 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationCertificateQRCode.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationCertificateQRCode.kt @@ -1,11 +1,14 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData +import de.rki.coronawarnapp.covidcertificate.common.certificate.VaccinationDccV1 import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString -import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1 data class VaccinationCertificateQRCode( override val qrCode: QrCodeString, override val data: DccData<VaccinationDccV1> -) : DccQrCode<VaccinationDccV1> +) : DccQrCode { + override val uniqueCertificateIdentifier: String + get() = data.certificate.vaccination.uniqueCertificateIdentifier +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt deleted file mode 100644 index 99fd3123e..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt +++ /dev/null @@ -1,83 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode - -import de.rki.coronawarnapp.bugreporting.censors.vaccination.CertificateQrCodeCensor -import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor -import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData -import de.rki.coronawarnapp.covidcertificate.common.decoder.DccCoseDecoder -import de.rki.coronawarnapp.covidcertificate.common.decoder.DccHeaderParser -import de.rki.coronawarnapp.covidcertificate.common.decoder.RawCOSEObject -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException -import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1 -import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1Parser -import de.rki.coronawarnapp.util.compression.inflate -import de.rki.coronawarnapp.util.encoding.Base45Decoder -import timber.log.Timber -import javax.inject.Inject - -class VaccinationQRCodeExtractor @Inject constructor( - private val coseDecoder: DccCoseDecoder, - private val headerParser: DccHeaderParser, - private val bodyParser: VaccinationDccV1Parser, -) : QrCodeExtractor<VaccinationCertificateQRCode> { - - override fun canHandle(rawString: String): Boolean = rawString.startsWith(PREFIX) - - override fun extract(rawString: String, mode: QrCodeExtractor.Mode): VaccinationCertificateQRCode { - CertificateQrCodeCensor.addQRCodeStringToCensor(rawString) - - val parsedData = rawString - .removePrefix(PREFIX) - .decodeBase45() - .decompress() - .parse(lenient = mode == QrCodeExtractor.Mode.CERT_VAC_LENIENT) - - return VaccinationCertificateQRCode( - qrCode = rawString, - data = parsedData, - ) - } - - private fun String.decodeBase45(): ByteArray = try { - Base45Decoder.decode(this) - } catch (e: Throwable) { - Timber.e(e) - throw InvalidVaccinationCertificateException(HC_BASE45_DECODING_FAILED) - } - - private fun ByteArray.decompress(): RawCOSEObject = try { - this.inflate(sizeLimit = DEFAULT_SIZE_LIMIT) - } catch (e: Throwable) { - Timber.e(e) - throw InvalidVaccinationCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) - } - - fun RawCOSEObject.parse(lenient: Boolean): DccData<VaccinationDccV1> = try { - Timber.v("Parsing COSE for vaccination certificate.") - val cbor = coseDecoder.decode(this) - - DccData( - header = headerParser.parse(cbor), - certificate = bodyParser.parse(cbor, lenient = lenient) - ).also { - CertificateQrCodeCensor.addCertificateToCensor(it) - }.also { - Timber.v("Parsed vaccination certificate for %s", it.certificate.nameData.familyNameStandardized) - } - } catch (e: InvalidHealthCertificateException) { - throw InvalidVaccinationCertificateException(e.errorCode) - } catch (e: Throwable) { - Timber.e(e) - throw InvalidVaccinationCertificateException(HC_CBOR_DECODING_FAILED) - } - - companion object { - private const val PREFIX = "HC1:" - - // Zip bomb - private const val DEFAULT_SIZE_LIMIT = 1024L * 1024 * 10L // 10 MB - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt deleted file mode 100644 index 57c93cbf5..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt +++ /dev/null @@ -1,28 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode - -import dagger.Reusable -import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException -import timber.log.Timber -import javax.inject.Inject - -@Reusable -class VaccinationQRCodeValidator @Inject constructor( - vaccinationQRCodeExtractor: VaccinationQRCodeExtractor -) { - private val extractors = setOf(vaccinationQRCodeExtractor) - - fun validate(rawString: String): VaccinationCertificateQRCode { - // If there is more than one "extractor" in the future, check censoring again. - // CertificateQrCodeCensor.addQRCodeStringToCensor(rawString) - return findExtractor(rawString) - ?.extract(rawString, mode = QrCodeExtractor.Mode.CERT_VAC_STRICT) - ?.also { Timber.i("Extracted data from QR code is %s", it) } - ?: throw InvalidVaccinationCertificateException(VC_PREFIX_INVALID) - } - - private fun findExtractor(rawString: String): QrCodeExtractor<VaccinationCertificateQRCode>? { - return extractors.find { it.canHandle(rawString) } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepository.kt index aba2b0b12..d611f5d55 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepository.kt @@ -2,12 +2,12 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.repository import de.rki.coronawarnapp.bugreporting.reportProblem import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.ALREADY_REGISTERED import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinatedPerson import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationCertificate import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode -import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.errors.VaccinationCertificateNotFoundException import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinatedPersonData import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationContainer @@ -38,7 +38,7 @@ class VaccinationRepository @Inject constructor( private val timeStamper: TimeStamper, private val storage: VaccinationStorage, valueSetsRepository: ValueSetsRepository, - private val vaccinationQRCodeExtractor: VaccinationQRCodeExtractor, + private val qrCodeExtractor: DccQrCodeExtractor, ) { private val internalData: HotDataFlow<Set<VaccinatedPerson>> = HotDataFlow( @@ -95,12 +95,12 @@ class VaccinationRepository @Inject constructor( if (matchingPerson.data.vaccinations.any { it.certificateId == qrCode.uniqueCertificateIdentifier }) { Timber.tag(TAG).e("Certificate is already registered: %s", qrCode.uniqueCertificateIdentifier) - throw InvalidVaccinationCertificateException(VC_ALREADY_REGISTERED) + throw InvalidVaccinationCertificateException(ALREADY_REGISTERED) } val newCertificate = qrCode.toVaccinationContainer( scannedAt = timeStamper.nowUTC, - qrCodeExtractor = vaccinationQRCodeExtractor, + qrCodeExtractor = qrCodeExtractor, ) val modifiedPerson = matchingPerson.copy( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/ContainerPostProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/ContainerPostProcessor.kt index ce35f0c23..4319f179c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/ContainerPostProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/ContainerPostProcessor.kt @@ -7,14 +7,14 @@ import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import dagger.Reusable -import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor import timber.log.Timber import java.io.IOException import javax.inject.Inject @Reusable class ContainerPostProcessor @Inject constructor( - private val vaccinationQrCodeExtractor: VaccinationQRCodeExtractor, + private val vaccinationQrCodeExtractor: DccQrCodeExtractor, ) : TypeAdapterFactory { override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> { val delegate = gson.getDelegateAdapter(this, type) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt index ca7f06e31..a2caa83cd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt @@ -2,15 +2,16 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storag import androidx.annotation.Keep import com.google.gson.annotations.SerializedName -import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData import de.rki.coronawarnapp.covidcertificate.common.certificate.DccHeader +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1 +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser +import de.rki.coronawarnapp.covidcertificate.common.certificate.VaccinationDccV1 import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationCertificate -import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1 import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode -import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.VaccinationValueSets import org.joda.time.Instant import org.joda.time.LocalDate @@ -23,7 +24,7 @@ data class VaccinationContainer internal constructor( ) { // Either set by [ContainerPostProcessor] or via [toVaccinationContainer] - @Transient lateinit var qrCodeExtractor: VaccinationQRCodeExtractor + @Transient lateinit var qrCodeExtractor: DccQrCodeExtractor @Transient internal var preParsedData: DccData<VaccinationDccV1>? = null // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null @@ -32,7 +33,13 @@ data class VaccinationContainer internal constructor( @delegate:Transient internal val certificateData: DccData<VaccinationDccV1> by lazy { - preParsedData ?: qrCodeExtractor.extract(vaccinationQrCode, mode = Mode.CERT_VAC_LENIENT).data + preParsedData ?: ( + qrCodeExtractor.extract( + vaccinationQrCode, + mode = DccV1Parser.Mode.CERT_VAC_LENIENT + ) as VaccinationCertificateQRCode + ) + .data } val header: DccHeader @@ -41,8 +48,8 @@ data class VaccinationContainer internal constructor( val certificate: VaccinationDccV1 get() = certificateData.certificate - val vaccination: VaccinationDccV1.VaccinationData - get() = certificate.payload + val vaccination: DccV1.VaccinationData + get() = certificate.vaccination val certificateId: String get() = vaccination.uniqueCertificateIdentifier @@ -110,7 +117,7 @@ data class VaccinationContainer internal constructor( fun VaccinationCertificateQRCode.toVaccinationContainer( scannedAt: Instant, - qrCodeExtractor: VaccinationQRCodeExtractor, + qrCodeExtractor: DccQrCodeExtractor, ) = VaccinationContainer( vaccinationQrCode = this.qrCode, scannedAt = scannedAt, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt index 90c0de242..5fc1ecbdd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt @@ -10,7 +10,7 @@ import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.DefaultDecoderFactory import de.rki.coronawarnapp.R import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException import de.rki.coronawarnapp.databinding.FragmentScanQrCodeBinding import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.ExternalActionHelper.openUrl @@ -69,9 +69,9 @@ class VaccinationQrCodeScanFragment : binding.qrCodeScanSpinner.hide() it.toErrorDialogBuilder(requireContext()).apply { setOnDismissListener { popBackStack() } - if (it is InvalidVaccinationCertificateException && it.showFaqButton) { - setNeutralButton(R.string.error_button_vc_faq) { _, _ -> - openUrl(getString(R.string.error_button_vc_faq_link)) + if (it is InvalidHealthCertificateException && it.showFaqButton) { + setNeutralButton(it.faqButtonText) { _, _ -> + openUrl(getString(it.faqLink)) } } }.show() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt index adbddadf7..64dfd89b0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt @@ -3,7 +3,10 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.ui.scan import com.journeyapps.barcodescanner.BarcodeResult import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeValidator +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_VACCINATION_ENTRY +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException +import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.DccQrCodeValidator +import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.VaccinationRepository import de.rki.coronawarnapp.util.permission.CameraSettings import de.rki.coronawarnapp.util.ui.SingleLiveEvent @@ -13,7 +16,7 @@ import timber.log.Timber class VaccinationQrCodeScanViewModel @AssistedInject constructor( private val cameraSettings: CameraSettings, - private val vaccinationQRCodeValidator: VaccinationQRCodeValidator, + private val vaccinationQRCodeValidator: DccQrCodeValidator, private val vaccinationRepository: VaccinationRepository ) : CWAViewModel() { @@ -25,6 +28,9 @@ class VaccinationQrCodeScanViewModel @AssistedInject constructor( try { event.postValue(Event.QrCodeScanInProgress) val qrCode = vaccinationQRCodeValidator.validate(barcodeResult.text) + if (qrCode !is VaccinationCertificateQRCode) { + throw InvalidVaccinationCertificateException(NO_VACCINATION_ENTRY) + } val vaccinationCertificate = vaccinationRepository.registerVaccination(qrCode) event.postValue(Event.QrCodeScanSucceeded(vaccinationCertificate.personIdentifier.codeSHA256)) } catch (e: Throwable) { diff --git a/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml index 384be191b..f574d1824 100644 --- a/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml @@ -94,9 +94,9 @@ <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"Този Ñертификат за вакÑиниране вÑе още не Ñе поддържа във верÑиÑта на вашето приложение. МолÑ, актуализирайте приложението Ñи или Ñе Ñвържете Ñ Ð³Ð¾Ñ€ÐµÑ‰Ð°Ñ‚Ð° Ð»Ð¸Ð½Ð¸Ñ Ð·Ð° техничеÑки проблеми от â€œÐ˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложениетоâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_scan_again">"ВакÑинационниÑÑ‚ Ñертификат не може да бъде запазен на Ñмартфона Ви. МолÑ, опитайте отново по-къÑно или Ñе Ñвържете Ñ Ð³Ð¾Ñ€ÐµÑ‰Ð°Ñ‚Ð° Ð»Ð¸Ð½Ð¸Ñ Ð·Ð° техничеÑки проблеми, поÑочена в â€žÐ˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложението“."</string> + <string name="error_dcc_scan_again">"ВакÑинационниÑÑ‚ Ñертификат не може да бъде запазен на Ñмартфона Ви. МолÑ, опитайте отново по-къÑно или Ñе Ñвържете Ñ Ð³Ð¾Ñ€ÐµÑ‰Ð°Ñ‚Ð° Ð»Ð¸Ð½Ð¸Ñ Ð·Ð° техничеÑки проблеми, поÑочена в â€žÐ˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложението“."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_already_registered">"ВакÑинационниÑÑ‚ Ñертификат вече е региÑтриран в приложението Ви."</string> + <string name="error_dcc_already_registered">"ВакÑинационниÑÑ‚ Ñертификат вече е региÑтриран в приложението Ви."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_different_person">"Личната Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð² този Ñертификат за вакÑиниране не ÑъответÑтва на тази във вече региÑтрираните Ñертификати. Ð’ приложението можете да региÑтрирате Ñертификати Ñамо за едно лице."</string> @@ -121,4 +121,4 @@ <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> -</resources> \ No newline at end of file +</resources> diff --git a/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml index 4b580c2aa..15df45fa6 100644 --- a/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml @@ -95,9 +95,9 @@ <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">Dieses Impfzertifikat wird in Ihrer App-Version noch nicht unterstützt. Bitte aktualisieren Sie Ihre App oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_scan_again">Das Impfzertifikat konnte nicht auf Ihrem Smartphone gespeichert werden. Bitte versuchen Sie es später noch einmal oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string> + <string name="error_dcc_scan_again">Das Zertifikat konnte nicht auf Ihrem Smartphone gespeichert werden. Bitte versuchen Sie es später noch einmal oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_already_registered">Das Impfzertifikat ist bereits in Ihrer App registriert.</string> + <string name="error_dcc_already_registered">Das Zertifikat ist bereits in Ihrer App registriert.</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_different_person">Die persönlichen Daten dieses Impfzertifikats stimmen nicht mit denen der bereits registrierten Zertifikate überein. Sie können in der App nur Zertifikate einer Person registrieren.</string> diff --git a/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml index fa39d43df..9f534b5c5 100644 --- a/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml @@ -94,9 +94,9 @@ <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"This vaccination certificate is not supported in your app version yet. Please update your app or contact the technical hotline under “App Informationâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_scan_again">"The vaccination certificate could not be saved on your smartphone. Please try again later or contact the technical hotline under “App Informationâ€."</string> + <string name="error_dcc_scan_again">"The vaccination certificate could not be saved on your smartphone. Please try again later or contact the technical hotline under “App Informationâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_already_registered">"The vaccination certificate is already registered in your app."</string> + <string name="error_dcc_already_registered">"The vaccination certificate is already registered in your app."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_different_person">"The personal information of this vaccination certificate does not match that of the certificates already registered. You can only register certificates for one person in the app."</string> @@ -121,4 +121,4 @@ <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> -</resources> \ No newline at end of file +</resources> diff --git a/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml index 58bcf8c67..596ff4268 100644 --- a/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml @@ -94,9 +94,9 @@ <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"To Å›wiadectwo szczepienia nie jest jeszcze obsÅ‚ugiwane w Twojej wersji aplikacji. Zaktualizuj aplikacjÄ™ lub skontaktuj siÄ™ z infoliniÄ… technicznÄ… dostÄ™pnÄ… w sekcji „Informacje o aplikacjiâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_scan_again">"Åšwiadectwo szczepienia nie mogÅ‚o zostać zapisane na Twoim smartfonie. Spróbuj ponownie później lub skontaktuj siÄ™ z infoliniÄ… technicznÄ… dostÄ™pnÄ… w sekcji „Informacje o aplikacjiâ€."</string> + <string name="error_dcc_scan_again">"Åšwiadectwo szczepienia nie mogÅ‚o zostać zapisane na Twoim smartfonie. Spróbuj ponownie później lub skontaktuj siÄ™ z infoliniÄ… technicznÄ… dostÄ™pnÄ… w sekcji „Informacje o aplikacjiâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_already_registered">"Åšwiadectwo szczepienia jest już zarejestrowane w Twojej aplikacji."</string> + <string name="error_dcc_already_registered">"Åšwiadectwo szczepienia jest już zarejestrowane w Twojej aplikacji."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_different_person">"Dane osobowe tego Å›wiadectwa szczepienia nie sÄ… zgodne z danymi już zarejestrowanych Å›wiadectw. W aplikacji można rejestrować Å›wiadectwa tylko dla jednej osoby."</string> @@ -121,4 +121,4 @@ <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> -</resources> \ No newline at end of file +</resources> diff --git a/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml index a6fefa697..8a610b802 100644 --- a/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml @@ -94,9 +94,9 @@ <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"Acest certificat de vaccinare nu este încă acceptat de versiunea aplicaÈ›iei dvs. ActualizaÈ›i-vă aplicaÈ›ia sau contactaÈ›i hotline-ul tehnic din „InformaÈ›ii aplicaÈ›ieâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_scan_again">"Certificatul de vaccinare nu a putut fi salvat pe smartphone-ul dvs. ÃŽncercaÈ›i din nou mai târziu sau contactaÈ›i hotline-ul tehnic din „InformaÈ›ii aplicaÈ›ieâ€."</string> + <string name="error_dcc_scan_again">"Certificatul de vaccinare nu a putut fi salvat pe smartphone-ul dvs. ÃŽncercaÈ›i din nou mai târziu sau contactaÈ›i hotline-ul tehnic din „InformaÈ›ii aplicaÈ›ieâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_already_registered">"Certificatul de vaccinare este deja înregistrat în aplicaÈ›ia dvs."</string> + <string name="error_dcc_already_registered">"Certificatul de vaccinare este deja înregistrat în aplicaÈ›ia dvs."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_different_person">"InformaÈ›iile personale din acest certificat de vaccinare nu corespund cu cele din certificatele deja înregistrate. PuteÈ›i înregistra certificate pentru o singură persoană în aplicaÈ›ie."</string> @@ -121,4 +121,4 @@ <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> -</resources> \ No newline at end of file +</resources> diff --git a/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml index 111e60df3..1f9565c47 100644 --- a/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml @@ -94,9 +94,9 @@ <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"Bu aşı sertifikası henüz uygulamanızın sürümünde desteklenmiyor. Lütfen uygulamanızı güncelleyin veya “Uygulama Bilgileri†bölümünde belirtilen teknik yardım hattı ile iletiÅŸime geçin."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_scan_again">"Aşı sertifikası akıllı telefonunuza kaydedilemedi. Lütfen daha sonra yeniden deneyin veya “Uygulama Bilgileri†bölümünde belirtilen teknik yardım hattı ile iletiÅŸime geçin."</string> + <string name="error_dcc_scan_again">"Aşı sertifikası akıllı telefonunuza kaydedilemedi. Lütfen daha sonra yeniden deneyin veya “Uygulama Bilgileri†bölümünde belirtilen teknik yardım hattı ile iletiÅŸime geçin."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_already_registered">"Aşı sertifikası zaten uygulamanıza kaydedilmiÅŸ."</string> + <string name="error_dcc_already_registered">"Aşı sertifikası zaten uygulamanıza kaydedilmiÅŸ."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_different_person">"Bu aşı sertifikasındaki kiÅŸisel bilgiler, kaydedilmiÅŸ sertifikalardaki bilgilerle eÅŸleÅŸmiyor. Uygulamaya yalnızca bir kiÅŸiye ait sertifikalar kaydedebilirsiniz."</string> @@ -121,4 +121,4 @@ <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> -</resources> \ No newline at end of file +</resources> diff --git a/Corona-Warn-App/src/main/res/values/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values/vaccination_strings.xml index 83b31ca56..a1ecec05d 100644 --- a/Corona-Warn-App/src/main/res/values/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values/vaccination_strings.xml @@ -95,9 +95,9 @@ <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"This vaccination certificate is not supported in your app version yet. Please update your app or contact the technical hotline under “App Informationâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_scan_again">"The vaccination certificate could not be saved on your smartphone. Please try again later or contact the technical hotline under “App Informationâ€."</string> + <string name="error_dcc_scan_again">"The vaccination certificate could not be saved on your smartphone. Please try again later or contact the technical hotline under “App Informationâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_already_registered">"The vaccination certificate is already registered in your app."</string> + <string name="error_dcc_already_registered">"The vaccination certificate is already registered in your app."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_different_person">"The personal information of this vaccination certificate does not match that of the certificates already registered. You can only register certificates for one person in the app."</string> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensorTest.kt similarity index 65% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensorTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensorTest.kt index ed3ee7d48..4f6210b46 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensorTest.kt @@ -1,18 +1,20 @@ package de.rki.coronawarnapp.bugreporting.censors.vaccination -import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc +import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData -import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1 +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1 +import de.rki.coronawarnapp.covidcertificate.common.certificate.VaccinationDccV1 import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.mockk import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.LocalDate import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @Suppress("MaxLineLength") -internal class CertificateQrCodeCensorTest { +internal class DccQrCodeCensorTest { private val testRawString = "HC1:6BFOXN*TS0BI\$ZD.P9UOL97O4-2HH77HRM3DSPTLRR+%3.ZH9M9ESIGUBA KWML/O6HXK 0D+4O5VC9:BPCNYKMXEE1JAA/CZIK0JK1WL260X638J3-E3GG396B-43FZT-43:S0X37*ZV+FNI6HXY0ZSVILVQJF//05MVZJ5V.499TXY9KK9+OC+G9QJPNF67J6QW67KQY466PPM4MLJE+.PDB9L6Q2+PFQ5DB96PP5/P-59A%N+892 7J235II3NJ7PK7SLQMIJSBHVA7UJQWT.+S+ND%%M%331BH.IA.C8KRDL4O54O4IGUJKJGI0JAXD15IAXMFU*GSHGHD63DAOC9JU0H11+*4.\$S6ZC0JBZAB-C3QHISKE MCAOI8%M3V96-PY\$N6XOWLIBPIAYU:*JIRHUF2XZQ4H9 XJ72WG1K36VF/9BL56%E8T1OEEG%5TW5A 6YO67N6UCE:WT6BT-UMM:ABJK2TMDN1:FW-%T+\$D78NDSC3%5F61NYS-P9LOE0%J/ZAY:N5L4H-H/LH:AO3FU JHG7K46IOIMT.RE%PHLA21JRI3HTC\$AH" @@ -20,26 +22,29 @@ internal class CertificateQrCodeCensorTest { header = mockk(), certificate = VaccinationDccV1( version = "1", - nameData = Dcc.NameData( + nameData = DccV1.NameData( familyName = "Kevin", familyNameStandardized = "KEVIN", givenName = "Bob", givenNameStandardized = "BOB" ), - dob = "1969-11-16", - payloads = listOf( - VaccinationDccV1.VaccinationData( - targetId = "12345", - vaccineId = "1214765", - medicalProductId = "aaEd/easd", - marketAuthorizationHolderId = "ASD-2312", - doseNumber = 2, - totalSeriesOfDoses = 5, - dt = "1969-04-20", - certificateCountry = "DE", - certificateIssuer = "Herbert", - uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" - ) + dateOfBirth = LocalDate.parse("1969-11-16"), + vaccination = DccV1.VaccinationData( + targetId = "12345", + vaccineId = "1214765", + medicalProductId = "aaEd/easd", + marketAuthorizationHolderId = "ASD-2312", + doseNumber = 2, + totalSeriesOfDoses = 5, + dt = "1969-04-20", + certificateCountry = "DE", + certificateIssuer = "Herbert", + uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" + ), + personIdentifier = CertificatePersonIdentifier( + dateOfBirth = LocalDate.parse("1969-11-16"), + lastNameStandardized = "KEVIN", + firstNameStandardized = "BOB" ) ) ) @@ -51,23 +56,23 @@ internal class CertificateQrCodeCensorTest { @AfterEach fun teardown() { - CertificateQrCodeCensor.clearCertificateToCensor() - CertificateQrCodeCensor.clearQRCodeStringToCensor() + DccQrCodeCensor.clearCertificateToCensor() + DccQrCodeCensor.clearQRCodeStringToCensor() } - private fun createInstance() = CertificateQrCodeCensor() + private fun createInstance() = DccQrCodeCensor() @Test fun `checkLog() should return censored LogLine`() = runBlockingTest { - CertificateQrCodeCensor.addQRCodeStringToCensor(testRawString) - CertificateQrCodeCensor.addCertificateToCensor(testCertificateData) + DccQrCodeCensor.addQRCodeStringToCensor(testRawString) + DccQrCodeCensor.addCertificateToCensor(testCertificateData) val censor = createInstance() val logLineToCensor = "Here comes the rawString: $testRawString of the vaccine certificate" censor.checkLog(logLineToCensor)!! - .compile()!!.censored shouldBe "Here comes the rawString: ########-####-####-####-########C\$AH of the vaccine certificate" + .compile()!!.censored shouldBe "Here comes the rawString: ###C\$AH of the vaccine certificate" val certDataToCensor = "Hello my name is Kevin Bob, i was born at 1969-11-16, i have been " + "vaccinated with: 12345 1214765 aaEd/easd ASD-2312 1969-04-20 DE Herbert" + @@ -75,7 +80,7 @@ internal class CertificateQrCodeCensorTest { censor.checkLog(certDataToCensor)!! .compile()!!.censored shouldBe "Hello my name is nameData/familyName nameData/givenName, i was born at " + - "vaccinationCertificate/dateOfBirth, i have been vaccinated with: vaccinationData/targetId " + + "covidCertificate/dateOfBirth, i have been vaccinated with: vaccinationData/targetId " + "vaccinationData/vaccineId vaccinationData/medicalProductId" + " vaccinationData/marketAuthorizationHolderId vaccinationData/vaccinatedAt" + " vaccinationData/certificateCountry vaccinationData/certificateIssuer" + @@ -93,8 +98,8 @@ internal class CertificateQrCodeCensorTest { @Test fun `checkLog() should return null if nothing should be censored`() = runBlockingTest { - CertificateQrCodeCensor.addQRCodeStringToCensor(testRawString.replace("1", "2")) - CertificateQrCodeCensor.addCertificateToCensor(testCertificateData) + DccQrCodeCensor.addQRCodeStringToCensor(testRawString.replace("1", "2")) + DccQrCodeCensor.addCertificateToCensor(testCertificateData) val censor = createInstance() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt index e5cdb3f4c..90a812bae 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt @@ -1,6 +1,5 @@ package de.rki.coronawarnapp.coronatest.qrcode -import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode import de.rki.coronawarnapp.coronatest.type.CoronaTest import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe @@ -46,8 +45,8 @@ class CoronaTestQrCodeValidatorTest : BaseTest() { fun `validator uses strict extraction mode`() { val instance = CoronaTestQrCodeValidator(raExtractor, pcrExtractor) instance.validate(pcrQrCode1).type shouldBe CoronaTest.Type.PCR - verify { pcrExtractor.extract(pcrQrCode1, Mode.TEST_STRICT) } + verify { pcrExtractor.extract(pcrQrCode1) } instance.validate(raQrCode1).type shouldBe CoronaTest.Type.RAPID_ANTIGEN - verify { raExtractor.extract(raQrCode1, Mode.TEST_STRICT) } + verify { raExtractor.extract(raQrCode1) } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt index 86e181271..d73a307fe 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt @@ -1,6 +1,5 @@ package de.rki.coronawarnapp.coronatest.qrcode -import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode.TEST_STRICT import io.kotest.matchers.shouldBe import org.junit.Test import testhelpers.BaseTest @@ -17,7 +16,7 @@ class PcrQrCodeExtractorTest : BaseTest() { val extractor = PcrQrCodeExtractor() try { if (extractor.canHandle("$prefixString$guid")) { - extractor.extract("$prefixString$guid", mode = TEST_STRICT) + extractor.extract("$prefixString$guid") conditionToMatch shouldBe true } else { conditionToMatch shouldBe false @@ -80,41 +79,32 @@ class PcrQrCodeExtractorTest : BaseTest() { fun extractGUID() { PcrQrCodeExtractor().extract( "$localhostUpperCase$guidUpperCase", - mode = TEST_STRICT ).qrCodeGUID shouldBe guidUpperCase PcrQrCodeExtractor().extract( "$localhostUpperCase$guidLowerCase", - mode = TEST_STRICT ).qrCodeGUID shouldBe guidLowerCase PcrQrCodeExtractor().extract( "$localhostUpperCase$guidMixedCase", - mode = TEST_STRICT ).qrCodeGUID shouldBe guidMixedCase PcrQrCodeExtractor().extract( "$localhostLowerCase$guidUpperCase", - mode = TEST_STRICT ).qrCodeGUID shouldBe guidUpperCase PcrQrCodeExtractor().extract( "$localhostLowerCase$guidLowerCase", - mode = TEST_STRICT ).qrCodeGUID shouldBe guidLowerCase PcrQrCodeExtractor().extract( "$localhostLowerCase$guidMixedCase", - mode = TEST_STRICT ).qrCodeGUID shouldBe guidMixedCase PcrQrCodeExtractor().extract( "$localhostMixedCase$guidUpperCase", - mode = TEST_STRICT ).qrCodeGUID shouldBe guidUpperCase PcrQrCodeExtractor().extract( "$localhostMixedCase$guidLowerCase", - mode = TEST_STRICT ).qrCodeGUID shouldBe guidLowerCase PcrQrCodeExtractor().extract( "$localhostMixedCase$guidMixedCase", - mode = TEST_STRICT ).qrCodeGUID shouldBe guidMixedCase } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt index 4a746b6ed..e78658b69 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt @@ -1,6 +1,5 @@ package de.rki.coronawarnapp.coronatest.qrcode -import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode.TEST_STRICT import de.rki.coronawarnapp.coronatest.type.CoronaTest import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe @@ -30,13 +29,13 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() { @Test fun `extracting valid codes does not throw exception`() { listOf(raQrCode1, raQrCode2, raQrCode3, raQrCode4, raQrCode5, raQrCode6, raQrCode7, raQrCode8).forEach { - instance.extract(it, mode = TEST_STRICT) + instance.extract(it) } } @Test fun `personal data is extracted`() { - val data = instance.extract(raQrCode3, mode = TEST_STRICT) + val data = instance.extract(raQrCode3) data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN data.hash shouldBe "7dce08db0d4abd5ac1d2498b571afb221ca947c75c847d05466b4cfe9d95dc66" data.createdAt shouldBe Instant.ofEpochMilli(1619618352000) @@ -47,7 +46,7 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() { @Test fun `empty strings are treated as null or notset`() { - val data = instance.extract(raQrCodeEmptyStrings, mode = TEST_STRICT) + val data = instance.extract(raQrCodeEmptyStrings) data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN data.hash shouldBe "d6e4d0181d8109bf05b346a0d2e0ef0cc472eed70d9df8c4b9ae5c7a009f3e34" data.createdAt shouldBe Instant.ofEpochMilli(1619012952000) @@ -58,14 +57,14 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() { @Test fun `personal data is only valid if complete or completely missing`() { - shouldThrow<InvalidQRCodeException> { instance.extract(raQrIncompletePersonalData, mode = TEST_STRICT) } + shouldThrow<InvalidQRCodeException> { instance.extract(raQrIncompletePersonalData) } } @Test fun `invalid json throws exception`() { val invalidCode = "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2" shouldThrow<InvalidQRCodeException> { - RapidAntigenQrCodeExtractor().extract(invalidCode, mode = TEST_STRICT) + RapidAntigenQrCodeExtractor().extract(invalidCode) } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/common/CertificatePersonIdentifierTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/common/CertificatePersonIdentifierTest.kt index f325680ff..407c664a5 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/common/CertificatePersonIdentifierTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/common/CertificatePersonIdentifierTest.kt @@ -1,8 +1,8 @@ package de.rki.coronawarnapp.covidcertificate.common import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.DOB_MISMATCH +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NAME_MISMATCH import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.assertions.throwables.shouldThrow @@ -58,14 +58,14 @@ class CertificatePersonIdentifierTest : BaseTest() { shouldThrow<InvalidVaccinationCertificateException> { testPersonMaxData.requireMatch(testPersonMaxData.copy(firstNameStandardized = "nope")) - }.errorCode shouldBe VC_NAME_MISMATCH + }.errorCode shouldBe NAME_MISMATCH shouldThrow<InvalidVaccinationCertificateException> { testPersonMaxData.requireMatch(testPersonMaxData.copy(lastNameStandardized = "nope")) - }.errorCode shouldBe VC_NAME_MISMATCH + }.errorCode shouldBe NAME_MISMATCH shouldThrow<InvalidVaccinationCertificateException> { testPersonMaxData.requireMatch(testPersonMaxData.copy(dateOfBirth = LocalDate.parse("1900-12-31"))) - }.errorCode shouldBe VC_DOB_MISMATCH + }.errorCode shouldBe DOB_MISMATCH } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt index 75e7d3204..c170f4869 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt @@ -1,9 +1,9 @@ package de.rki.coronawarnapp.covidcertificate.test import de.rki.coronawarnapp.appconfig.CovidCertificateConfig +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificateRepository -import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode -import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData import de.rki.coronawarnapp.covidcertificate.test.core.storage.StoredTestCertificateData import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateProcessor @@ -25,7 +25,7 @@ import testhelpers.TestDispatcherProvider class TestCertificateRepositoryTest : BaseTest() { @MockK lateinit var storage: TestCertificateStorage - @MockK lateinit var qrCodeExtractor: TestCertificateQRCodeExtractor + @MockK lateinit var qrCodeExtractor: DccQrCodeExtractor @MockK lateinit var covidTestCertificateConfig: CovidCertificateConfig.TestCertificate @MockK lateinit var valueSetsRepository: ValueSetsRepository @MockK lateinit var testCertificateProcessor: TestCertificateProcessor @@ -61,7 +61,7 @@ class TestCertificateRepositoryTest : BaseTest() { every { storage.testCertificates } answers { storageSet } } - coEvery { qrCodeExtractor.extract(any(), any()) } returns mockk<TestCertificateQRCode>().apply { + coEvery { qrCodeExtractor.extract(any()) } returns mockk<DccQrCode>().apply { every { qrCode } returns "qrCode" every { data } returns mockk() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt index 3082a41b5..ae152c4d2 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.covidcertificate.test -import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData import de.rki.coronawarnapp.covidcertificate.test.core.storage.RACertificateData import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateContainer @@ -11,7 +11,7 @@ import javax.inject.Inject @Suppress("MaxLineLength") class TestCertificateTestData @Inject constructor( - qrCodeExtractor: TestCertificateQRCodeExtractor + qrCodeExtractor: DccQrCodeExtractor ) { val personATest1CertQRCodeString = diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateDccParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateDccParserTest.kt index f9f50f5c6..128df7653 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateDccParserTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateDccParserTest.kt @@ -2,8 +2,8 @@ package de.rki.coronawarnapp.covidcertificate.test.core.qrcode import com.google.gson.Gson import com.upokecenter.cbor.CBORObject +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser import de.rki.coronawarnapp.covidcertificate.test.TestData -import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccParser import io.kotest.matchers.shouldBe import okio.ByteString.Companion.decodeHex import org.joda.time.LocalDate @@ -11,12 +11,12 @@ import org.junit.jupiter.api.Test class TestCertificateDccParserTest { - private val bodyParser = TestDccParser(Gson()) + private val bodyParser = DccV1Parser(Gson()) @Test fun `happy path cose decryption with Ellen Cheng`() { val coseObject = CBORObject.DecodeFromBytes(TestData.cborObject.decodeHex().toByteArray()) - with(bodyParser.parse(coseObject)) { + with(bodyParser.parse(coseObject, DccV1Parser.Mode.CERT_TEST_STRICT)) { with(nameData) { familyName shouldBe "Musterfrau-Gößinger" @@ -28,7 +28,7 @@ class TestCertificateDccParserTest { dateOfBirth shouldBe LocalDate.parse("1998-02-26") version shouldBe "1.2.1" - with(payloads[0]) { + with(tests!!.single()) { uniqueCertificateIdentifier shouldBe "URN:UVCI:01:AT:71EE2559DE38C6BF7304FB65A1A451EC#3" certificateCountry shouldBe "AT" certificateIssuer shouldBe "Ministry of Health, Austria" diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractorTest.kt index b823fef1e..c9ee9ee0a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractorTest.kt @@ -1,13 +1,14 @@ package de.rki.coronawarnapp.covidcertificate.test.core.qrcode import com.google.gson.Gson +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser import de.rki.coronawarnapp.covidcertificate.common.cryptography.AesCryptography import de.rki.coronawarnapp.covidcertificate.common.decoder.DccCoseDecoder import de.rki.coronawarnapp.covidcertificate.common.decoder.DccHeaderParser import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException import de.rki.coronawarnapp.covidcertificate.test.TestData -import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccParser import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationQrCodeTestData import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe @@ -20,12 +21,12 @@ import testhelpers.BaseTest class TestCertificateQRCodeExtractorTest : BaseTest() { private val coseDecoder = DccCoseDecoder(AesCryptography()) private val headerParser = DccHeaderParser() - private val bodyParser = TestDccParser(Gson()) - private val extractor = TestCertificateQRCodeExtractor(coseDecoder, headerParser, bodyParser) + private val bodyParser = DccV1Parser(Gson()) + private val extractor = DccQrCodeExtractor(coseDecoder, headerParser, bodyParser) @Test fun `happy path qr code`() { - val qrCode = extractor.extract(TestData.qrCodeTestCertificate) + val qrCode = extractor.extract(TestData.qrCodeTestCertificate) as TestCertificateQRCode with(qrCode.data.header) { issuer shouldBe "AT" issuedAt shouldBe Instant.parse("2021-06-01T10:12:48.000Z") @@ -39,11 +40,10 @@ class TestCertificateQRCodeExtractorTest : BaseTest() { givenName shouldBe "Gabriele" givenNameStandardized shouldBe "GABRIELE" } - dob shouldBe "1998-02-26" dateOfBirth shouldBe LocalDate.parse("1998-02-26") version shouldBe "1.2.1" - with(payloads[0]) { + with(test) { uniqueCertificateIdentifier shouldBe "URN:UVCI:01:AT:71EE2559DE38C6BF7304FB65A1A451EC#3" certificateCountry shouldBe "AT" certificateIssuer shouldBe "Ministry of Health, Austria" @@ -62,7 +62,7 @@ class TestCertificateQRCodeExtractorTest : BaseTest() { with(TestData.EllenCheng()) { val coseObject = coseWithEncryptedPayload.decodeBase64()!!.toByteArray() val dek = dek.decodeBase64()!!.toByteArray() - val result = extractor.extract(dek, coseObject) + val result = extractor.extractEncrypted(dek, coseObject) with(result.data.certificate.nameData) { familyName shouldBe "Cheng" givenName shouldBe "Ellen" @@ -80,7 +80,7 @@ class TestCertificateQRCodeExtractorTest : BaseTest() { with(TestData.BrianCalamandrei()) { val coseObject = coseWithEncryptedPayload.decodeBase64()!!.toByteArray() val dek = dek.decodeBase64()!!.toByteArray() - val result = extractor.extract(dek, coseObject) + val result = extractor.extractEncrypted(dek, coseObject) with(result.data.certificate.nameData) { familyName shouldBe "Calamandrei" givenName shouldBe "Brian" @@ -95,21 +95,21 @@ class TestCertificateQRCodeExtractorTest : BaseTest() { @Test fun `valid encoding but not a health certificate fails with HC_CWT_NO_ISS`() { - shouldThrow<InvalidTestCertificateException> { + shouldThrow<InvalidHealthCertificateException> { extractor.extract(VaccinationQrCodeTestData.validEncoded) }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS } @Test fun `random string fails with HC_BASE45_DECODING_FAILED`() { - shouldThrow<InvalidTestCertificateException> { + shouldThrow<InvalidHealthCertificateException> { extractor.extract("nothing here to see") }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED } @Test fun `uncompressed base45 string fails with HC_ZLIB_DECOMPRESSION_FAILED`() { - shouldThrow<InvalidTestCertificateException> { + shouldThrow<InvalidHealthCertificateException> { extractor.extract("6BFOABCDEFGHIJKLMNOPQRSTUVWXYZ %*+-./:") }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED } @@ -117,13 +117,13 @@ class TestCertificateQRCodeExtractorTest : BaseTest() { @Test fun `vaccination certificate fails with NO_TEST_ENTRY`() { shouldThrow<InvalidTestCertificateException> { - extractor.extract(VaccinationQrCodeTestData.certificateMissing) + extractor.extract(VaccinationQrCodeTestData.certificateMissing, mode = DccV1Parser.Mode.CERT_TEST_STRICT) }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY } @Test fun `null values fail with JSON_SCHEMA_INVALID`() { - shouldThrow<InvalidTestCertificateException> { + shouldThrow<InvalidHealthCertificateException> { extractor.extract(TestData.qrCodeMssingValues) }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt index 4d018467e..83d374bb7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt @@ -3,8 +3,8 @@ package de.rki.coronawarnapp.covidcertificate.test.execution import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.CovidCertificateConfig +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode -import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateComponents import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateServer import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData @@ -35,7 +35,7 @@ class TestCertificateProcessorTest : BaseTest() { @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var certificateServer: TestCertificateServer @MockK lateinit var rsaCryptography: RSACryptography - @MockK lateinit var qrCodeExtractor: TestCertificateQRCodeExtractor + @MockK lateinit var qrCodeExtractor: DccQrCodeExtractor @MockK lateinit var appConfigProvider: AppConfigProvider @MockK lateinit var appConfigData: ConfigData @MockK lateinit var covidTestCertificateConfig: CovidCertificateConfig.TestCertificate @@ -82,7 +82,12 @@ class TestCertificateProcessorTest : BaseTest() { every { rsaCryptography.decrypt(any(), any()) } returns ByteString.Companion.EMPTY - coEvery { qrCodeExtractor.extract(any(), any()) } returns mockk<TestCertificateQRCode>().apply { + coEvery { + qrCodeExtractor.extractEncrypted( + any<ByteArray>(), + any() + ) + } returns mockk<TestCertificateQRCode>().apply { every { qrCode } returns "qrCode" every { data } returns mockk() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java index a4aa6186f..54c251877 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java @@ -28,3 +28,5 @@ public class VaccinationQrCodeTestData { // vaccination date (`dt`) with real time information static public String passDatesWithRealTimeInfo = "HC1:6BFS80J80T9WTWGSLKC 4759S-JXYFZJU:6MFBBF+5*70HS8FN07LCK.KWY0LACLJED97TK0F90$PC5$CUZCY$5Y$5TPCBEC7ZKW.CUEEY3EAECWGDMXG2QDUW5*MEWUMLPCG/DD3E01ALB81C9%NASB9MN951A JC6/DYOACEC+EDR/OLECMPCG/D8EDETAG+9*NAWB8JPCT3E5JDKA7Q47%964W5-A67:EDOL9WEQDD+Q6TW6FA7C466KCK9E2H9G:6V6BEM6Q$D.UDRYA 96OF6L/5SW6KB7B$D% D3IA4W5646946846.96XJC +D3KC.SCXJCQWEF83846Y969464W5K57.964G72A6646VK5XF6646WJCT3E 6A%JCXQEIN8G/D6LE ZDQZCAJB0LEE4F0ECOPCY8FHZA1+9LZAZM81G72A6 6A2G7O/5L*8U%61H8E46H%6SG8H%6M*8:G85S8D46DM8+A8BM8-Q68+8O98J41PL2X+2P4OJ1K2:M7GKVJIG%MZ1TC 8X:6UL2C88TL9T DNKP/W5Z3QZ3T/EQHU7A78$JD0TPGDVYW778PC48+ADC%9:DHWCJ55"; } + + diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt index 8bd1d7837..88126d681 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt @@ -2,8 +2,8 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core import dagger.Component import dagger.Module -import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractorTest -import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQrCodeValidatorTest +import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.DccQrCodeExtractorTest +import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.DccQrCodeValidatorTest import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.VaccinationRepositoryTest import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationContainerTest import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationStorageTest @@ -21,10 +21,10 @@ interface VaccinationTestComponent { fun inject(testClass: VaccinationStorageTest) fun inject(testClass: VaccinationContainerTest) - fun inject(testClass: VaccinationQRCodeExtractorTest) + fun inject(testClass: DccQrCodeExtractorTest) fun inject(testClass: VaccinatedPersonTest) fun inject(testClass: VaccinationRepositoryTest) - fun inject(testClass: VaccinationQrCodeValidatorTest) + fun inject(testClass: DccQrCodeValidatorTest) @Component.Factory interface Factory { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt index be9583f22..4811bef61 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt @@ -1,19 +1,21 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core -import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc +import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData import de.rki.coronawarnapp.covidcertificate.common.certificate.DccHeader -import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1 +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1 +import de.rki.coronawarnapp.covidcertificate.common.certificate.VaccinationDccV1 import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode -import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinatedPersonData import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationContainer import org.joda.time.Instant +import org.joda.time.LocalDate import javax.inject.Inject @Suppress("MaxLineLength") class VaccinationTestData @Inject constructor( - private var qrCodeExtractor: VaccinationQRCodeExtractor, + private var qrCodeExtractor: DccQrCodeExtractor, ) { // AndreasAstra1.pdf @@ -22,26 +24,30 @@ class VaccinationTestData @Inject constructor( val personAVac1Certificate = VaccinationDccV1( version = "1.0.0", - nameData = Dcc.NameData( + nameData = DccV1.NameData( givenName = "Andreas", givenNameStandardized = "ANDREAS", familyName = "Astrá Eins", familyNameStandardized = "ASTRA<EINS", ), - dob = "1966-11-11", - payloads = listOf( - VaccinationDccV1.VaccinationData( - targetId = "840539006", - vaccineId = "1119305005", - medicalProductId = "EU/1/21/1529", - marketAuthorizationHolderId = "ORG-100001699", - doseNumber = 1, - totalSeriesOfDoses = 2, - dt = "2021-03-01", - certificateCountry = "DE", - certificateIssuer = "Bundesministerium für Gesundheit - Test01", - uniqueCertificateIdentifier = "01DE/00001/1119305005/7T1UG87G61Y7NRXIBQJDTYQ9#S", - ) + dateOfBirth = LocalDate.parse("1966-11-11"), + vaccination = + DccV1.VaccinationData( + targetId = "840539006", + vaccineId = "1119305005", + medicalProductId = "EU/1/21/1529", + marketAuthorizationHolderId = "ORG-100001699", + doseNumber = 1, + totalSeriesOfDoses = 2, + dt = "2021-03-01", + certificateCountry = "DE", + certificateIssuer = "Bundesministerium für Gesundheit - Test01", + uniqueCertificateIdentifier = "01DE/00001/1119305005/7T1UG87G61Y7NRXIBQJDTYQ9#S", + ), + personIdentifier = CertificatePersonIdentifier( + dateOfBirth = LocalDate.parse("1966-11-11"), + lastNameStandardized = "ASTRA<EINS", + firstNameStandardized = "ANDREAS" ) ) @@ -74,26 +80,29 @@ class VaccinationTestData @Inject constructor( val personAVac2Certificate = VaccinationDccV1( version = "1.0.0", - nameData = Dcc.NameData( + nameData = DccV1.NameData( givenName = "Andreas", givenNameStandardized = "ANDREAS", familyName = "Astrá Eins", familyNameStandardized = "ASTRA<EINS", ), - dob = "1966-11-11", - payloads = listOf( - VaccinationDccV1.VaccinationData( - targetId = "840539006", - vaccineId = "1119305005", - medicalProductId = "EU/1/21/1529", - marketAuthorizationHolderId = "ORG-100001699", - doseNumber = 2, - totalSeriesOfDoses = 2, - dt = "2021-04-27", - certificateCountry = "DE", - certificateIssuer = "Bundesministerium für Gesundheit - Test01", - uniqueCertificateIdentifier = "01DE/00001/1119305005/6IPYBAIDWEWRWW73QEP92FQSN#S", - ) + dateOfBirth = LocalDate.parse("1966-11-11"), + vaccination = DccV1.VaccinationData( + targetId = "840539006", + vaccineId = "1119305005", + medicalProductId = "EU/1/21/1529", + marketAuthorizationHolderId = "ORG-100001699", + doseNumber = 2, + totalSeriesOfDoses = 2, + dt = "2021-04-27", + certificateCountry = "DE", + certificateIssuer = "Bundesministerium für Gesundheit - Test01", + uniqueCertificateIdentifier = "01DE/00001/1119305005/6IPYBAIDWEWRWW73QEP92FQSN#S", + ), + personIdentifier = CertificatePersonIdentifier( + dateOfBirth = LocalDate.parse("1966-11-11"), + lastNameStandardized = "ASTRA<EINS", + firstNameStandardized = "ANDREAS" ) ) @@ -130,26 +139,29 @@ class VaccinationTestData @Inject constructor( val personBVac1Certificate = VaccinationDccV1( version = "1.0.0", - nameData = Dcc.NameData( + nameData = DccV1.NameData( givenName = "Boris", givenNameStandardized = "BORIS", familyName = "Johnson Gültig", familyNameStandardized = "JOHNSON<GUELTIG", ), - dob = "1966-11-11", - payloads = listOf( - VaccinationDccV1.VaccinationData( - targetId = "840539006", - vaccineId = "1119305005", - medicalProductId = "EU/1/20/1525", - marketAuthorizationHolderId = "ORG-100001417", - doseNumber = 1, - totalSeriesOfDoses = 1, - dt = "2021-04-20", - certificateCountry = "DE", - certificateIssuer = "Bundesministerium für Gesundheit - Test01", - uniqueCertificateIdentifier = "01DE/00001/1119305005/3H24U2KVOTPCSINK7N64F2OB9#S", - ) + dateOfBirth = LocalDate.parse("1966-11-11"), + vaccination = DccV1.VaccinationData( + targetId = "840539006", + vaccineId = "1119305005", + medicalProductId = "EU/1/20/1525", + marketAuthorizationHolderId = "ORG-100001417", + doseNumber = 1, + totalSeriesOfDoses = 1, + dt = "2021-04-20", + certificateCountry = "DE", + certificateIssuer = "Bundesministerium für Gesundheit - Test01", + uniqueCertificateIdentifier = "01DE/00001/1119305005/3H24U2KVOTPCSINK7N64F2OB9#S", + ), + personIdentifier = CertificatePersonIdentifier( + dateOfBirth = LocalDate.parse("1966-11-11"), + lastNameStandardized = "JOHNSON<GUELTIG", + firstNameStandardized = "BORIS" ) ) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeExtractorTest.kt similarity index 76% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeExtractorTest.kt index 3b07c2bc3..fe132556f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeExtractorTest.kt @@ -1,11 +1,16 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode -import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_RECOVERY_ENTRY +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_VACCINATION_ENTRY +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidRecoveryCertificateException +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationQrCodeTestData @@ -19,9 +24,9 @@ import org.junit.jupiter.api.Test import testhelpers.BaseTest import javax.inject.Inject -class VaccinationQRCodeExtractorTest : BaseTest() { +class DccQrCodeExtractorTest : BaseTest() { - @Inject lateinit var extractor: VaccinationQRCodeExtractor + @Inject lateinit var extractor: DccQrCodeExtractor @Inject lateinit var vaccinationTestData: VaccinationTestData @BeforeEach @@ -36,12 +41,15 @@ class VaccinationQRCodeExtractorTest : BaseTest() { @Test fun `happy path extraction 2`() { - extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode2, mode = Mode.CERT_VAC_STRICT) + extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode2) } @Test fun `happy path extraction with data`() { - val qrCode = extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode3, mode = Mode.CERT_VAC_STRICT) + val qrCode = extractor.extract( + VaccinationQrCodeTestData.validVaccinationQrCode3, + mode = Mode.CERT_VAC_STRICT + ) as VaccinationCertificateQRCode with(qrCode.data.header) { issuer shouldBe "AT" @@ -56,11 +64,10 @@ class VaccinationQRCodeExtractorTest : BaseTest() { givenName shouldBe "Gabriele" givenNameStandardized shouldBe "GABRIELE" } - dob shouldBe "1998-02-26" dateOfBirth shouldBe LocalDate.parse("1998-02-26") version shouldBe "1.0.0" - with(payloads[0]) { + with(vaccination) { uniqueCertificateIdentifier shouldBe "urn:uvci:01:AT:10807843F94AEE0EE5093FBC254BD813P" certificateCountry shouldBe "AT" doseNumber shouldBe 1 @@ -86,7 +93,7 @@ class VaccinationQRCodeExtractorTest : BaseTest() { @Test fun `valid encoding but not a health certificate fails with HC_CWT_NO_ISS`() { - shouldThrow<InvalidVaccinationCertificateException> { + shouldThrow<InvalidHealthCertificateException> { extractor.extract( VaccinationQrCodeTestData.validEncoded, mode = Mode.CERT_VAC_STRICT @@ -121,7 +128,7 @@ class VaccinationQRCodeExtractorTest : BaseTest() { VaccinationQrCodeTestData.certificateMissing, mode = Mode.CERT_VAC_STRICT ) - }.errorCode shouldBe VC_NO_VACCINATION_ENTRY + }.errorCode shouldBe NO_VACCINATION_ENTRY } @Test @@ -167,7 +174,7 @@ class VaccinationQRCodeExtractorTest : BaseTest() { val qrCode = extractor.extract( VaccinationQrCodeTestData.qrCodeBulgaria, mode = Mode.CERT_VAC_STRICT - ) + ) as VaccinationCertificateQRCode with(qrCode.data.header) { issuer shouldBe "BG" issuedAt shouldBe Instant.parse("2021-06-02T14:07:56.000Z") @@ -181,11 +188,10 @@ class VaccinationQRCodeExtractorTest : BaseTest() { givenName shouldBe "СТÐМО ГЕОРГИЕВ" givenNameStandardized shouldBe "STAMO<GEORGIEV" } - dob shouldBe "1978-01-26T00:00:00" dateOfBirth shouldBe LocalDate.parse("1978-01-26") version shouldBe "1.0.0" - payload.apply { + vaccination.apply { uniqueCertificateIdentifier shouldBe "urn:uvci:01:BG:UFR5PLGKU8WDSZK7#0" certificateCountry shouldBe "BG" doseNumber shouldBe 2 @@ -243,8 +249,9 @@ class VaccinationQRCodeExtractorTest : BaseTest() { VaccinationQrCodeTestData.passGermanReferenceCase, mode = Mode.CERT_VAC_STRICT ).apply { + this as VaccinationCertificateQRCode data.certificate.dateOfBirth shouldBe LocalDate.parse("1964-08-12") - data.certificate.payload.vaccinatedAt shouldBe LocalDate.parse("2021-05-29") + data.certificate.vaccination.vaccinatedAt shouldBe LocalDate.parse("2021-05-29") } } @@ -254,8 +261,9 @@ class VaccinationQRCodeExtractorTest : BaseTest() { VaccinationQrCodeTestData.passDatesWithTimeAtMidnight, mode = Mode.CERT_VAC_STRICT ).apply { + this as VaccinationCertificateQRCode data.certificate.dateOfBirth shouldBe LocalDate.parse("1978-01-26") - data.certificate.payload.vaccinatedAt shouldBe LocalDate.parse("2021-03-09") + data.certificate.vaccination.vaccinatedAt shouldBe LocalDate.parse("2021-03-09") } } @@ -265,8 +273,54 @@ class VaccinationQRCodeExtractorTest : BaseTest() { VaccinationQrCodeTestData.passDatesWithRealTimeInfo, mode = Mode.CERT_VAC_STRICT ).apply { + this as VaccinationCertificateQRCode data.certificate.dateOfBirth shouldBe LocalDate.parse("1958-11-11") - data.certificate.payload.vaccinatedAt shouldBe LocalDate.parse("2021-03-18") + data.certificate.vaccination.vaccinatedAt shouldBe LocalDate.parse("2021-03-18") } } + + @Test + fun `happy path extraction recovery`() { + extractor.extract( + RecoveryQrCodeTestData.validRecovery, + ) + } + + @Test + fun `happy path extraction recovery with strict mode`() { + extractor.extract( + RecoveryQrCodeTestData.validRecovery, + mode = Mode.CERT_REC_STRICT + ) + } + + @Test + fun `recovery cert fails in mode CERT_VAC_STRICT`() { + shouldThrow<InvalidVaccinationCertificateException> { + extractor.extract( + RecoveryQrCodeTestData.validRecovery, + mode = Mode.CERT_VAC_STRICT + ) + }.errorCode shouldBe NO_VACCINATION_ENTRY + } + + @Test + fun `recovery cert fails in mode CERT_TEST_STRICT`() { + shouldThrow<InvalidTestCertificateException> { + extractor.extract( + RecoveryQrCodeTestData.validRecovery, + mode = Mode.CERT_TEST_STRICT + ) + }.errorCode shouldBe NO_TEST_ENTRY + } + + @Test + fun `vaccination cert fails in mode CERT_REC_STRICT`() { + shouldThrow<InvalidRecoveryCertificateException> { + extractor.extract( + VaccinationQrCodeTestData.validVaccinationQrCode, + mode = Mode.CERT_REC_STRICT + ) + }.errorCode shouldBe NO_RECOVERY_ENTRY + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidatorTest.kt new file mode 100644 index 000000000..f91781522 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidatorTest.kt @@ -0,0 +1,46 @@ +package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode + +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException +import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent +import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationTestData +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import javax.inject.Inject + +class DccQrCodeValidatorTest : BaseTest() { + @Inject lateinit var testData: VaccinationTestData + @Inject lateinit var vacExtractor: DccQrCodeExtractor + private lateinit var vacExtractorSpy: DccQrCodeExtractor + + @BeforeEach + fun setup() { + DaggerVaccinationTestComponent.factory().create().inject(this) + + vacExtractorSpy = spyk(vacExtractor) + } + + @Test + fun `validator uses strict extraction mode`() { + val instance = DccQrCodeValidator(vacExtractorSpy) + instance.validate(testData.personAVac1QRCodeString).apply { + uniqueCertificateIdentifier shouldBe testData.personAVac1Container.certificateId + } + verify { vacExtractorSpy.extract(testData.personAVac1QRCodeString, DccV1Parser.Mode.CERT_SINGLE_STRICT) } + } + + @Test + fun `validator throws invalid vaccination exception for pcr test qr code`() { + val instance = DccQrCodeValidator(vacExtractorSpy) + shouldThrow<InvalidVaccinationCertificateException> { + instance.validate("HTTPS://LOCALHOST/?123456-12345678-1234-4DA7-B166-B86D85475064") + }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.PREFIX_INVALID + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/RecoveryQrCodeTestData.java b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/RecoveryQrCodeTestData.java new file mode 100644 index 000000000..3abe166d4 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/RecoveryQrCodeTestData.java @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode; + +public class RecoveryQrCodeTestData { + public static String validRecovery = "HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH-XIIOO6+IGURR%28WA-JHR6N+QI6M8SA3/-2E%5UR5+VBZXIFNJ1VCSWC1QDRCKH9CJZIS/1VY9X5QF36FY1OSMNV1L8VNF6O M9R1VNM8.MM.EM M%/EJ.E%+MXM6Q*ER56CL6F8E4IMI%6NJM3VE1*AA/98T5UEIY0Q$UPR$5:NLOEPNRAE69K P4NPDDAJP5DMH1$4D-I/2DBAJDAJCNB-437Y4-U2UYLX76SW6B699D9ISU39GEA7IB6$C94JB2E9Z3E8AE-QD+PB.QCD-H/8O3BEQ8L9VN.6A4JBLHLXHQT*QR$M% OP*B9YOB*16CQJN9TW5F/94O5 9E6UEDTUD1VVY95CQ-8EDS9%PP%.P3Y9UM97H98$Q PQ4T9AKPCPP0%M0YMBXR7+21KUO8UFGKSBV1/C.4W2MEGHPH:UM-JZ535IT1$1KQQQFF+ C5BBPHT/.VJ*H71I3-EIJ9Q 5P+DQ3K%USN7MS6L2VHX+KOD9VODFL96Y0441:*G"; +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt deleted file mode 100644 index 1a169d430..000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode - -import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode -import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent -import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationTestData -import io.kotest.matchers.shouldBe -import io.mockk.spyk -import io.mockk.verify -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import testhelpers.BaseTest -import javax.inject.Inject - -class VaccinationQrCodeValidatorTest : BaseTest() { - @Inject lateinit var testData: VaccinationTestData - @Inject lateinit var vacExtractor: VaccinationQRCodeExtractor - private lateinit var vacExtractorSpy: VaccinationQRCodeExtractor - - @BeforeEach - fun setup() { - DaggerVaccinationTestComponent.factory().create().inject(this) - - vacExtractorSpy = spyk(vacExtractor) - } - - @Test - fun `validator uses strict extraction mode`() { - val instance = VaccinationQRCodeValidator(vacExtractorSpy) - instance.validate(testData.personAVac1QRCodeString).apply { - uniqueCertificateIdentifier shouldBe testData.personAVac1Container.certificateId - } - verify { vacExtractorSpy.extract(testData.personAVac1QRCodeString, Mode.CERT_VAC_STRICT) } - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepositoryTest.kt index 107dae36a..fabc048ab 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepositoryTest.kt @@ -1,10 +1,10 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.repository -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.ALREADY_REGISTERED import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationTestData -import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.errors.VaccinationCertificateNotFoundException import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinatedPersonData import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationStorage @@ -35,7 +35,7 @@ class VaccinationRepositoryTest : BaseTest() { @MockK lateinit var storage: VaccinationStorage @MockK lateinit var valueSetsRepository: ValueSetsRepository @MockK lateinit var vaccinationValueSet: VaccinationValueSets - @MockK lateinit var qrCodeExtractor: VaccinationQRCodeExtractor + @MockK lateinit var qrCodeExtractor: DccQrCodeExtractor private var testStorage: Set<VaccinatedPersonData> = emptySet() @@ -66,7 +66,7 @@ class VaccinationRepositoryTest : BaseTest() { timeStamper = timeStamper, storage = storage, valueSetsRepository = valueSetsRepository, - vaccinationQRCodeExtractor = qrCodeExtractor, + qrCodeExtractor = qrCodeExtractor, ) @Test @@ -134,7 +134,7 @@ class VaccinationRepositoryTest : BaseTest() { shouldThrow<InvalidVaccinationCertificateException> { instance.registerVaccination(vaccinationTestData.personAVac1QRCode) - }.errorCode shouldBe VC_ALREADY_REGISTERED + }.errorCode shouldBe ALREADY_REGISTERED testStorage.first() shouldBe dataBefore } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt index 2f27974c9..486398e30 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt @@ -1,11 +1,11 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage -import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationTestData import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode -import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.DefaultValueSet import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.VaccinationValueSets import io.kotest.matchers.shouldBe @@ -151,8 +151,13 @@ class VaccinationContainerTest : BaseTest() { vaccinationQrCode = testData.personYVacTwoEntriesQrCode, scannedAt = Instant.EPOCH ) - val extractor = mockk<VaccinationQRCodeExtractor>().apply { - every { extract(any(), any()) } returns mockk<VaccinationCertificateQRCode>().apply { + val extractor = mockk<DccQrCodeExtractor>().apply { + every { + extract( + any(), + DccV1Parser.Mode.CERT_VAC_LENIENT + ) + } returns mockk<VaccinationCertificateQRCode>().apply { every { data } returns mockk() } } @@ -160,11 +165,11 @@ class VaccinationContainerTest : BaseTest() { container.certificateData shouldNotBe null - verify { extractor.extract(testData.personYVacTwoEntriesQrCode, QrCodeExtractor.Mode.CERT_VAC_LENIENT) } + verify { extractor.extract(testData.personYVacTwoEntriesQrCode, DccV1Parser.Mode.CERT_VAC_LENIENT) } } @Test fun `gracefully handle semi invalid data - multiple entries`() { - testData.personYVacTwoEntriesContainer.certificate.payloads.size shouldBe 1 + testData.personYVacTwoEntriesContainer.certificate.vaccination } } -- GitLab