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