From 0a123511ee3db4ad2373cad30bd28e5735a9d1f3 Mon Sep 17 00:00:00 2001 From: Juraj Kusnier <jurajkusnier@users.noreply.github.com> Date: Wed, 28 Apr 2021 17:11:15 +0200 Subject: [PATCH] Implement more secure RAT QR Code design (EXPOSUREAPP-6776) (#2994) * Update CoronaTestQRCode parser * Update CoronaTestQRCode parser for anonymous qr codes Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com> --- .../coronatest/qrcode/CoronaTestQRCode.kt | 2 ++ .../qrcode/RapidAntigenQrCodeExtractor.kt | 36 ++++++++++++++++--- .../qrcode/RapidAntigenQrCodeExtractorTest.kt | 10 +++--- .../coronatest/qrcode/TestQrCodes.kt | 16 ++++----- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt index 0c4879f0d..9a5c1801a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt @@ -37,6 +37,8 @@ sealed class CoronaTestQRCode : Parcelable, TestRegistrationRequest { val firstName: String? = null, val lastName: String? = null, val dateOfBirth: LocalDate? = null, + val testid: String? = null, + val salt: String? = null ) : CoronaTestQRCode() { @IgnoredOnParcel 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 f74aa2d87..c1a95544c 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 @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.coronatest.qrcode import com.google.common.io.BaseEncoding import com.google.gson.Gson import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.util.HashExtensions.toSHA256 import de.rki.coronawarnapp.util.hashing.isSha256Hash import de.rki.coronawarnapp.util.serialization.fromJson import okio.internal.commonToUtf8String @@ -21,14 +22,16 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona Timber.v("extract(rawString=%s)", rawString) val payload = CleanPayload(extractData(rawString)) - payload.requireValidPersonalData() + payload.requireValidData() return CoronaTestQRCode.RapidAntigen( hash = payload.hash, createdAt = payload.createdAt, firstName = payload.firstName, lastName = payload.lastName, - dateOfBirth = payload.dateOfBirth + dateOfBirth = payload.dateOfBirth, + testid = payload.testId, + salt = payload.salt ) } @@ -68,7 +71,9 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona @SerializedName("timestamp") val timestamp: Long?, @SerializedName("fn") val firstName: String?, @SerializedName("ln") val lastName: String?, - @SerializedName("dob") val dateOfBirth: String? + @SerializedName("dob") val dateOfBirth: String?, + @SerializedName("testid") val testid: String?, + @SerializedName("salt") val salt: String? ) private data class CleanPayload(val raw: RawPayload) { @@ -104,7 +109,20 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona } } - fun requireValidPersonalData() { + val testId: String? by lazy { + if (raw.testid.isNullOrEmpty()) null else raw.testid + } + + val salt: String? by lazy { + if (raw.salt.isNullOrEmpty()) null else raw.salt + } + + fun requireValidData() { + requireValidPersonalData() + requireValidHash() + } + + private fun requireValidPersonalData() { val allOrNothing = listOf( firstName != null, lastName != null, @@ -113,6 +131,16 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona val complete = allOrNothing.all { it } || allOrNothing.all { !it } if (!complete) throw InvalidQRCodeException("QRCode contains incomplete personal data: $raw") } + + private fun requireValidHash() { + val isQrCodeWithPersonalData = firstName != null && lastName != null && dateOfBirth != null + val generatedHash = + "${raw.dateOfBirth}#${raw.firstName}#${raw.lastName}#${raw.timestamp}#${raw.testid}#${raw.salt}" + .toSHA256() + if (isQrCodeWithPersonalData && !generatedHash.equals(hash, true)) { + throw InvalidQRCodeException("Generated hash doesn't match QRCode hash") + } + } } companion object { 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 43e945ad8..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 @@ -37,11 +37,11 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() { fun `personal data is extracted`() { val data = instance.extract(raQrCode3) data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN - data.hash shouldBe "7b1c063e883063f8c33ffaa256aded506afd907f7446143b3da0f938a21967a9" - data.createdAt shouldBe Instant.ofEpochMilli(1618563782000) - data.dateOfBirth shouldBe LocalDate.parse("1962-01-08") - data.lastName shouldBe "Hayes" - data.firstName shouldBe "Alma" + data.hash shouldBe "7dce08db0d4abd5ac1d2498b571afb221ca947c75c847d05466b4cfe9d95dc66" + data.createdAt shouldBe Instant.ofEpochMilli(1619618352000) + data.dateOfBirth shouldBe LocalDate.parse("1963-03-17") + data.lastName shouldBe "Tyler" + data.firstName shouldBe "Jacob" } @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt index c43995138..b29edd24d 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt @@ -5,21 +5,21 @@ internal val pcrQrCode2 = "https://localhost/?123456-12345678-1234-4DA7-B166-B86 internal val pcrQrCode3 = "https://LOCALHOST/?123456-12345678-1234-4DA7-B166-B86D85475064" internal val raQrCode1 = - "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjA5NjQsInNhbHQiOiIwQ0ZEMUJCQzI2Q0FCODVCNkZFNDE5MTJFQjFBQUU1QUNEN0QyNjA0RTQwNTQyRUVEQjZEQUYyQkRBMDQ5QzRGIiwidGVzdElkIjoiYjM2YzUzN2ItZWQ5NC00Njc3LTkzZmQtODUwMTY4NjlkYjEwIiwiaGFzaCI6IjJiNTc0NjhlN2Q4MTkyMWQzOGM4OGI1NjExOWE0Y2ViMzYyNmI1MDM4ZWI5Njk3ZjkxOTQ4NmJjMzg0Y2U2M2UifQ" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzMTEsInNhbHQiOiJBODczOTVDRjYyMjc1QzRCQjczMjAxOERFRTRDQzhCRSIsInRlc3RpZCI6IjQwNDBkNTRlLWIzNmYtNGQ1Yi05MThiLTExODZjM2E0OTZhNSIsImhhc2giOiI4MzFhNzNmNGZhODZkMDdjMjViOTdjNzdiZjg5MzNhN2Q5MzAzODIxZDRjNzdiZDc5YzlkNzJlMmU0ZTI1MWYyIiwiZm4iOiJEeWxhbiIsImxuIjoiR2FyZGluZXIiLCJkb2IiOiIxOTY3LTEyLTIyIn0=" internal val raQrCode2 = - "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjA5NjQsInNhbHQiOiIwQ0ZEMUJCQzI2Q0FCODVCNkZFNDE5MTJFQjFBQUU1QUNEN0QyNjA0RTQwNTQyRUVEQjZEQUYyQkRBMDQ5QzRGIiwidGVzdElkIjoiYjM2YzUzN2ItZWQ5NC00Njc3LTkzZmQtODUwMTY4NjlkYjEwIiwiaGFzaCI6IjJiNTc0NjhlN2Q4MTkyMWQzOGM4OGI1NjExOWE0Y2ViMzYyNmI1MDM4ZWI5Njk3ZjkxOTQ4NmJjMzg0Y2U2M2UifQ" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzMzMsInNhbHQiOiI5Nzk5OUY4QTBFMjBBMDgxMDMyMDdGQzkxOEQzRTVFRiIsInRlc3RpZCI6IjE3ZjFlOGMxLTBiMWMtNDE1Ni1iMTZkLTlmMmQwNzEzMDJmNSIsImhhc2giOiIzMDcxNmQzM2FkNDFhZjQwNTk1Y2IyOThkMDcwMDllM2QwZjIxZDk5Njg4ZWZkOTIyNGQ4OWQ0OTQ3YjRkZDU3IiwiZm4iOiJIYXJyaWV0IiwibG4iOiJCbGFuYyIsImRvYiI6IjE5OTUtMDItMTUifQ==" internal val raQrCode3 = - "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM3ODIsInNhbHQiOiI1QTI3M0REREJCQTFEMkFDQUEzN0ExMDg4NjhGNkIwMjM3NjQzRjhBNjdCQTNENkQ3RUE3RkREQ0M0RDJGMjBEIiwidGVzdElkIjoiMGQ5ZTg0MzItZWI5MS00YzhmLTgyYWYtNWEwMWZiMWI2NzYwIiwiaGFzaCI6IjdiMWMwNjNlODgzMDYzZjhjMzNmZmFhMjU2YWRlZDUwNmFmZDkwN2Y3NDQ2MTQzYjNkYTBmOTM4YTIxOTY3YTkiLCJmbiI6IkFsbWEiLCJsbiI6IkhheWVzIiwiZG9iIjoiMTk2Mi0wMS0wOCJ9" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzNTIsInNhbHQiOiJDRjBDNTQ3RkY2RDMwOTlBMkIwNkMxQzRFNEYwOEFGOSIsInRlc3RpZCI6ImY0N2VmODA0LTZmMGMtNDhiMy1hODY5LWUyZjg4NmIxMjU0ZiIsImhhc2giOiI3ZGNlMDhkYjBkNGFiZDVhYzFkMjQ5OGI1NzFhZmIyMjFjYTk0N2M3NWM4NDdkMDU0NjZiNGNmZTlkOTVkYzY2IiwiZm4iOiJKYWNvYiIsImxuIjoiVHlsZXIiLCJkb2IiOiIxOTYzLTAzLTE3In0=" internal val raQrCode4 = - "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM3ODIsInNhbHQiOiI1QTI3M0REREJCQTFEMkFDQUEzN0ExMDg4NjhGNkIwMjM3NjQzRjhBNjdCQTNENkQ3RUE3RkREQ0M0RDJGMjBEIiwidGVzdElkIjoiMGQ5ZTg0MzItZWI5MS00YzhmLTgyYWYtNWEwMWZiMWI2NzYwIiwiaGFzaCI6IjdiMWMwNjNlODgzMDYzZjhjMzNmZmFhMjU2YWRlZDUwNmFmZDkwN2Y3NDQ2MTQzYjNkYTBmOTM4YTIxOTY3YTkiLCJmbiI6IkFsbWEiLCJsbiI6IkhheWVzIiwiZG9iIjoiMTk2Mi0wMS0wOCJ9" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzNjksInNhbHQiOiJEQTg3M0YwOUJCQzZCQzVFMEQ0QTdBMzc2MjZERkMwNSIsInRlc3RpZCI6IjlmYjYzNWE2LThhZTQtNDVjZS1iZTZkLTg5MjdmMjM1ZmIzNiIsImhhc2giOiJmNGYzMDU0NTMwODI1MjkxYjhmMDQ3MWZkZTRiY2EzNTljOGVjZDI0ZTBmNTkxNTA5NTQyY2ZmNGJhNDBkNmY1IiwiZm4iOiJFZGRpZSIsImxuIjoiQm91Y2hlciIsImRvYiI6IjE5NzktMDEtMjMifQ==" internal val raQrCode5 = - "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM4NDMsInNhbHQiOiJBNkVFRkZERDE2Qzk5RDFCODEyREE2NTc1NzgwMDM1QTQ0REI0OUM5NTBGODdCQkY0NDJBRkIwMDE5NkZCNDQ4IiwidGVzdElkIjoiNDhjOTc2ODgtY2U2ZC00MDFjLWEwMmMtMjU5MTE2YTRmODhmIiwiaGFzaCI6IjIyMTA4Y2FmNTQwNWM0MzI5Y2I3ZTEzNzg3MTMxMDJhMGNkNjY4OWM3YWFmMWJjOGViYzk3MDdiMjNjNTZhN2UifQ==" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzODIsInNhbHQiOiI2RUJCMUM4NTc0QUYxQzcwQkY2MTNGQjMzNDM3MkM3MiIsInRlc3RpZCI6Ijg2MzkzMTE1LWVkYjAtNGE3Zi1iZTg1LWEwYjViMjY5M2Q3MSIsImhhc2giOiIzMmQxYjk4MTRjNWU0ZjI3Mjc5NWU0NjNhMmViZjI5Y2Y1ZDZkYzdiZmRhODNhYzViZWY5Y2Q5M2E3YjMxMjYwIiwiZm4iOiJBZGVsYWlkZSIsImxuIjoiSHVpc21hbiIsImRvYiI6IjE5NTktMDgtMDIifQ==" internal val raQrCode6 = - "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM4NDMsInNhbHQiOiJBNkVFRkZERDE2Qzk5RDFCODEyREE2NTc1NzgwMDM1QTQ0REI0OUM5NTBGODdCQkY0NDJBRkIwMDE5NkZCNDQ4IiwidGVzdElkIjoiNDhjOTc2ODgtY2U2ZC00MDFjLWEwMmMtMjU5MTE2YTRmODhmIiwiaGFzaCI6IjIyMTA4Y2FmNTQwNWM0MzI5Y2I3ZTEzNzg3MTMxMDJhMGNkNjY4OWM3YWFmMWJjOGViYzk3MDdiMjNjNTZhN2UifQ==" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzOTcsInNhbHQiOiI3MDcyQzAyMTkyM0ZFOEMzMDVDNTAxQkU5MjQyMjNBQyIsInRlc3RpZCI6IjhmMzA0OTE4LWI2YmMtNDQyOS1iYzhlLWMzMzRkMjdhNTdiNCIsImhhc2giOiI2ZjVhMjJhYzY2ZTc1Y2JiYTE3MTBlN2IxZWMwZTllMDk4NjUyMjY0MWE3NTYyNGY0MGZhMDc4YTZkZjY0ZTVjIiwiZm4iOiJBbmRyZSIsImxuIjoiQmFyZ2VsbGluaSIsImRvYiI6IjE5OTItMTEtMDcifQ==" internal val raQrCode7 = - "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM5MDIsInNhbHQiOiJGRkVERDZCMEM5MzZGRDVBQTg1M0NBNjgzOTY1QzYzMDI1NDRBMTIyNTY5MDAyQjg5OEI1NkM5OTY1NjRENTNGIiwidGVzdElkIjoiODYyNDcwYzItMzk3MS00M2Y0LWFjY2UtMjIzMzRlMjZiZTg3IiwiaGFzaCI6Ijc1ZmU5M2MyOWI1ZDI4NTU3NmJjZmM5NTZmYWIxMzllNTgxMWNhZWY1MTNiN2Y4MzhmNjY3NWJhYTU4MGM5YWUiLCJmbiI6IkJyeWFuIiwibG4iOiJNYXJzaCIsImRvYiI6IjE5OTAtMDQtMjgifQ==" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTg0MjMsInNhbHQiOiJEM0Y1MzcyODU2NkMxMDFENjE1MkVCQ0I0OEMxMkFCOCIsInRlc3RpZCI6Ijc5NWIxY2MwLWU2NjQtNGFmZi05NTk3LWU3MTk2ODE4ZGVmYiIsImhhc2giOiI3NmMxMjJiOTlmZWVmZmM5Mjc3MTE2YjUwZGIwZGM1NjI0ZjY5OWFiMzliMDAwOWMwYzg5YmRlMWNjZjM4YmQxIiwiZm4iOiJWaWN0b3JpYSIsImxuIjoiTWFubmVsbGkiLCJkb2IiOiIxOTc4LTA0LTIxIn0=" internal val raQrCode8 = - "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM5MDIsInNhbHQiOiJGRkVERDZCMEM5MzZGRDVBQTg1M0NBNjgzOTY1QzYzMDI1NDRBMTIyNTY5MDAyQjg5OEI1NkM5OTY1NjRENTNGIiwidGVzdElkIjoiODYyNDcwYzItMzk3MS00M2Y0LWFjY2UtMjIzMzRlMjZiZTg3IiwiaGFzaCI6Ijc1ZmU5M2MyOWI1ZDI4NTU3NmJjZmM5NTZmYWIxMzllNTgxMWNhZWY1MTNiN2Y4MzhmNjY3NWJhYTU4MGM5YWUiLCJmbiI6IkJyeWFuIiwibG4iOiJNYXJzaCIsImRvYiI6IjE5OTAtMDQtMjgifQ==" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MjEzMzEsInNhbHQiOiI5NERDMkUyRkNCNTE2NDFFQzA5RkZENjVEMkJEMDg4QiIsInRlc3RpZCI6ImMyMGI5OTgxLWZmNzAtNGRmOC1hYTAyLTUwOTdmMmJkN2YzYyIsImhhc2giOiI1MmVlZjA3YjQwYzU1ODM0OTg3MGQ3YTA2Yzc0OGIwOTAxNGMxMTBlYTkzYjZhNmRhNDhkOWI4NTU0NTU2MzY0In0=" // { // "fn":"", -- GitLab