From 0ab2b010eb3254a0d923b68de1c02c0d3dcaf083 Mon Sep 17 00:00:00 2001
From: Rituraj Sambherao <54317407+ritsam@users.noreply.github.com>
Date: Mon, 14 Sep 2020 16:29:10 +0100
Subject: [PATCH] QR Code stricter validation (EXPOSUREAPP-2570) (#1127)

* test for OnboardingFragment

* QR code stricter validations

* regex pattern improvement

* removed Obsolete test

* lint changes to fix pipeline issue

* logic refinement and test coverage improvement

* pipeline related fixes

* piepline related fixes : unnecessary test removed

* pipeline fixes and increased tests coverage

* QR code validation logic improvement

* detekt pipeline issue resolved

* pipeline issue Fix

* logic ovehaul and improvement for QRCode scans / validations

1, Data object for ScanResults
2. new Test for ScanResults
3. logic simplified

* removing unused imports from tests

* Small refactoring together with Rituraj.

* Adjust detekt, early abort via return is GOOD!

Co-authored-by: Matthias Urhahn <darken@darken.eu>
Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>
---
 Corona-Warn-App/config/detekt.yml             |  2 +-
 .../service/submission/QRScanResult.kt        | 32 +++++++++++
 .../service/submission/SubmissionConstants.kt |  4 --
 .../service/submission/SubmissionService.kt   | 13 +----
 .../ui/viewmodel/SubmissionViewModel.kt       |  9 ++--
 .../service/submission/ScanResultTest.kt      | 54 +++++++++++++++++++
 .../submission/SubmissionConstantsTest.kt     | 11 ++--
 .../submission/SubmissionServiceTest.kt       | 22 --------
 .../ui/viewmodel/SubmissionViewModelTest.kt   |  2 +-
 9 files changed, 102 insertions(+), 47 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/QRScanResult.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt

diff --git a/Corona-Warn-App/config/detekt.yml b/Corona-Warn-App/config/detekt.yml
index 1a5fd4a6d..2555aa687 100644
--- a/Corona-Warn-App/config/detekt.yml
+++ b/Corona-Warn-App/config/detekt.yml
@@ -572,7 +572,7 @@ style:
     excludedFunctions: 'equals'
     excludeLabeled: false
     excludeReturnFromLambda: true
-    excludeGuardClauses: false
+    excludeGuardClauses: true
   SafeCast:
     active: true
   SerialVersionUIDInSerializableClass:
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/QRScanResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/QRScanResult.kt
new file mode 100644
index 000000000..783ec13a2
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/QRScanResult.kt
@@ -0,0 +1,32 @@
+package de.rki.coronawarnapp.service.submission
+
+import java.util.regex.Pattern
+
+data class QRScanResult(val rawResult: String) {
+
+    val isValid: Boolean
+        get() = guid != null
+    val guid: String? by lazy { extractGUID(rawResult) }
+
+    private fun extractGUID(rawResult: String): String? {
+        if (rawResult.length > MAX_QR_CODE_LENGTH) return null
+        if (rawResult.count { it == GUID_SEPARATOR } != 1) return null
+        if (!QR_CODE_REGEX.toRegex().matches(rawResult)) return null
+
+        val potentialGUID = rawResult.substringAfterLast(GUID_SEPARATOR, "")
+        if (potentialGUID.isBlank() || potentialGUID.length > MAX_GUID_LENGTH) return null
+
+        return potentialGUID
+    }
+
+    companion object {
+        // regex pattern for scanned QR code URL
+        val QR_CODE_REGEX: Pattern = Pattern.compile(
+            "^((^https:\\/{2}localhost)(\\/\\?)[A-Fa-f0-9]{6}" +
+                    "[-][A-Fa-f0-9]{8}[-][A-Fa-f0-9]{4}[-][A-Fa-f0-9]{4}[-][A-Fa-f0-9]{4}[-][A-Fa-f0-9]{12})\$"
+        )
+        const val GUID_SEPARATOR = '?'
+        const val MAX_QR_CODE_LENGTH = 150
+        const val MAX_GUID_LENGTH = 80
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt
index 4ab404508..43de684e2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt
@@ -14,10 +14,6 @@ object SubmissionConstants {
     val TEST_RESULT_URL = "$VERSIONED_VERIFICATION_CDN_URL/$TEST_RESULT"
     val TAN_REQUEST_URL = "$VERSIONED_VERIFICATION_CDN_URL/$TAN"
 
-    const val MAX_QR_CODE_LENGTH = 150
-    const val MAX_GUID_LENGTH = 80
-    const val GUID_SEPARATOR = '?'
-
     const val SERVER_ERROR_CODE_400 = 400
 
     const val EMPTY_HEADER = ""
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt
index defce20ed..e33668fa5 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt
@@ -65,19 +65,10 @@ object SubmissionService {
     }
 
     fun containsValidGUID(scanResult: String): Boolean {
-        if (scanResult.length > SubmissionConstants.MAX_QR_CODE_LENGTH ||
-            scanResult.count { it == SubmissionConstants.GUID_SEPARATOR } != 1
-        )
-            return false
-
-        val potentialGUID = extractGUID(scanResult)
-
-        return !(potentialGUID.isEmpty() || potentialGUID.length > SubmissionConstants.MAX_GUID_LENGTH)
+        val scanResult = QRScanResult(scanResult)
+        return scanResult.isValid
     }
 
-    fun extractGUID(scanResult: String): String =
-        scanResult.substringAfterLast(SubmissionConstants.GUID_SEPARATOR, "")
-
     fun storeTestGUID(guid: String) = LocalData.testGUID(guid)
 
     fun deleteTestGUID() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt
index 2fced7642..c5f670cb0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt
@@ -9,6 +9,7 @@ import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.TransactionException
 import de.rki.coronawarnapp.exception.http.CwaWebException
 import de.rki.coronawarnapp.exception.reporting.report
+import de.rki.coronawarnapp.service.submission.QRScanResult
 import de.rki.coronawarnapp.service.submission.SubmissionService
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.storage.SubmissionRepository
@@ -98,10 +99,10 @@ class SubmissionViewModel : ViewModel() {
             _uiStateError
         )
 
-    fun validateAndStoreTestGUID(scanResult: String) {
-        if (SubmissionService.containsValidGUID(scanResult)) {
-            val guid = SubmissionService.extractGUID(scanResult)
-            SubmissionService.storeTestGUID(guid)
+    fun validateAndStoreTestGUID(rawResult: String) {
+        val scanResult = QRScanResult(rawResult)
+        if (scanResult.isValid) {
+            SubmissionService.storeTestGUID(scanResult.guid!!)
             _scanStatus.value = Event(ScanStatus.SUCCESS)
         } else {
             _scanStatus.value = Event(ScanStatus.INVALID)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt
new file mode 100644
index 000000000..2f7e78179
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt
@@ -0,0 +1,54 @@
+package de.rki.coronawarnapp.service.submission
+
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockkObject
+import org.junit.Before
+import org.junit.Test
+
+class ScanResultTest {
+    private val guid = "123456-12345678-1234-4DA7-B166-B86D85475064"
+
+    @MockK
+    private lateinit var scanResult: QRScanResult
+
+    @Before
+    fun setUp() {
+        MockKAnnotations.init(this)
+        mockkObject(scanResult)
+        every { scanResult.isValid } returns false
+    }
+
+    @Test
+    fun containsValidGUID() {
+        //valid test
+        scanResult = QRScanResult("https://localhost/?$guid")
+        scanResult.isValid shouldBe true
+
+        // more invalid tests checks
+        scanResult = QRScanResult("http://localhost/?$guid")
+        scanResult.isValid shouldBe false
+        scanResult = QRScanResult("https://localhost/?")
+        scanResult.isValid shouldBe false
+        scanResult = QRScanResult("htps://wrongformat.com")
+        scanResult.isValid shouldBe false
+        scanResult =
+            QRScanResult("https://localhost/%20?3D6D08-3567F3F2-4DCF-43A3-8737-4CD1F87D6FDA")
+        scanResult.isValid shouldBe false
+        scanResult =
+            QRScanResult("https://some-host.com/?3D6D08-3567F3F2-4DCF-43A3-8737-4CD1F87D6FDA")
+        scanResult.isValid shouldBe false
+        scanResult = QRScanResult("https://localhost/?3567F3F2-4DCF-43A3-8737-4CD1F87D6FDA")
+        scanResult.isValid shouldBe false
+        scanResult = QRScanResult("https://localhost/?4CD1F87D6FDA")
+        scanResult.isValid shouldBe false
+    }
+
+    @Test
+    fun extractGUID() {
+        QRScanResult("https://localhost/?$guid").guid shouldBe guid
+    }
+}
+
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionConstantsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionConstantsTest.kt
index 98efa5fec..b65d00b69 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionConstantsTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionConstantsTest.kt
@@ -11,13 +11,16 @@ class SubmissionConstantsTest {
         Assert.assertEquals(KeyType.GUID.name, "GUID")
         Assert.assertEquals(KeyType.TELETAN.name, "TELETAN")
 
-        Assert.assertEquals(SubmissionConstants.REGISTRATION_TOKEN_URL, "version/v1/registrationToken")
+        Assert.assertEquals(
+            SubmissionConstants.REGISTRATION_TOKEN_URL,
+            "version/v1/registrationToken"
+        )
         Assert.assertEquals(SubmissionConstants.TEST_RESULT_URL, "version/v1/testresult")
         Assert.assertEquals(SubmissionConstants.TAN_REQUEST_URL, "version/v1/tan")
 
-        Assert.assertEquals(SubmissionConstants.MAX_QR_CODE_LENGTH, 150)
-        Assert.assertEquals(SubmissionConstants.MAX_GUID_LENGTH, 80)
-        Assert.assertEquals(SubmissionConstants.GUID_SEPARATOR, '?')
+        Assert.assertEquals(QRScanResult.MAX_QR_CODE_LENGTH, 150)
+        Assert.assertEquals(QRScanResult.MAX_GUID_LENGTH, 80)
+        Assert.assertEquals(QRScanResult.GUID_SEPARATOR, '?')
 
         Assert.assertEquals(SubmissionConstants.SERVER_ERROR_CODE_400, 400)
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt
index fef637070..f1d222553 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt
@@ -163,26 +163,4 @@ class SubmissionServiceTest {
         }
     }
 
-    @Test
-    fun containsValidGUID() {
-        // valid
-        assertThat(
-            SubmissionService.containsValidGUID("https://bs-sd.de/covid-19/?$guid"),
-            equalTo(true)
-        )
-
-        // invalid
-        assertThat(
-            SubmissionService.containsValidGUID("https://no-guid-here"),
-            equalTo(false)
-        )
-    }
-
-    @Test
-    fun extractGUID() {
-        assertThat(
-            SubmissionService.extractGUID("https://bs-sd.de/covid-19/?$guid"),
-            equalTo(guid)
-        )
-    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModelTest.kt
index a7fa73cbf..5a93e5ac2 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModelTest.kt
@@ -51,7 +51,7 @@ class SubmissionViewModelTest {
 
         // valid guid
         val guid = "123456-12345678-1234-4DA7-B166-B86D85475064"
-        viewModel.validateAndStoreTestGUID("https://bs-sd.de/covid-19/?$guid")
+        viewModel.validateAndStoreTestGUID("https://localhost/?$guid")
         viewModel.scanStatus.value?.getContent().let { Assert.assertEquals(ScanStatus.SUCCESS, it) }
 
         // invalid guid
-- 
GitLab