From 97d40f26d01930a2b6c31ec79a06b8c81370ac47 Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Thu, 10 Jun 2021 20:03:13 +0200 Subject: [PATCH] Handle semi-invalid vaccination certificate QR-codes (EXPOSUREAPP-7755) (#3414) * Introduce qrcode data parsing modes. UI based parsing will be STRICT and show error codes. Storage based parsing will be LENIENT. If the user managed to scan something invalid, then we can neither just delete it, nor just crash the app. * LINTs * Fix plural typo. Co-authored-by: Mohamed Metwalli <mohamed.metwalli@sap.com> --- .../qrcode/CoronaTestQRCodeValidator.kt | 10 +++- .../coronatest/qrcode/PcrQrCodeExtractor.kt | 2 +- .../qrcode/RapidAntigenQrCodeExtractor.kt | 2 +- .../InvalidHealthCertificateException.kt | 5 ++ .../certificate/VaccinationDGCV1Parser.kt | 37 ++++++++----- .../core/qrcode/VaccinationQRCodeExtractor.kt | 8 +-- .../core/qrcode/VaccinationQRCodeValidator.kt | 2 +- .../storage/VaccinationContainer.kt | 3 +- .../qrcode/CoronaTestQrCodeValidatorTest.kt | 20 +++++-- .../qrcode/PcrQrCodeExtractorTest.kt | 48 +++++++++++++---- .../qrcode/RapidAntigenQrCodeExtractorTest.kt | 11 ++-- .../core/VaccinationTestComponent.kt | 2 + .../vaccination/core/VaccinationTestData.kt | 10 ++++ .../qrcode/VaccinationQRCodeExtractorTest.kt | 52 ++++++++++++++----- .../qrcode/VaccinationQrCodeValidatorTest.kt | 34 ++++++++++++ .../storage/VaccinationContainerTest.kt | 28 ++++++++++ 16 files changed, 221 insertions(+), 53 deletions(-) create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt 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 7b31a9773..4f12913e4 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) + ?.extract(rawString, mode = QrCodeExtractor.Mode.TEST_STRICT) ?.also { Timber.i("Extracted data from QR code is %s", it) } ?: throw InvalidQRCodeException() } @@ -25,5 +25,11 @@ class CoronaTestQrCodeValidator @Inject constructor( interface QrCodeExtractor<T> { fun canHandle(rawString: String): Boolean - fun extract(rawString: String): T + fun extract(rawString: String, mode: Mode): T + + enum class Mode { + TEST_STRICT, + CERT_VAC_STRICT, + CERT_VAC_LENIENT + } } 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 49633e3de..c94eb873f 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): CoronaTestQRCode.PCR { + override fun extract(rawString: String, mode: QrCodeExtractor.Mode): 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 c5881f973..96dd01a71 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): CoronaTestQRCode.RapidAntigen { + override fun extract(rawString: String, mode: QrCodeExtractor.Mode): 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/vaccination/core/certificate/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt index 39061747c..14110a6f2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt @@ -19,6 +19,7 @@ import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificat import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID +import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_MULTIPLE_VACCINATION_ENTRIES import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID @@ -36,6 +37,7 @@ class InvalidHealthCertificateException( 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."), VC_PREFIX_INVALID("Prefix invalid."), VC_STORING_FAILED("Storing failed."), VC_JSON_SCHEMA_INVALID("Json schema invalid."), @@ -73,6 +75,9 @@ class InvalidHealthCertificateException( VC_NO_VACCINATION_ENTRY -> CachedString { context -> context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) } + VC_MULTIPLE_VACCINATION_ENTRIES -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) + } VC_STORING_FAILED -> CachedString { context -> context.getString(ERROR_MESSAGE_VC_SCAN_AGAIN) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt index e25defbc9..be296d648 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt @@ -9,7 +9,9 @@ import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificat import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_DGC import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID +import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_MULTIPLE_VACCINATION_ENTRIES import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY +import timber.log.Timber import javax.inject.Inject @Reusable @@ -17,35 +19,44 @@ class VaccinationDGCV1Parser @Inject constructor( @BaseGson private val gson: Gson ) { - fun parse(map: CBORObject): VaccinationDGCV1 = try { + fun parse(map: CBORObject, lenient: Boolean): VaccinationDGCV1 = try { val certificate: VaccinationDGCV1 = map[keyHCert]?.run { this[keyEuDgcV1]?.run { toCertificate() } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_DGC) } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_HCERT) - certificate.validate() + certificate.toValidated(lenient) } catch (e: InvalidHealthCertificateException) { throw e } catch (e: Throwable) { throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED) } - private fun VaccinationDGCV1.validate(): VaccinationDGCV1 { - if (vaccinationDatas.isEmpty()) { - throw InvalidHealthCertificateException(VC_NO_VACCINATION_ENTRY) + private fun VaccinationDGCV1.toValidated(lenient: Boolean): VaccinationDGCV1 = this + .run { + if (vaccinationDatas.isEmpty()) throw InvalidHealthCertificateException(VC_NO_VACCINATION_ENTRY) + + if (vaccinationDatas.size == 1) return@run this + + if (lenient) { + Timber.w("Lenient: Vaccination data contained multiple entries.") + copy(vaccinationDatas = listOf(vaccinationDatas.maxByOrNull { it.vaccinatedAt }!!)) + } else { + throw InvalidHealthCertificateException(VC_MULTIPLE_VACCINATION_ENTRIES) + } } - // Force date parsing - dateOfBirth - vaccinationDatas.forEach { - it.vaccinatedAt + .also { + // Force date parsing + dateOfBirth + vaccinationDatas.forEach { + it.vaccinatedAt + } } - return this - } - private fun CBORObject.toCertificate() = try { + private fun CBORObject.toCertificate(): VaccinationDGCV1 = try { val json = ToJSONString() - gson.fromJson<VaccinationDGCV1>(json) + gson.fromJson(json) } catch (e: Throwable) { throw InvalidHealthCertificateException(VC_JSON_SCHEMA_INVALID) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt index ae3065df3..d1d9496bf 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt @@ -22,14 +22,14 @@ class VaccinationQRCodeExtractor @Inject constructor( override fun canHandle(rawString: String): Boolean = rawString.startsWith(PREFIX) - override fun extract(rawString: String): VaccinationCertificateQRCode { + override fun extract(rawString: String, mode: QrCodeExtractor.Mode): VaccinationCertificateQRCode { CertificateQrCodeCensor.addQRCodeStringToCensor(rawString) val parsedData = rawString .removePrefix(PREFIX) .decodeBase45() .decompress() - .parse() + .parse(lenient = mode == QrCodeExtractor.Mode.CERT_VAC_LENIENT) return VaccinationCertificateQRCode( parsedData = parsedData, @@ -51,13 +51,13 @@ class VaccinationQRCodeExtractor @Inject constructor( throw InvalidHealthCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) } - fun RawCOSEObject.parse(): VaccinationCertificateData { + fun RawCOSEObject.parse(lenient: Boolean): VaccinationCertificateData { Timber.v("Parsing COSE for vaccination certificate.") val cbor = coseDecoder.decode(this) return VaccinationCertificateData( header = headerParser.parse(cbor), - certificate = bodyParser.parse(cbor) + certificate = bodyParser.parse(cbor, lenient = lenient) ).also { CertificateQrCodeCensor.addCertificateToCensor(it) }.also { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt index 3e7f60396..aefdff6e5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt @@ -17,7 +17,7 @@ class VaccinationQRCodeValidator @Inject constructor( // If there is more than one "extractor" in the future, check censoring again. // CertificateQrCodeCensor.addQRCodeStringToCensor(rawString) return findExtractor(rawString) - ?.extract(rawString) + ?.extract(rawString, mode = QrCodeExtractor.Mode.CERT_VAC_STRICT) ?.also { Timber.i("Extracted data from QR code is %s", it) } ?: throw InvalidHealthCertificateException(VC_PREFIX_INVALID) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt index 9694f5f9d..7914dd278 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.vaccination.core.repository.storage import androidx.annotation.Keep import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate import de.rki.coronawarnapp.vaccination.core.certificate.CoseCertificateHeader @@ -33,7 +34,7 @@ data class VaccinationContainer internal constructor( @delegate:Transient internal val certificateData: VaccinationCertificateData by lazy { - preParsedData ?: qrCodeExtractor.extract(vaccinationQrCode).parsedData + preParsedData ?: qrCodeExtractor.extract(vaccinationQrCode, mode = Mode.CERT_VAC_LENIENT).parsedData } val header: CoseCertificateHeader 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 f9665d0af..e5cdb3f4c 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,16 +1,21 @@ 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 +import io.mockk.spyk +import io.mockk.verify import org.junit.jupiter.api.Test import testhelpers.BaseTest class CoronaTestQrCodeValidatorTest : BaseTest() { + private val raExtractor = spyk(RapidAntigenQrCodeExtractor()) + private val pcrExtractor = spyk(PcrQrCodeExtractor()) @Test fun `valid codes are extracted by corresponding extractor`() { - val instance = CoronaTestQrCodeValidator(RapidAntigenQrCodeExtractor(), PcrQrCodeExtractor()) + val instance = CoronaTestQrCodeValidator(raExtractor, pcrExtractor) instance.validate(pcrQrCode1).type shouldBe CoronaTest.Type.PCR instance.validate(pcrQrCode2).type shouldBe CoronaTest.Type.PCR instance.validate(pcrQrCode3).type shouldBe CoronaTest.Type.PCR @@ -22,7 +27,7 @@ class CoronaTestQrCodeValidatorTest : BaseTest() { @Test fun `invalid prefix throws exception`() { val invalidCode = "HTTPS://somethingelse/?123456-12345678-1234-4DA7-B166-B86D85475064" - val instance = CoronaTestQrCodeValidator(RapidAntigenQrCodeExtractor(), PcrQrCodeExtractor()) + val instance = CoronaTestQrCodeValidator(raExtractor, pcrExtractor) shouldThrow<InvalidQRCodeException> { instance.validate(invalidCode) } @@ -31,9 +36,18 @@ class CoronaTestQrCodeValidatorTest : BaseTest() { @Test fun `invalid json throws exception`() { val invalidCode = "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2" - val instance = CoronaTestQrCodeValidator(RapidAntigenQrCodeExtractor(), PcrQrCodeExtractor()) + val instance = CoronaTestQrCodeValidator(raExtractor, pcrExtractor) shouldThrow<InvalidQRCodeException> { instance.validate(invalidCode) } } + + @Test + 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) } + instance.validate(raQrCode1).type shouldBe CoronaTest.Type.RAPID_ANTIGEN + verify { raExtractor.extract(raQrCode1, Mode.TEST_STRICT) } + } } 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 e4537ff5a..86e181271 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,5 +1,6 @@ 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 @@ -16,7 +17,7 @@ class PcrQrCodeExtractorTest : BaseTest() { val extractor = PcrQrCodeExtractor() try { if (extractor.canHandle("$prefixString$guid")) { - extractor.extract("$prefixString$guid") + extractor.extract("$prefixString$guid", mode = TEST_STRICT) conditionToMatch shouldBe true } else { conditionToMatch shouldBe false @@ -77,16 +78,43 @@ class PcrQrCodeExtractorTest : BaseTest() { @Test fun extractGUID() { - PcrQrCodeExtractor().extract("$localhostUpperCase$guidUpperCase").qrCodeGUID shouldBe guidUpperCase - PcrQrCodeExtractor().extract("$localhostUpperCase$guidLowerCase").qrCodeGUID shouldBe guidLowerCase - PcrQrCodeExtractor().extract("$localhostUpperCase$guidMixedCase").qrCodeGUID shouldBe guidMixedCase + 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").qrCodeGUID shouldBe guidUpperCase - PcrQrCodeExtractor().extract("$localhostLowerCase$guidLowerCase").qrCodeGUID shouldBe guidLowerCase - PcrQrCodeExtractor().extract("$localhostLowerCase$guidMixedCase").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").qrCodeGUID shouldBe guidUpperCase - PcrQrCodeExtractor().extract("$localhostMixedCase$guidLowerCase").qrCodeGUID shouldBe guidLowerCase - PcrQrCodeExtractor().extract("$localhostMixedCase$guidMixedCase").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 e78658b69..4a746b6ed 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,5 +1,6 @@ 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 @@ -29,13 +30,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) + instance.extract(it, mode = TEST_STRICT) } } @Test fun `personal data is extracted`() { - val data = instance.extract(raQrCode3) + val data = instance.extract(raQrCode3, mode = TEST_STRICT) data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN data.hash shouldBe "7dce08db0d4abd5ac1d2498b571afb221ca947c75c847d05466b4cfe9d95dc66" data.createdAt shouldBe Instant.ofEpochMilli(1619618352000) @@ -46,7 +47,7 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() { @Test fun `empty strings are treated as null or notset`() { - val data = instance.extract(raQrCodeEmptyStrings) + val data = instance.extract(raQrCodeEmptyStrings, mode = TEST_STRICT) data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN data.hash shouldBe "d6e4d0181d8109bf05b346a0d2e0ef0cc472eed70d9df8c4b9ae5c7a009f3e34" data.createdAt shouldBe Instant.ofEpochMilli(1619012952000) @@ -57,14 +58,14 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() { @Test fun `personal data is only valid if complete or completely missing`() { - shouldThrow<InvalidQRCodeException> { instance.extract(raQrIncompletePersonalData) } + shouldThrow<InvalidQRCodeException> { instance.extract(raQrIncompletePersonalData, mode = TEST_STRICT) } } @Test fun `invalid json throws exception`() { val invalidCode = "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2" shouldThrow<InvalidQRCodeException> { - RapidAntigenQrCodeExtractor().extract(invalidCode) + RapidAntigenQrCodeExtractor().extract(invalidCode, mode = TEST_STRICT) } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestComponent.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestComponent.kt index 181970bd6..7f80b1fbb 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestComponent.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestComponent.kt @@ -4,6 +4,7 @@ import dagger.Component import dagger.Module import de.rki.coronawarnapp.util.serialization.SerializationModule import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractorTest +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQrCodeValidatorTest import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepositoryTest import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationContainerTest import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationStorageTest @@ -23,6 +24,7 @@ interface VaccinationTestComponent { fun inject(testClass: VaccinationQRCodeExtractorTest) fun inject(testClass: VaccinatedPersonTest) fun inject(testClass: VaccinationRepositoryTest) + fun inject(testClass: VaccinationQrCodeValidatorTest) @Component.Factory interface Factory { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt index 0c5117d63..9ff6c95e5 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt @@ -184,4 +184,14 @@ class VaccinationTestData @Inject constructor( ).apply { qrCodeExtractor = this@VaccinationTestData.qrCodeExtractor } + + val personYVacTwoEntriesQrCode = + "HC1:6BFOXN%TSMAHN-HVN8J7UQMJ4/36 L-AHQ+R1WG%MP8*ICG5QKM0658WAULO8NASA3/-2E%5G%5TW5A 6YO6XL6Q3QR\$P*NI92KV6TKOJ06JYZJV1JJ7UGOJUTIJ7J:ZJ83BL8TFVTV9T.ZJC0J*PIZ.TJ STPT*IJ5OI9YI:8DJ:D%PDDIKIWCHAB.YMAHLW 70SO:GOLIROGO3T59YLY1S7HOPC5NDOEC5/64ND7BT5PE4D/5:/6N9R%EPXCROGO+GOVIR-PQ395R4IUHLW\$G-B5ET42HPPEPHCR6W97DON95N14Q6SP+PJD1W9L \$N3-Q.VBAO8MN9*QHAO96Y2/*13A5-8E6V59I9BZK6:IZW4I:A6J3ARN QT1BGL4OMJKR.K\$A1EB14UVC2O+5T3.CE1M33KS2JKA8Y*99CCLLOR/CH0GRP8 GLY 1LA7551DC2U.NVOTJOII:8DKEK%N92T9YQ$0MK%P6\$G9K7QQUY9KI.EK*8XRS-DPA5W64SMVR1NF6D0 2S0.7R:ASENTI094PIDS:T32DRE8N" + + val personYVacTwoEntriesContainer = VaccinationContainer( + scannedAt = Instant.ofEpochMilli(1620062834471), + vaccinationQrCode = personYVacTwoEntriesQrCode, + ).apply { + qrCodeExtractor = this@VaccinationTestData.qrCodeExtractor + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt index 1b41ddfa0..0c7670d9a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.vaccination.core.qrcode +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.vaccination.core.VaccinationQrCodeTestData import de.rki.coronawarnapp.vaccination.core.VaccinationTestData @@ -29,17 +30,17 @@ class VaccinationQRCodeExtractorTest : BaseTest() { @Test fun `happy path extraction`() { - extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode) + extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode, mode = Mode.CERT_VAC_STRICT) } @Test fun `happy path extraction 2`() { - extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode2) + extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode2, mode = Mode.CERT_VAC_STRICT) } @Test fun `happy path extraction with data`() { - val qrCode = extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode3) + val qrCode = extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode3, mode = Mode.CERT_VAC_STRICT) with(qrCode.parsedData.header) { issuer shouldBe "AT" @@ -76,52 +77,76 @@ class VaccinationQRCodeExtractorTest : BaseTest() { @Test fun `happy path extraction 4`() { - extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode4) + extractor.extract( + VaccinationQrCodeTestData.validVaccinationQrCode4, + mode = Mode.CERT_VAC_STRICT + ) } @Test fun `valid encoding but not a health certificate fails with VC_HC_CWT_NO_ISS`() { shouldThrow<InvalidHealthCertificateException> { - extractor.extract(VaccinationQrCodeTestData.validEncoded) + extractor.extract( + VaccinationQrCodeTestData.validEncoded, + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe VC_HC_CWT_NO_ISS } @Test fun `random string fails with HC_BASE45_DECODING_FAILED`() { shouldThrow<InvalidHealthCertificateException> { - extractor.extract("nothing here to see") + extractor.extract( + "nothing here to see", + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe HC_BASE45_DECODING_FAILED } @Test fun `uncompressed base45 string fails with HC_ZLIB_DECOMPRESSION_FAILED`() { shouldThrow<InvalidHealthCertificateException> { - extractor.extract("6BFOABCDEFGHIJKLMNOPQRSTUVWXYZ %*+-./:") + extractor.extract( + "6BFOABCDEFGHIJKLMNOPQRSTUVWXYZ %*+-./:", + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe HC_ZLIB_DECOMPRESSION_FAILED } @Test fun `vaccination certificate missing fails with VC_NO_VACCINATION_ENTRY`() { shouldThrow<InvalidHealthCertificateException> { - extractor.extract(VaccinationQrCodeTestData.certificateMissing) + extractor.extract( + VaccinationQrCodeTestData.certificateMissing, + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe VC_NO_VACCINATION_ENTRY } @Test fun `test data person A check`() { - val extracted = extractor.extract(vaccinationTestData.personAVac1QRCodeString) + val extracted = extractor.extract( + vaccinationTestData.personAVac1QRCodeString, + mode = Mode.CERT_VAC_STRICT + ) extracted shouldBe vaccinationTestData.personAVac1QRCode } @Test fun `test data person B check`() { - val extracted = extractor.extract(vaccinationTestData.personBVac1QRCodeString) + val extracted = extractor.extract( + vaccinationTestData.personBVac1QRCodeString, + mode = Mode.CERT_VAC_STRICT + ) extracted shouldBe vaccinationTestData.personBVac1QRCode } @Test fun `Bulgarian qr code passes`() { - val qrCode = extractor.extract(VaccinationQrCodeTestData.qrCodeBulgaria) + val qrCode = extractor.extract( + VaccinationQrCodeTestData.qrCodeBulgaria, + mode = Mode.CERT_VAC_STRICT + ) with(qrCode.parsedData.header) { issuer shouldBe "BG" issuedAt shouldBe Instant.parse("2021-06-02T14:07:56.000Z") @@ -157,6 +182,9 @@ class VaccinationQRCodeExtractorTest : BaseTest() { @Test fun `Swedish qr code passes`() { - extractor.extract(VaccinationQrCodeTestData.qrCodeSweden) + extractor.extract( + VaccinationQrCodeTestData.qrCodeSweden, + mode = Mode.CERT_VAC_STRICT + ) } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt new file mode 100644 index 000000000..9c08a3e0e --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt @@ -0,0 +1,34 @@ +package de.rki.coronawarnapp.vaccination.core.qrcode + +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode +import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent +import de.rki.coronawarnapp.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/vaccination/core/repository/storage/VaccinationContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt index fad39f683..a2817d544 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt @@ -1,12 +1,17 @@ package de.rki.coronawarnapp.vaccination.core.repository.storage +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinationTestData +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationValueSet import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.mockk.every import io.mockk.mockk +import io.mockk.verify import org.joda.time.Instant import org.joda.time.LocalDate import org.junit.jupiter.api.BeforeEach @@ -138,4 +143,27 @@ class VaccinationContainerTest : BaseTest() { certificateCountry shouldBe "YY" } } + + @Test + fun `default parsing mode for containers is lenient`() { + val container = VaccinationContainer( + vaccinationQrCode = testData.personYVacTwoEntriesQrCode, + scannedAt = Instant.EPOCH + ) + val extractor = mockk<VaccinationQRCodeExtractor>().apply { + every { extract(any(), any()) } returns mockk<VaccinationCertificateQRCode>().apply { + every { parsedData } returns mockk() + } + } + container.qrCodeExtractor = extractor + + container.certificateData shouldNotBe null + + verify { extractor.extract(testData.personYVacTwoEntriesQrCode, QrCodeExtractor.Mode.CERT_VAC_LENIENT) } + } + + @Test + fun `gracefully handle semi invalid data - multiple entries`() { + testData.personYVacTwoEntriesContainer.certificate.vaccinationDatas.size shouldBe 1 + } } -- GitLab