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 638bc4082866a671d4fa8b1222f3da2111faf573..b753f30df1c058f28895e797933d93c78edd28f0 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 @@ -16,6 +16,7 @@ import de.rki.coronawarnapp.bugreporting.censors.submission.PcrQrCodeCensor 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.debuglog.internal.DebugLoggerScope import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadApiV1 @@ -117,4 +118,8 @@ class BugReportingSharedModule { @Provides @IntoSet fun ratProfileCensor(censor: RatProfileCensor): BugCensor = censor + + @Provides + @IntoSet + fun certificateQrCodeCensor(censor: CertificateQrCodeCensor): 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/CertificateQrCodeCensor.kt new file mode 100644 index 0000000000000000000000000000000000000000..b3de1ea4f938ebcb8e7b4f2287702d5a50a4072c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensor.kt @@ -0,0 +1,162 @@ +package de.rki.coronawarnapp.bugreporting.censors.vaccination + +import dagger.Reusable +import de.rki.coronawarnapp.bugreporting.censors.BugCensor +import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.toNewLogLineIfDifferent +import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1 +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateData +import java.util.LinkedList +import javax.inject.Inject + +@Reusable +class CertificateQrCodeCensor @Inject constructor() : BugCensor { + + override suspend fun checkLog(entry: LogLine): LogLine? { + var newMessage = entry.message + + synchronized(qrCodeStringsToCensor) { qrCodeStringsToCensor.toList() }.forEach { + newMessage = newMessage.replace( + it, + PLACEHOLDER + it.takeLast(4) + ) + } + + synchronized(certsToCensor) { certsToCensor.toList() }.forEach { + it.certificate.apply { + newMessage = newMessage.replace( + dob, + "vaccinationCertificate/dob" + ) + + newMessage = newMessage.replace( + dateOfBirth.toString(), + "vaccinationCertificate/dateOfBirth" + ) + + newMessage = censorNameData(nameData, newMessage) + + vaccinationDatas.forEach { data -> + newMessage = censorVaccinationData(data, newMessage) + } + } + } + + return entry.toNewLogLineIfDifferent(newMessage) + } + + private fun censorVaccinationData( + vaccinationData: VaccinationDGCV1.VaccinationData, + message: String + ): String { + var newMessage = message + + newMessage = newMessage.replace( + vaccinationData.dt, + "vaccinationData/dt" + ) + + newMessage = newMessage.replace( + vaccinationData.marketAuthorizationHolderId, + "vaccinationData/marketAuthorizationHolderId" + ) + + newMessage = newMessage.replace( + vaccinationData.medicalProductId, + "vaccinationData/medicalProductId" + ) + + newMessage = newMessage.replace( + vaccinationData.targetId, + "vaccinationData/targetId" + ) + + newMessage = newMessage.replace( + vaccinationData.certificateIssuer, + "vaccinationData/certificateIssuer" + ) + + newMessage = newMessage.replace( + vaccinationData.uniqueCertificateIdentifier, + "vaccinationData/uniqueCertificateIdentifier" + ) + + newMessage = newMessage.replace( + vaccinationData.countryOfVaccination, + "vaccinationData/countryOfVaccination" + ) + + newMessage = newMessage.replace( + vaccinationData.vaccineId, + "vaccinationData/vaccineId" + ) + + newMessage = newMessage.replace( + vaccinationData.vaccinatedAt.toString(), + "vaccinationData/vaccinatedAt" + ) + + return newMessage + } + + private fun censorNameData(nameData: VaccinationDGCV1.NameData, message: String): String { + var newMessage = message + + nameData.familyName?.let { fName -> + newMessage = newMessage.replace( + fName, + "nameData/familyName" + ) + } + + newMessage = newMessage.replace( + nameData.familyNameStandardized, + "nameData/familyNameStandardized" + ) + + nameData.givenName?.let { gName -> + newMessage = newMessage.replace( + gName, + "nameData/givenName" + ) + } + + nameData.givenNameStandardized?.let { gName -> + newMessage = newMessage.replace( + gName, + "nameData/givenNameStandardized" + ) + } + + return newMessage + } + + companion object { + private val qrCodeStringsToCensor = LinkedList<String>() + + fun addQRCodeStringToCensor(rawString: String) = synchronized(qrCodeStringsToCensor) { + qrCodeStringsToCensor.apply { + if (contains(rawString)) return@apply + addFirst(rawString) + // Max certs is at 4, but we may scan invalid qr codes that are not added which will be shown in raw + if (size > 8) removeLast() + } + } + + fun clearQRCodeStringToCensor() = synchronized(qrCodeStringsToCensor) { qrCodeStringsToCensor.clear() } + + private val certsToCensor = LinkedList<VaccinationCertificateData>() + fun addCertificateToCensor(cert: VaccinationCertificateData) = synchronized(certsToCensor) { + certsToCensor.apply { + if (contains(cert)) return@apply + addFirst(cert) + // max certs we should have is 2, 50% leeway + if (size > 4) removeLast() + } + } + + fun clearCertificateToCensor() = synchronized(certsToCensor) { certsToCensor.clear() } + + 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 55ddd315ae27b7b1fac8c1e446b9b09f58980d7c..7b31a977326e8cf8f72a7b76938766d79e1c1d3f 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 @@ -14,7 +14,7 @@ class CoronaTestQrCodeValidator @Inject constructor( fun validate(rawString: String): CoronaTestQRCode { return findExtractor(rawString) ?.extract(rawString) - ?.also { Timber.i("Extracted data from QR code is $it") } + ?.also { Timber.i("Extracted data from QR code is %s", it) } ?: throw InvalidQRCodeException() } 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 b4ab3e75377eb1e08946d52e27f01f3175155e9e..ae3065df399be47ee02e1d6fe5650ad3ea51b2a0 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 @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.vaccination.core.qrcode +import de.rki.coronawarnapp.bugreporting.censors.vaccination.CertificateQrCodeCensor import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor import de.rki.coronawarnapp.util.compression.inflate import de.rki.coronawarnapp.util.encoding.Base45Decoder @@ -22,6 +23,8 @@ class VaccinationQRCodeExtractor @Inject constructor( override fun canHandle(rawString: String): Boolean = rawString.startsWith(PREFIX) override fun extract(rawString: String): VaccinationCertificateQRCode { + CertificateQrCodeCensor.addQRCodeStringToCensor(rawString) + val parsedData = rawString .removePrefix(PREFIX) .decodeBase45() @@ -56,6 +59,8 @@ class VaccinationQRCodeExtractor @Inject constructor( header = headerParser.parse(cbor), certificate = bodyParser.parse(cbor) ).also { + CertificateQrCodeCensor.addCertificateToCensor(it) + }.also { Timber.v("Parsed vaccination certificate for %s", it.certificate.nameData.familyNameStandardized) } } 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 84f1a745a2b2afbdb95d8396999e784f5bee4113..3e7f603962d7306ea8bde1da607008559c94c7c1 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 @@ -14,9 +14,11 @@ class VaccinationQRCodeValidator @Inject constructor( 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) - ?.also { Timber.i("Extracted data from QR code is $it") } + ?.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 c286d3d9b4cbd87dc75dfbc8b556b5c6dfc1355d..b76916194b49ab4bcb1090bedb3cea556db50694 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 @@ -32,7 +32,7 @@ data class VaccinationContainer internal constructor( constructor() : this("", Instant.EPOCH) @delegate:Transient - private val certificateData: VaccinationCertificateData by lazy { + internal val certificateData: VaccinationCertificateData by lazy { preParsedData ?: qrCodeExtractor.extract(vaccinationQrCode).parsedData } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorage.kt index ee8d04c714deaf19c73b3d36dde07c43b3c7bd92..1424b943171378f04cc41ee44a398e0705afc95f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorage.kt @@ -36,9 +36,9 @@ class VaccinationStorage @Inject constructor( return@mapNotNull null } value as String - gson.fromJson<VaccinatedPersonData>(value).also { - Timber.tag(TAG).v("Person loaded: %s", it) - requireNotNull(it.identifier) + gson.fromJson<VaccinatedPersonData>(value).also { personData -> + Timber.tag(TAG).v("Person loaded: %s", personData) + requireNotNull(personData.identifier) } } return persons.toSet() 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/CertificateQrCodeCensorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e6c2927121c341a2a08c37e043b5953f86340121 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensorTest.kt @@ -0,0 +1,130 @@ +package de.rki.coronawarnapp.bugreporting.censors.vaccination + +import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1 +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateData +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.mockk +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class CertificateQrCodeCensorTest { + + 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" + private val testCertificateData = VaccinationCertificateData( + header = mockk(), + certificate = VaccinationDGCV1( + version = "1", + nameData = VaccinationDGCV1.NameData( + familyName = "Kevin", + familyNameStandardized = "Kevin2", + givenName = "Bob", + givenNameStandardized = "Bob2" + ), + dob = "1969-11-16", + vaccinationDatas = listOf( + VaccinationDGCV1.VaccinationData( + targetId = "12345", + vaccineId = "1214765", + medicalProductId = "aaEd/easd", + marketAuthorizationHolderId = "ASD-2312", + doseNumber = 2, + totalSeriesOfDoses = 5, + dt = "1969-04-20", + countryOfVaccination = "DE", + certificateIssuer = "Herbert", + uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" + ) + ) + ) + ) + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + } + + @AfterEach + fun teardown() { + CertificateQrCodeCensor.clearCertificateToCensor() + CertificateQrCodeCensor.clearQRCodeStringToCensor() + } + + private fun createInstance() = CertificateQrCodeCensor() + + @Test + fun `checkLog() should return censored LogLine`() = runBlockingTest { + CertificateQrCodeCensor.addQRCodeStringToCensor(testRawString) + CertificateQrCodeCensor.addCertificateToCensor(testCertificateData) + + val censor = createInstance() + + val logLineToCensor = LogLine( + timestamp = 1, + priority = 3, + message = "Here comes the rawString: $testRawString of the vaccine certificate", + tag = "I am tag", + throwable = null + ) + + censor.checkLog(logLineToCensor) shouldBe logLineToCensor.copy( + message = "Here comes the rawString: ########-####-####-####-########C\$AH of the vaccine certificate", + ) + + val certDataToCensor = LogLine( + timestamp = 1, + priority = 3, + message = "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" + + " urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ", + tag = "I am tag", + throwable = null + ) + + censor.checkLog(certDataToCensor) shouldBe certDataToCensor.copy( + message = "Hello my name is nameData/familyName nameData/givenName, i was born at " + + "vaccinationCertificate/dob, i have been vaccinated with: vaccinationData/targetId " + + "vaccinationData/vaccineId vaccinationData/medicalProductId" + + " vaccinationData/marketAuthorizationHolderId vaccinationData/dt" + + " vaccinationData/countryOfVaccination vaccinationData/certificateIssuer" + + " vaccinationData/uniqueCertificateIdentifier" + ) + } + + @Test + fun `checkLog() should return null if no data to censor was set`() = runBlockingTest { + val censor = createInstance() + + val logLineNotToCensor = LogLine( + timestamp = 1, + priority = 3, + message = "Here comes the rawData: $testRawString", + tag = "I am tag", + throwable = null + ) + + censor.checkLog(logLineNotToCensor) shouldBe null + } + + @Test + fun `checkLog() should return null if nothing should be censored`() = runBlockingTest { + CertificateQrCodeCensor.addQRCodeStringToCensor(testRawString.replace("1", "2")) + CertificateQrCodeCensor.addCertificateToCensor(testCertificateData) + + val censor = createInstance() + + val logLineNotToCensor = LogLine( + timestamp = 1, + priority = 3, + message = "Here comes the rawString: $testRawString", + tag = "I am tag", + throwable = null + ) + + censor.checkLog(logLineNotToCensor) shouldBe null + } +}