From dd3ab20e8ad772035d57abd0efcf75e3713bfa5d Mon Sep 17 00:00:00 2001 From: Mohamed <mohamed.metwalli@sap.com> Date: Tue, 6 Apr 2021 17:09:15 +0200 Subject: [PATCH] Trace Location URL and ID (EXPOSUREAPP-6137, EXPOSUREAPP-6182) (#2737) * Use config regex * Refactoring * Verify functionality * Specific exceptions * Tests * lint * Generate QR Code url * Test base32 * Generate QrCode from url data * Remove / added by the system * Remove prefix * Prepare for location id calculation * Calculate location id * Location Id from trace location and tests split * Display last added location data in test menu * Update EventRegistrationTestFragmentViewModel.kt * Simplify * Display last locations * Pass check In Id * Tweaking * Style text * Base64 * Refactoring * Move location id and url to TraceLocation and create them lazily * Update ConfirmCheckInViewModelTest.kt * Update test screen * Base64 Id for readability * Remove added descriptor * Return null instead of 0 it does change the logic ,but how the usr sees it * Use Okio extension * Add parcelization test * Update VerifiedTraceLocationTest.kt * Provide locationId hash * Delete DefaultQRCodeVerifierTest2.kt * Add traceLocationIdHash to TraceLocation * Rename * Revert , server will match both * Wrap exceptions * Pass cause * lint * Return null * Create ProtoBufKtTest.kt * lint Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com> Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com> --- .../qrcode/VerifiedTraceLocationTest.kt | 279 +++--------------- .../ui/EventRegistrationTestFragment.kt | 67 ++++- .../EventRegistrationTestFragmentViewModel.kt | 126 +++----- .../fragment_test_eventregistration.xml | 76 +++++ Corona-Warn-App/src/main/AndroidManifest.xml | 1 - .../appconfig/PresenceTracingConfig.kt | 2 + .../PresenceTracingConfigContainer.kt | 4 +- .../mapping/PresenceTracingConfigMapper.kt | 15 +- .../checkins/CheckInsTransformer.kt | 3 - .../qrcode/InvalidQRCodeDataException.kt | 6 - .../qrcode/InvalidQRCodeSignatureException.kt | 6 - .../checkins/qrcode/QRCodeException.kt | 17 +- .../checkins/qrcode/QRCodeUriParser.kt | 84 ++++-- .../checkins/qrcode/QrCodePayload.kt | 57 ++++ .../checkins/qrcode/TraceLocation.kt | 49 ++- .../checkins/qrcode/VerifiedTraceLocation.kt | 43 +-- .../attendee/checkins/CheckInsFragment.kt | 35 +-- .../attendee/checkins/CheckInsViewModel.kt | 12 +- .../confirm/ConfirmCheckInViewModel.kt | 37 +-- .../details/QrCodeDetailViewModel.kt | 11 +- .../de/rki/coronawarnapp/util/ProtoBuf.kt | 7 + .../confirm/ConfirmCheckInViewModelTest.kt | 27 +- .../checkins/qrcode/Base32UrlProvider.kt | 81 +++++ .../checkins/qrcode/Base64UrlProvider.kt | 82 +++++ .../qrcode/DefaultQRCodeVerifierTest2.kt | 64 ---- .../checkins/qrcode/QRCodeUriParserTest.kt | 81 ++++- .../checkins/qrcode/ValidUrlProvider.kt | 38 --- .../events/QrCodePayloadTest.kt | 67 +++++ .../events/TraceLocationIdTest.kt | 77 +++++ .../events/TraceLocationUrlTest.kt | 62 ++++ .../checkins/CheckInsViewModelTest.kt | 7 +- .../rki/coronawarnapp/util/ProtoBufKtTest.kt | 28 ++ 32 files changed, 953 insertions(+), 598 deletions(-) delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodePayload.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoBuf.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base32UrlProvider.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base64UrlProvider.kt delete mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest2.kt delete mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/QrCodePayloadTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUrlTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ProtoBufKtTest.kt diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt index 838f409bd..2e78d0912 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt @@ -1,13 +1,10 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode -import de.rki.coronawarnapp.environment.EnvironmentSetup +import android.os.Bundle +import android.os.Parcel import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import io.kotest.matchers.shouldBe -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK import okio.ByteString.Companion.decodeBase64 -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -15,254 +12,68 @@ import testhelpers.BaseTestInstrumentation @RunWith(JUnit4::class) class VerifiedTraceLocationTest : BaseTestInstrumentation() { - - @MockK lateinit var environmentSetup: EnvironmentSetup - - @Before - fun setUp() { - MockKAnnotations.init(this) - every { environmentSetup.appConfigPublicKey } returns PUB_KEY - } - - // TODO: Ugly but kinda works @Test - fun verifyTraceLocationIdGenerationHash1() { - val base64Payload = "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekAT" + - "D3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEst" + - "cUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3" + - "cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIECAEQAg==" - val base64LocationID = "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" - - val qrCodePayload = - TraceLocationOuterClass.QRCodePayload.parseFrom(base64Payload.decodeBase64()!!.toByteArray()) - val instance = VerifiedTraceLocation(qrCodePayload) + fun verifyTraceLocationMapping1() { + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + BASE64_PAYLOAD_1.decodeBase64()!!.toByteArray() + ) - instance.traceLocationID.sha256().base64() shouldBe base64LocationID + VerifiedTraceLocation(qrCodePayload).traceLocation + .apply { + locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" + qrCodePayload() shouldBe qrCodePayload + } } @Test - fun verifyTraceLocationIdGenerationHash2() { - val base64Payload = "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJ" + - "bMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT" + - "0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK" - val base64LocationID = "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" - - val qrCodePayload = - TraceLocationOuterClass.QRCodePayload.parseFrom(base64Payload.decodeBase64()!!.toByteArray()) - val instance = VerifiedTraceLocation(qrCodePayload) - - instance.traceLocationID.sha256().base64() shouldBe base64LocationID - } - - /* disabled because of incompatibilities due to latest tech spec changes... needs to be re-written anyway - -@Test -fun verifyEventSuccess() = runBlockingTest { - val instant = Instant.ofEpochMilli(2687960 * 1_000L) - shouldNotThrowAny { - val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray()) - verifyResult.apply { - traceLocation.description shouldBe "My Birthday Party" - traceLocation.isBeforeStartTime(instant) shouldBe false - traceLocation.isAfterEndTime(instant) shouldBe false - } - } -} - -@Test -fun verifyParcelization() = runBlockingTest { - val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray()) - - val expectedTraceLocation = TraceLocation( - guid = "3055331c-2306-43f3-9742-6d8fab54e848", - version = 1, - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, - description = "My Birthday Party", - address = "at my place", - startDate = Instant.ofEpochSecond(2687955), - endDate = Instant.ofEpochSecond(2687991), - defaultCheckInLengthInMinutes = 0, - byteRepresentation = verifyResult.traceLocationBytes, - signature = verifyResult.signature.toByteArray().toByteString(), - ) - - verifyResult.traceLocation shouldBe expectedTraceLocation - - val bundle = Bundle().apply { - putParcelable("test", verifyResult.traceLocation) - } - - val parcelRaw = Parcel.obtain().apply { - writeBundle(bundle) - }.marshall() - - val restoredParcel = Parcel.obtain().apply { - unmarshall(parcelRaw, 0, parcelRaw.size) - setDataPosition(0) - } - - val restoredData = restoredParcel.readBundle()!!.run { - classLoader = TraceLocation::class.java.classLoader - getParcelable<TraceLocation>("test") - } - restoredData shouldBe expectedTraceLocation -} - -@Test -fun verifyEventStartTimeWaning() = runBlockingTest { - val instant = Instant.ofEpochMilli(2687940 * 1_000L) - shouldNotThrowAny { - val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray()) - verifyResult.apply { - traceLocation.description shouldBe "My Birthday Party" - traceLocation.isBeforeStartTime(instant) shouldBe true - traceLocation.isAfterEndTime(instant) shouldBe false - } - } -} + fun verifyTraceLocationMapping2() { + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + BASE64_PAYLOAD_2.decodeBase64()!!.toByteArray() + ) -@Test -fun verifyEventEndTimeWarning() = runBlockingTest { - val instant = Instant.now() - shouldNotThrowAny { - val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray()) - verifyResult.apply { - traceLocation.description shouldBe "My Birthday Party" - traceLocation.isBeforeStartTime(instant) shouldBe false - traceLocation.isAfterEndTime(instant) shouldBe true - } + VerifiedTraceLocation(qrCodePayload).traceLocation + .apply { + locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" + qrCodePayload() shouldBe qrCodePayload + } } -} -@Test -fun verifyEventWithInvalidKey() = runBlockingTest { - every { environmentSetup.appConfigVerificationKey } returns INVALID_PUB_KEY - shouldThrow<InvalidQRCodeSignatureException> { - traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray()) - } -} - -@Test -fun eventHasMalformedData() = runBlockingTest { - shouldThrow<InvalidQRCodeDataException> { - traceLocationQRCodeVerifier.verify( - INVALID_ENCODED_EVENT.decodeBase32().toByteArray() + @Test + fun parcelization() { + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + BASE64_PAYLOAD_2.decodeBase64()!!.toByteArray() ) - } -} -@Test -fun decodingTest1() = runBlockingTest { - val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom( - ENCODED_EVENT1.decodeBase32().toByteArray() - ) - val expectedSignature = - "MEQCIGVKfqPF2851IrEyDeVMazlRnIzLX16H6r1TB37PRzjbAiBGP13ADQcbQZsztKUCZMRcvnv5Mgdo0LY/v3qFMnrUkQ==" + val expectedVerifiedLocation = VerifiedTraceLocation(qrCodePayload) - val base32 = signedTraceLocation.toByteArray().toByteString().base32() - - shouldNotThrowAny { - val verifyResult = traceLocationQRCodeVerifier.verify(base32.decodeBase32().toByteArray()) - - verifyResult.apply { - traceLocation.description shouldBe "My Birthday Party" - signedTraceLocation.signature.toByteArray().toByteString().base64() shouldBe expectedSignature + val bundle = Bundle().apply { + putParcelable("verifiedTraceLocation", expectedVerifiedLocation) } - } -} -@Test -fun decodingTest2() = runBlockingTest { - val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom( - ENCODED_EVENT2.decodeBase32().toByteArray() - ) - val expectedSignature = - "MEQCIDWRTM4ujn1GFPuHlgpUnQIWwwzwI8abxSrF5Er2I5HaAiAbucxg+6d3nC/Iwzo7AXehJAS20TRX1S2rl0LO8kcYxA==" + val parcelRaw = Parcel.obtain().apply { + writeBundle(bundle) + }.marshall() - val base32 = signedTraceLocation.toByteArray().toByteString().base32() - - shouldNotThrowAny { - val verifyResult = traceLocationQRCodeVerifier.verify(base32.decodeBase32().toByteArray()) - - verifyResult.apply { - traceLocation.description shouldBe "Icecream Shop" - signedTraceLocation.signature.toByteArray().toByteString().base64() shouldBe expectedSignature + val restoredParcel = Parcel.obtain().apply { + unmarshall(parcelRaw, 0, parcelRaw.size) + setDataPosition(0) } - } -} -@Test -fun testVerifiedTraceLocationMapping() { - shouldNotThrowAny { - val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom( - ENCODED_EVENT1.decodeBase32().toByteArray() - ) - - val traceLocation = TraceLocationOuterClass.TraceLocation.parseFrom( - ENCODED_EVENT1_LOCATION.decodeBase32().toByteArray() - ) - val verifiedTraceLocation = VerifiedTraceLocation( - protoSignedTraceLocation = signedTraceLocation, - protoTraceLocation = traceLocation - ).traceLocation - - verifiedTraceLocation shouldBe TraceLocation( - guid = "3055331c-2306-43f3-9742-6d8fab54e848", - version = 1, - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, - description = "My Birthday Party", - address = "at my place", - startDate = Instant.ofEpochSecond(2687955), - endDate = Instant.ofEpochSecond(2687991), - defaultCheckInLengthInMinutes = 0, - byteRepresentation = signedTraceLocation.location.toByteArray().toByteString(), - signature = signedTraceLocation.signature.toByteArray().toByteString() - ) + val restoredData = restoredParcel.readBundle()!!.run { + classLoader = VerifiedTraceLocation::class.java.classLoader + getParcelable<VerifiedTraceLocation>("verifiedTraceLocation") + } + restoredData shouldBe expectedVerifiedLocation } -} - -*/ companion object { + private const val BASE64_PAYLOAD_1 = "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekAT" + + "D3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEst" + + "cUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3" + + "cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIECAEQAg==" - // "signedLocation": { - // "location": { - // "guid": "3055331c-2306-43f3-9742-6d8fab54e848", - // "version": 1, - // "type": 2, - // "description": "My Birthday Party", - // "address": "at my place", - // "startTimestamp": 2687955, - // "endTimestamp": 2687991, - // "defaultCheckInLengthInMinutes": 0 - // }, - // "signature": "MEQCIGVKfqPF2851IrEyDeVMazlRnIzLX16H6r1TB37PRzjbAiBGP13ADQcbQZsztKUCZMRcvnv5Mgdo0LY/v3qFMnrUkQ==" - private const val ENCODED_EVENT1 = - "BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVGRSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAFAAAESGGBCAEIDFJJ7KHRO3ZZ2SFMJSBXSUY2ZZKGOIZS27L2D6VPKTA57M6RZY3MBCARR7LXAA2BY3IGNTHNFFAJSMIXF6PP4TEB3I2C3D7P32QUZHVVER" - private const val ENCODED_EVENT1_LOCATION = - "BISDGMBVGUZTGMLDFUZDGMBWFU2DGZRTFU4TONBSFU3GIODGMFRDKNDFHA2DQEABDABCEEKNPEQEE2LSORUGIYLZEBIGC4TUPEVAWYLUEBWXSIDQNRQWGZJQ2OD2IAJY66D2IAKAAA" - - // "signedLocation": { - // "location": { - // "guid": "fca84b37-61c0-4a7c-b2f8-825cadd506cf", - // "version": 1, - // "type": 1, - // "description": "Icecream Shop", - // "address": "Main Street 1", - // "startTimestamp": 0, - // "endTimestamp": 0, - // "defaultCheckInLengthInMinutes": 10 - // }, - // "signature": "MEQCIDWRTM4ujn1GFPuHlgpUnQIWwwzwI8abxSrF5Er2I5HaAiAbucxg+6d3nC/Iwzo7AXehJAS20TRX1S2rl0LO8kcYxA==" - private const val ENCODED_EVENT2 = - "BJHAUJDGMNQTQNDCGM3S2NRRMMYC2NDBG5RS2YRSMY4C2OBSGVRWCZDEGUYDMY3GCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDDARACEA2ZCTGOF2HH2RQU7ODZMCSUTUBBNQYM6AR4NG6FFLC6ISXWEOI5UARADO44YYH3U53ZYL6IYM5DWALXUESAJNWRGRL5KLNLS5BM54SHDDCA" - - private const val INVALID_ENCODED_EVENT = - "NB2HI4DTHIXS653XO4XHK4TCMFXGI2LDORUW63TBOJ4S4Y3PNUXWIZLGNFXGKLTQNBYD65DFOJWT2VDIMUSTEMCDN53GSZBFGIYDCOI=" - - private const val PUB_KEY = - "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEafIKZOiRPuJWjKOUmKv7OTJWTyii4oCQLcGn3FgYoLQaJIvAM3Pl7anFDPPY/jxfqqrLyGc0f6hWQ9JPR3QjBw==" - private const val INVALID_PUB_KEY = - "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + private const val BASE64_PAYLOAD_2 = "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJ" + + "bMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT" + + "0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK" } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt index beb72df52..dc916befb 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt @@ -2,13 +2,21 @@ package de.rki.coronawarnapp.test.eventregistration.ui import android.annotation.SuppressLint import android.os.Bundle +import android.text.SpannedString import android.view.View import android.widget.Toast +import androidx.core.text.bold +import androidx.core.text.buildSpannedString +import androidx.core.text.color +import androidx.core.text.scale +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentTestEventregistrationBinding +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.test.menu.ui.TestMenuItem +import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 @@ -50,10 +58,6 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis showEventsButton.setOnClickListener { findNavController().navigate(R.id.showStoredEventsTestFragment) } - - generateTestTraceLocations.setOnClickListener { - viewModel.generateTestTraceLocations() - } } binding.runMatcher.setOnClickListener { viewModel.runMatcher() @@ -82,8 +86,63 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis viewModel.riskCalculationRuntime.observe2(this) { binding.riskCalculationRuntimeText.text = "Risk calculation runtime in millis: $it" } + + viewModel.lastOrganiserLocation.observe(viewLifecycleOwner) { + binding.lastOrganiserLocationCard.isVisible = it != null + it?.let { traceLocation -> + with(binding) { + lastOrganiserLocation.text = traceLocationText(traceLocation) + lastOrganiserLocationId.text = styleText("ID", traceLocation.locationId.base64()) + lastOrganiserLocationUrl.text = styleText("URL", traceLocation.locationUrl) + } + } + } + + viewModel.lastAttendeeLocation.observe(viewLifecycleOwner) { + binding.lastAttendeeLocationCard.isVisible = it != null + it?.let { traceLocation -> + with(binding) { + lastAttendeeLocation.text = traceLocationText(traceLocation) + lastAttendeeLocationId.text = styleText("ID", traceLocation.locationId.base64()) + lastAttendeeLocationUrl.text = styleText("URL", traceLocation.locationUrl) + } + } + } } + private fun traceLocationText(traceLocation: TraceLocation): SpannedString = with(traceLocation) { + buildSpannedString { + append("TraceLocation [\n") + append(styleText("Id", id)) + append(styleText("type", type)) + append(styleText("version", version)) + append(styleText("address", address)) + append(styleText("description", description)) + append(styleText("startDate", startDate)) + append(styleText("endDate", endDate)) + append(styleText("defaultCheckInLengthInMinutes", defaultCheckInLengthInMinutes)) + append(styleText("cnPublicKey", cnPublicKey)) + append(styleText("cryptographicSeed", cryptographicSeed.base64())) + append("]") + } + } + + private fun styleText(key: String, value: Any?): SpannedString = + buildSpannedString { + bold { + color(requireContext().getColorCompat(R.color.colorAccent)) { + append("$key=") + } + } + + scale(0.85f) { + color(requireContext().getColorCompat(R.color.colorTextPrimary1)) { + append(value.toString()) + } + } + append("\n") + } + companion object { val MENU_ITEM = TestMenuItem( title = "Event Registration", diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt index a87c13dc9..a44796fb2 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt @@ -1,8 +1,12 @@ package de.rki.coronawarnapp.test.eventregistration.ui +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository import de.rki.coronawarnapp.presencetracing.risk.CheckInWarningMatcher @@ -13,17 +17,27 @@ import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.debug.measureTime import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import okio.ByteString.Companion.encode -import org.joda.time.DateTime +import kotlinx.coroutines.flow.map import timber.log.Timber class EventRegistrationTestFragmentViewModel @AssistedInject constructor( - private val dispatcherProvider: DispatcherProvider, - private val traceLocationRepository: TraceLocationRepository, + dispatcherProvider: DispatcherProvider, + traceLocationRepository: TraceLocationRepository, + checkInRepository: CheckInRepository, private val checkInWarningMatcher: CheckInWarningMatcher, private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + val lastOrganiserLocation: LiveData<TraceLocation?> = + traceLocationRepository.allTraceLocations + .map { lastLocationData(it) } + .asLiveData(dispatcherProvider.Default) + + val lastAttendeeLocation: LiveData<TraceLocation?> = + checkInRepository.allCheckIns + .map { lastAttendeeLocationData(it) } + .asLiveData(dispatcherProvider.Default) + private val checkInWarningOverlaps = mutableListOf<CheckInWarningOverlap>() val checkInOverlapsText = MutableLiveData<String>() val matchingRuntime = MutableLiveData<Long>() @@ -93,92 +107,24 @@ class EventRegistrationTestFragmentViewModel @AssistedInject constructor( } } - fun generateTestTraceLocations() { - launch { - val permanent = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_FOOD_SERVICE, - description = "SAP Kantine WDF20", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = null, - endDate = null, - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(permanent) - - val oneDayEvent = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CULTURAL_EVENT, - description = "Jahrestreffen der deutschen SAP Anwendergruppe (one day)", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = DateTime.now().plusHours(2).toInstant(), - endDate = DateTime.now().plusHours(3).toInstant(), - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(oneDayEvent) - - val partyHardEvent = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY, - description = "Jahrestreffen der deutschen SAP Anwendergruppe (many days)", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = DateTime.now().plusHours(2).toInstant(), - endDate = DateTime.now().plusDays(5).plusHours(2).toInstant(), - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(partyHardEvent) - - val oldPermanent = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_FOOD_SERVICE, - description = "SAP Kantine MOW07", - address = "Moscow, Kosmodomianskaya 52/7", - startDate = null, - endDate = null, - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(oldPermanent) - - val oldTemporaryOne = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY, - description = "Old temporary 1", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = DateTime.now().minusSeconds(16 * 86400).toInstant(), - endDate = DateTime.now().minusSeconds(15 * 86400 - 10).toInstant(), - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(oldTemporaryOne) - - val oldTemporaryTwo = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY, - description = "Old temporary 2", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = DateTime.now().minusSeconds(16 * 86400).toInstant(), - endDate = DateTime.now().minusSeconds(15 * 86400).toInstant(), - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(oldTemporaryTwo) - - val oldTemporaryThree = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY, - description = "Old temporary 3", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = DateTime.now().minusSeconds(16 * 86400).toInstant(), - endDate = DateTime.now().minusSeconds(15 * 86400 + 10).toInstant(), - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(oldTemporaryThree) - } + private fun lastLocationData(it: List<TraceLocation>): TraceLocation? = + it.maxByOrNull { traceLocation -> traceLocation.id } + + private fun lastAttendeeLocationData(it: List<CheckIn>): TraceLocation? { + val checkIn = it.maxByOrNull { checkIn -> checkIn.id } ?: return null + + return TraceLocation( + id = checkIn.id, + type = TraceLocationOuterClass.TraceLocationType.forNumber(checkIn.type), + description = checkIn.description, + address = checkIn.address, + startDate = checkIn.traceLocationStart, + endDate = checkIn.traceLocationEnd, + defaultCheckInLengthInMinutes = checkIn.defaultCheckInLengthInMinutes, + cryptographicSeed = checkIn.cryptographicSeed, + cnPublicKey = checkIn.cnPublicKey, + version = checkIn.version + ) } @AssistedFactory diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml index 7cde04273..d35124a53 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml @@ -118,6 +118,82 @@ </LinearLayout> + <LinearLayout + android:id="@+id/lastOrganiserLocationCard" + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_tiny" + android:layout_marginTop="10dp" + android:orientation="vertical"> + + <TextView + style="@style/headline6" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Last organiser location" /> + + <TextView + android:id="@+id/lastOrganiserLocation" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:textIsSelectable="true" /> + + <TextView + android:id="@+id/lastOrganiserLocationUrl" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:autoLink="web" + android:textIsSelectable="true" /> + + <TextView + android:id="@+id/lastOrganiserLocationId" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:textIsSelectable="true" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/lastAttendeeLocationCard" + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_tiny" + android:layout_marginTop="10dp" + android:orientation="vertical"> + + <TextView + style="@style/headline6" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Last attendee location" /> + + <TextView + android:id="@+id/lastAttendeeLocation" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:textIsSelectable="true" /> + + <TextView + android:id="@+id/lastAttendeeLocationUrl" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:autoLink="web" + android:textIsSelectable="true" /> + + <TextView + android:id="@+id/lastAttendeeLocationId" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:textIsSelectable="true" /> + </LinearLayout> + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/event_container" style="@style/Card" diff --git a/Corona-Warn-App/src/main/AndroidManifest.xml b/Corona-Warn-App/src/main/AndroidManifest.xml index d0ebbbe78..2cefbd599 100644 --- a/Corona-Warn-App/src/main/AndroidManifest.xml +++ b/Corona-Warn-App/src/main/AndroidManifest.xml @@ -85,7 +85,6 @@ <data android:host="e.coronawarn.app" - android:pathPrefix="/" android:scheme="https" /> </intent-filter> </activity> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt index 7b73dbd49..699e95b6f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptorOrBuilder interface PresenceTracingConfig { val qrCodeErrorCorrectionLevel: ErrorCorrectionLevel @@ -9,6 +10,7 @@ interface PresenceTracingConfig { val riskCalculationParameters: PresenceTracingRiskCalculationParamContainer val submissionParameters: PresenceTracingSubmissionParamContainer val plausibleDeniabilityParameters: PlausibleDeniabilityParametersContainer + val qrCodeDescriptors: List<PresenceTracingQRCodeDescriptorOrBuilder> interface Mapper : ConfigMapper<PresenceTracingConfig> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt index 91e178527..5957fcdd9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.appconfig import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptorOrBuilder data class PresenceTracingConfigContainer( override val qrCodeErrorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H, @@ -10,5 +11,6 @@ data class PresenceTracingConfigContainer( override val submissionParameters: PresenceTracingSubmissionParamContainer = PresenceTracingSubmissionParamContainer(), override val plausibleDeniabilityParameters: PlausibleDeniabilityParametersContainer = - PlausibleDeniabilityParametersContainer() + PlausibleDeniabilityParametersContainer(), + override val qrCodeDescriptors: List<PresenceTracingQRCodeDescriptorOrBuilder> = emptyList() ) : PresenceTracingConfig diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt index bdba0734e..739a0e6fd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt @@ -8,14 +8,10 @@ import de.rki.coronawarnapp.appconfig.PresenceTracingConfigContainer import de.rki.coronawarnapp.appconfig.PresenceTracingRiskCalculationParamContainer import de.rki.coronawarnapp.appconfig.PresenceTracingSubmissionParamContainer import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid -import de.rki.coronawarnapp.server.protocols.internal.v2 - .PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters -import de.rki.coronawarnapp.server.protocols.internal.v2 - .PresenceTracingParametersOuterClass.PresenceTracingRiskCalculationParameters -import de.rki.coronawarnapp.server.protocols.internal.v2 - .PresenceTracingParametersOuterClass.PresenceTracingParameters.QRCodeErrorCorrectionLevel -import de.rki.coronawarnapp.server.protocols.internal.v2 - .PresenceTracingParametersOuterClass.PresenceTracingPlausibleDeniabilityParameters +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingRiskCalculationParameters +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingParameters.QRCodeErrorCorrectionLevel +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingPlausibleDeniabilityParameters import timber.log.Timber import javax.inject.Inject @@ -95,7 +91,8 @@ class PresenceTracingConfigMapper @Inject constructor() : PresenceTracingConfig. revokedTraceLocationVersions = revokedTraceLocationVersionsList.orEmpty(), riskCalculationParameters = riskCalculationParameters, submissionParameters = submissionParameters, - plausibleDeniabilityParameters = plausibleDeniabilityParameters + plausibleDeniabilityParameters = plausibleDeniabilityParameters, + qrCodeDescriptors = qrCodeDescriptorsOrBuilderList ) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt index 6f76b0b04..68c52400c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt @@ -1,6 +1,5 @@ package de.rki.coronawarnapp.eventregistration.checkins -import com.google.protobuf.ByteString import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.eventregistration.checkins.derivetime.deriveTime import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC @@ -107,5 +106,3 @@ fun CheckIn.determineRiskTransmission(now: Instant, transmissionVector: Transmis val ageInDays = Days.daysBetween(startMidnight, nowMidnight).days return transmissionVector.raw.getOrElse(ageInDays) { 1 } // Default value } - -private fun okio.ByteString.toProtoByteString() = ByteString.copyFrom(toByteArray()) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt deleted file mode 100644 index 935e86b31..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt +++ /dev/null @@ -1,6 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins.qrcode - -class InvalidQRCodeDataException constructor( - message: String? = null, - cause: Throwable? = null -) : QRCodeException(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt deleted file mode 100644 index eae77689d..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt +++ /dev/null @@ -1,6 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins.qrcode - -class InvalidQRCodeSignatureException constructor( - message: String? = null, - cause: Throwable? = null -) : QRCodeException(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt index 1fb4c881a..3df60ea22 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt @@ -1,6 +1,21 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode -open class QRCodeException constructor( +sealed class QRCodeException constructor( message: String? = null, cause: Throwable? = null ) : Exception(message, cause) + +class InvalidQrCodeUriException constructor( + message: String? = null, + cause: Throwable? = null +) : QRCodeException(message, cause) + +class InvalidQrCodePayloadException constructor( + message: String? = null, + cause: Throwable? = null +) : QRCodeException(message, cause) + +class InvalidQrCodeDataException constructor( + message: String? = null, + cause: Throwable? = null +) : QRCodeException(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt index 7d622c342..493d2bdb6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt @@ -1,46 +1,86 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode +import com.google.common.io.BaseEncoding import dagger.Reusable +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptorOrBuilder +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptor.PayloadEncoding import de.rki.coronawarnapp.util.decodeBase32 -import okio.ByteString +import okio.ByteString.Companion.toByteString import timber.log.Timber import java.net.URI import javax.inject.Inject @Reusable -class QRCodeUriParser @Inject constructor() { +class QRCodeUriParser @Inject constructor( + private val configProvider: AppConfigProvider +) { /** - * Validate that QRCode scanned uri matches the following formulas: - * https://e.coronawarn.app/c1/SIGNED_TRACE_LOCATION_BASE32 - * HTTPS://E.CORONAWARN.APP/C1/SIGNED_TRACE_LOCATION_BASE32 + * Parse [QRCodePayload] from [input] + * + * @throws [Exception] such as [QRCodeException], + * exceptions from [URI.create] + * and possible decoding exceptions */ - fun getQrCodePayload(maybeUri: String): ByteString? = URI.create(maybeUri).run { - if (!scheme.equals(SCHEME, true)) return@run null - if (!authority.equals(AUTHORITY, true)) return@run null + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun getQrCodePayload(input: String): QRCodePayload { + Timber.d("input=$input") + try { + URI.create(input) // Verify it is a valid uri + } catch (e: Exception) { + Timber.d(e, "Invalid URI") + throw InvalidQrCodeUriException("Invalid URI", e) + } - if (!path.substringBeforeLast("/").equals(PATH_PREFIX, true)) return@run null + val descriptor = descriptor(input) + val groups = descriptor.matchedGroups(input) - val rawData = path.substringAfterLast("/") - val paddingDiff = 8 - (rawData.length % 8) - val maybeBase32 = rawData + createPadding(paddingDiff) + val payload = groups[descriptor.encodedPayloadGroupIndex] + Timber.d("payload=$payload") - if (!maybeBase32.matches(BASE32_REGEX)) return@run null + val encoding = PayloadEncoding.forNumber(descriptor.payloadEncoding.number) + Timber.d("encoding=$encoding") - return@run try { - maybeBase32.decodeBase32() + val rawPayload = try { + when (encoding) { + PayloadEncoding.BASE32 -> payload.decodeBase32() + PayloadEncoding.BASE64 -> BaseEncoding.base64Url().decode(payload).toByteString() + else -> null + } } catch (e: Exception) { - Timber.w(e, "Data wasn't base32: %s", maybeBase32) + Timber.d(e, "Payload decoding failed") null + } ?: throw InvalidQrCodeDataException("Payload decoding failed") + + return QRCodePayload.parseFrom(rawPayload.toByteArray()) + } + + private suspend fun descriptor(input: String): PresenceTracingQRCodeDescriptorOrBuilder { + val descriptors = configProvider.getAppConfig().presenceTracing.qrCodeDescriptors + Timber.d("descriptors=$descriptors") + val descriptor = descriptors.find { it.regexPattern.toRegex(RegexOption.IGNORE_CASE).matches(input) } + if (descriptor == null) { + Timber.d("Invalid URI - no matchedDescriptor") + throw InvalidQrCodeUriException("Invalid URI - no matchedDescriptor") } + Timber.d("descriptor=$descriptor") + return descriptor } - companion object { - private fun createPadding(length: Int) = (0 until length).joinToString(separator = "") { "=" } + private fun PresenceTracingQRCodeDescriptorOrBuilder.matchedGroups( + input: String + ): List<String> { + val groups = regexPattern + .toRegex(RegexOption.IGNORE_CASE).find(input) // Find matched result [MatchResult] + ?.destructured?.toList().orEmpty() // Destructured groups - excluding the zeroth group (Whole String) + Timber.d("groups=$groups") - private const val SCHEME = "https" - private const val AUTHORITY = "e.coronawarn.app" - private const val PATH_PREFIX = "/c1" - private val BASE32_REGEX = "^([A-Z2-7=]{8})+$".toRegex(RegexOption.IGNORE_CASE) + if (encodedPayloadGroupIndex !in groups.indices) { + Timber.d("Invalid payload - group index is out of bounds") + throw InvalidQrCodePayloadException("Invalid payload - group index is out of bounds") + } + return groups } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodePayload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodePayload.kt new file mode 100644 index 000000000..7ac741299 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodePayload.kt @@ -0,0 +1,57 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CrowdNotifierData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant +import de.rki.coronawarnapp.util.toOkioByteString +import de.rki.coronawarnapp.util.toProtoByteString +import okio.ByteString.Companion.decodeBase64 +import org.joda.time.Instant + +fun TraceLocation.qrCodePayload(): QRCodePayload { + val vendorData = CWALocationData.newBuilder() + .setType(type) + .setDefaultCheckInLengthInMinutes(defaultCheckInLengthInMinutes ?: 0) + .setVersion(TraceLocation.VERSION) + .build() + + val crowdNotifierData = CrowdNotifierData.newBuilder() + .setCryptographicSeed(cryptographicSeed.toProtoByteString()) + .setPublicKey(cnPublicKey.decodeBase64()!!.toProtoByteString()) + .setVersion(TraceLocation.VERSION) + + val locationData = TraceLocationOuterClass.TraceLocation.newBuilder() + .setDescription(description) + .setAddress(address) + .setStartTimestamp(startDate?.seconds ?: 0) + .setEndTimestamp(endDate?.seconds ?: 0) + .setVersion(TraceLocation.VERSION) + .build() + + return QRCodePayload.newBuilder() + .setVendorData(vendorData.toByteString()) + .setCrowdNotifierData(crowdNotifierData) + .setLocationData(locationData) + .setVersion(TraceLocation.VERSION) + .build() +} + +fun QRCodePayload.traceLocation(): TraceLocation { + val cwaLocationData = CWALocationData.parseFrom(vendorData) + return TraceLocation( + version = version, + type = cwaLocationData.type, + defaultCheckInLengthInMinutes = cwaLocationData.defaultCheckInLengthInMinutes, + description = locationData.description, + address = locationData.address, + startDate = locationData.startTimestamp.instant(), + endDate = locationData.endTimestamp.instant(), + cryptographicSeed = crowdNotifierData.cryptographicSeed.toOkioByteString(), + cnPublicKey = crowdNotifierData.publicKey.toOkioByteString().base64() + ) +} + +private fun Long.instant(): Instant? = if (this == 0L) null else secondsToInstant() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt index 92409f9bf..0669968f0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt @@ -1,15 +1,17 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode import android.os.Parcelable +import com.google.common.io.BaseEncoding import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeGenerator +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import okio.ByteString import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.toByteString import org.joda.time.Instant -const val TRACE_LOCATION_VERSION = 1 - @Parcelize data class TraceLocation( val id: Long = 0L, @@ -21,12 +23,53 @@ data class TraceLocation( val defaultCheckInLengthInMinutes: Int?, val cryptographicSeed: ByteString, val cnPublicKey: String, - val version: Int = TRACE_LOCATION_VERSION, + val version: Int = VERSION, ) : Parcelable { + /** + * Return a url for [TraceLocation] to be used as an input for [QrCodeGenerator] + * URL format https://e.coronawarn.app?v=1#QR_CODE_PAYLOAD_BASE64URL + */ + @IgnoredOnParcel + val locationUrl: String by lazy { + val payloadBytes = qrCodePayload().toByteArray() + val base64Url = BaseEncoding.base64Url().omitPadding().encode(payloadBytes) + AUTHORITY.plus(base64Url) + } + + /** + * Returns a byte sequence that serves as an identifier for the trace location. + * The ID is the byte representation of SHA-256 hash. + */ + @IgnoredOnParcel + val locationId: ByteString by lazy { + val cwaDomain = CWA_GUID.toByteArray() + val payloadBytes = qrCodePayload().toByteArray() + val totalByteSequence = cwaDomain + payloadBytes + totalByteSequence.toByteString().sha256() + } + + /** + * Returns SHA-256 hash of [locationId] which itself is SHA-256 hash + */ + @IgnoredOnParcel + val locationIdHash: ByteString by lazy { + locationId.sha256() + } + fun isBeforeStartTime(now: Instant): Boolean = startDate?.isAfter(now) ?: false fun isAfterEndTime(now: Instant): Boolean = endDate?.isBefore(now) ?: false + + companion object { + /** + * Trace location version. This is a static data and not calculated from [TraceLocation] + */ + const val VERSION = 1 + + private const val AUTHORITY = "https://e.coronawarn.app?v=$VERSION#" + private const val CWA_GUID = "CWA-GUID" + } } fun List<TraceLocationEntity>.toTraceLocations() = this.map { it.toTraceLocation() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt index 6eb4f3645..b524577b5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt @@ -7,12 +7,6 @@ import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parceler import kotlinx.parcelize.Parcelize import kotlinx.parcelize.TypeParceler -import okio.Buffer -import okio.ByteString -import okio.ByteString.Companion.encode -import okio.ByteString.Companion.toByteString -import org.joda.time.Instant -import java.util.concurrent.TimeUnit @Parcelize @TypeParceler<TraceLocationOuterClass.TraceLocation, TraceLocationParceler>() @@ -20,42 +14,7 @@ import java.util.concurrent.TimeUnit data class VerifiedTraceLocation( private val protoQrCodePayload: TraceLocationOuterClass.QRCodePayload ) : Parcelable { - - @IgnoredOnParcel private val vendorData by lazy { - TraceLocationOuterClass.CWALocationData.parseFrom(protoQrCodePayload.vendorData) - } - - @IgnoredOnParcel val traceLocation: TraceLocation by lazy { - - TraceLocation( - version = protoQrCodePayload.version, - type = vendorData.type, - description = protoQrCodePayload.locationData.description, - address = protoQrCodePayload.locationData.address, - startDate = protoQrCodePayload.locationData.startTimestamp.toInstant(), - endDate = protoQrCodePayload.locationData.endTimestamp.toInstant(), - defaultCheckInLengthInMinutes = vendorData.defaultCheckInLengthInMinutes, - cryptographicSeed = protoQrCodePayload.crowdNotifierData.cryptographicSeed.toByteArray().toByteString(), - cnPublicKey = protoQrCodePayload.crowdNotifierData.publicKey.toStringUtf8() - ) - } - - @IgnoredOnParcel private val traceLocationHeader: ByteString by lazy { - "CWA-GUID".encode(Charsets.UTF_8) - } - - @IgnoredOnParcel val traceLocationID: ByteString by lazy { - Buffer() - .write(traceLocationHeader) - .write(protoQrCodePayload.toByteArray()) - .readByteString() - } - - /** - * Converts time in seconds into [Instant] - */ - private fun Long.toInstant() = - if (this == 0L) null else Instant.ofEpochMilli(TimeUnit.SECONDS.toMillis(this)) + @IgnoredOnParcel val traceLocation: TraceLocation = protoQrCodePayload.traceLocation() } private object TraceLocationParceler : Parceler<TraceLocationOuterClass.TraceLocation> { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt index eaa180073..b39c06703 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt @@ -12,7 +12,6 @@ import androidx.appcompat.widget.Toolbar import androidx.core.net.toUri import androidx.core.view.isGone import androidx.fragment.app.Fragment -import androidx.navigation.NavOptions import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -24,7 +23,6 @@ import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsFragmentBin import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CameraPermissionVH import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CheckInsItem -import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.list.isSwipeable import de.rki.coronawarnapp.util.list.onSwipeItem @@ -38,6 +36,8 @@ import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted import timber.log.Timber +import java.lang.Exception +import java.net.URLEncoder import javax.inject.Inject class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_fragment), AutoInject { @@ -149,18 +149,6 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag FragmentNavigatorExtras(this to transitionName) ) } - // TODO Remove once feature is done - if (CWADebug.isDeviceForTestersBuild) { - setOnLongClickListener { - findNavController().navigate( - createCheckInUri(DEBUG_CHECKINS.random()), - NavOptions.Builder().apply { - setLaunchSingleTop(true) - }.build() - ) - true - } - } } } @@ -224,15 +212,14 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag } companion object { - fun createCheckInUri(rootUri: String): Uri = "coronawarnapp://check-ins/$rootUri".toUri() - - @Suppress("MaxLineLength") - private val DEBUG_CHECKINS = listOf( - "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDFMNSTKMJYGY3S2NJYHA4S2NBRG5QS2YLGMM3C2ZDDHFRTSNRSGZTGIYZWCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEBFRIDICXSP4QTNMBRDF7EOJ3EIJD6AWT24YDOWWXQI22KCUD7R7WARBAC7ONBRPJDB2KK6QKZLF4RE3PXU7PMON4IOZVIHCYPJGBZ27FF5S4", - "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDEMVRDGZTGMU2C2MZUGQ2C2NBWGZQS2YLCHEYC2NJQHBRDCMBRMVTDIZBTCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEEAJRWAYJARF3V4AS5OVBODPLPX2V3IJFMFU4O2CAKRH6HGHHWCDMJYCEBH7BO2IU2EEGRKEXBZT2DAOFIMXES5ETUT45QIWDCX64APY7C2ME", - "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDBMNQWIMLFHA3S2NZQGVTC2NDDGY3C2ODGGBTC2ZBWGQYDCZJUMRTDEN3FCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEEAKJM3RPYMM2VVCE2GLVK6OKY36F64FRNSI6DWYV7WW6MGESFCDNNQCEA44UHS2GEWHJYHTIJ3AJYM6BC3HEIYHY2HRMPIP7ZF62YBAUKOIY", - "HTTPS://E.CORONAWARN.APP/C1/BJHAUJBQGZRDOMJXHEYC2NBRG44C2NBWGZRC2YRQGA4S2MJTGRRDQOJZMU4DMMRQCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEB365TX5SEWICC3JUOAZCQX5YUK2LZZA7RGRTNBXTSEBXTD2766CAARBADXEYUJHQSE7QRQOIPEMSSPLCVC5D4I3FOBDRX64NASE47XKKK5EY", - "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDGGE4WKMTEMQZC2OJRMUYC2NBQGNTC2OJZMZRC2MTEG4ZWGMJTGA3GEOBTCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEEANT4HDNB7V5DWCKKUV22YQ7NYOBCTOZ2QUFBOUDZS6V5J2VRVLVSICEBU2YHAEBPQSLWTR75VFC6OEFIS22V6KU4NRDYZHTIBMHS4FDADG6", - ) + fun createCheckInUri(rootUri: String): Uri { + val encodedUrl = try { + URLEncoder.encode(rootUri, Charsets.UTF_8.name()) + } catch (e: Exception) { + Timber.d(e, "URL Encoding failed url($rootUri)") + rootUri // Pass original + } + return "coronawarnapp://check-ins/$encodedUrl".toUri() + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt index dbfafa23a..e78ae163b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt @@ -8,13 +8,11 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository -import de.rki.coronawarnapp.eventregistration.checkins.qrcode.InvalidQRCodeDataException import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutHandler -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.ActiveCheckInVH import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CameraPermissionVH import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CheckInsItem @@ -138,15 +136,7 @@ class CheckInsViewModel @AssistedInject constructor( private fun verifyUri(uri: String) = launch { try { Timber.i("uri: $uri") - val qrCodePayloadRaw = qrCodeUriParser.getQrCodePayload(uri)?.toByteArray() - ?: throw IllegalArgumentException("Invalid uri: $uri") - - val qrCodePayload = try { - TraceLocationOuterClass.QRCodePayload.parseFrom(qrCodePayloadRaw) - } catch (e: Exception) { - throw InvalidQRCodeDataException(cause = e, message = "QR-code data could not be parsed.") - } - + val qrCodePayload = qrCodeUriParser.getQrCodePayload(uri) val verifiedTraceLocation = VerifiedTraceLocation(qrCodePayload) events.postValue(CheckInEvent.ConfirmCheckIn(verifiedTraceLocation)) } catch (e: Exception) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt index bd50ffcbe..685f2f245 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt @@ -88,23 +88,26 @@ class ConfirmCheckInViewModel @AssistedInject constructor( ), completed: Boolean = false, createJournalEntry: Boolean = true - ): CheckIn = CheckIn( - traceLocationId = verifiedTraceLocation.traceLocationID, - traceLocationIdHash = verifiedTraceLocation.traceLocationID.sha256(), - version = traceLocation.version, - type = traceLocation.type.number, - description = traceLocation.description, - address = traceLocation.address, - traceLocationStart = traceLocation.startDate, - traceLocationEnd = traceLocation.endDate, - defaultCheckInLengthInMinutes = traceLocation.defaultCheckInLengthInMinutes, - cryptographicSeed = traceLocation.cryptographicSeed, - cnPublicKey = traceLocation.cnPublicKey, - checkInStart = checkInStart, - checkInEnd = checkInEnd, - completed = completed, - createJournalEntry = createJournalEntry, - ) + ): CheckIn { + val traceLocation = verifiedTraceLocation.traceLocation + return CheckIn( + traceLocationId = traceLocation.locationId, + traceLocationIdHash = traceLocation.locationIdHash, + version = traceLocation.version, + type = traceLocation.type.number, + description = traceLocation.description, + address = traceLocation.address, + traceLocationStart = traceLocation.startDate, + traceLocationEnd = traceLocation.endDate, + defaultCheckInLengthInMinutes = traceLocation.defaultCheckInLengthInMinutes, + cryptographicSeed = traceLocation.cryptographicSeed, + cnPublicKey = traceLocation.cnPublicKey, + checkInStart = checkInStart, + checkInEnd = checkInEnd, + completed = completed, + createJournalEntry = createJournalEntry, + ) + } @AssistedFactory interface Factory : CWAViewModelFactory<ConfirmCheckInViewModel> { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt index d9a58e387..863daf7a1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt @@ -53,11 +53,7 @@ class QrCodeDetailViewModel @AssistedInject constructor( traceLocationFlow.value = traceLocation - createQrCode( - "HTTPS://E.CORONAWARN.APP/C1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUD" + - "BOJ2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFF" + - "BU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI=" - ) + createQrCode(traceLocation) } } @@ -91,9 +87,10 @@ class QrCodeDetailViewModel @AssistedInject constructor( /** * Creates a QR Code [Bitmap] ,result is delivered by [qrCodeBitmap] */ - private fun createQrCode(input: String) = launch(context = dispatcher.IO) { - + private fun createQrCode(traceLocation: TraceLocation) = launch(context = dispatcher.IO) { try { + val input = traceLocation.locationUrl + Timber.d("input=$input") qrCodeBitmap.postValue(qrCodeGenerator.createQrCode(input)) } catch (e: Exception) { Timber.d(e, "Qr code creation failed") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoBuf.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoBuf.kt new file mode 100644 index 000000000..c0a185307 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoBuf.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.util + +import com.google.protobuf.ByteString +import okio.ByteString.Companion.toByteString + +fun okio.ByteString.toProtoByteString(): ByteString = ByteString.copyFrom(toByteArray()) +fun ByteString.toOkioByteString() = toByteArray().toByteString() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt index 054e7b947..aee24dd76 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt @@ -3,13 +3,18 @@ package de.rki.coronawarnapp.eventregistration.attendee.confirm import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInNavigation import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInViewModel +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations +import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK +import okio.ByteString.Companion.decodeBase64 +import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -20,19 +25,32 @@ import testhelpers.extensions.getOrAwaitValue @ExtendWith(InstantExecutorExtension::class) class ConfirmCheckInViewModelTest : BaseTest() { - @MockK lateinit var traceLocation: TraceLocation @MockK lateinit var verifiedTraceLocation: VerifiedTraceLocation @MockK lateinit var checkInRepository: CheckInRepository @MockK lateinit var timeStamper: TimeStamper private lateinit var viewModel: ConfirmCheckInViewModel + private val traceLocation = TraceLocation( + id = 1, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "My Birthday Party", + address = "at my place", + startDate = 2687955L.secondsToInstant(), + endDate = 2687991L.secondsToInstant(), + defaultCheckInLengthInMinutes = null, + cryptographicSeed = "CRYPTOGRAPHIC_SEED".decodeBase64()!!, + cnPublicKey = "PUB_KEY", + version = TraceLocation.VERSION + ) + @BeforeEach fun setUp() { MockKAnnotations.init(this) + coEvery { checkInRepository.addCheckIn(any()) } returns 1L every { verifiedTraceLocation.traceLocation } returns traceLocation - every { traceLocation.defaultCheckInLengthInMinutes } returns 10 + every { timeStamper.nowUTC } returns Instant.parse("2021-03-04T10:30:00Z") viewModel = ConfirmCheckInViewModel( verifiedTraceLocation = verifiedTraceLocation, @@ -49,8 +67,7 @@ class ConfirmCheckInViewModelTest : BaseTest() { @Test fun onConfirmEvent() { - // TODO -// viewModel.onConfirmTraceLocation() -// viewModel.events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.ConfirmNavigation + viewModel.onConfirmTraceLocation() + viewModel.events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.ConfirmNavigation } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base32UrlProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base32UrlProvider.kt new file mode 100644 index 000000000..036343e6d --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base32UrlProvider.kt @@ -0,0 +1,81 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CrowdNotifierData +import de.rki.coronawarnapp.util.toProtoByteString +import okio.ByteString.Companion.decodeBase64 +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class Base32UrlProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { + return Stream.of( + Arguments.of( + "https://e.coronawarn.app?v=1#BAARELAIAEJBCTLZEBBGS4TUNBSGC6JAKBQXE5DZDIFWC5BANV4SA4DMMFRWKKGTQ6SA" + + "CMHXQ6SACGTFBAAREWZQLEYBGBQHFKDERTR5AIAQMCBKQZEM4PIDAEDQGQQAARZ3BRFS24KCCFZSSN7E4YBSPXT7QU4DO" + + "UKYNRUDLSSPJTFOZWCHHV5DFTRKISOOU5Y3ENLQ2WS2HYW5YAPUE3C6XBJRSAEF6352AOK6DICDCMRTGQRAICABCABA", + QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocation.newBuilder() + .setDescription("My Birthday Party") + .setAddress("at my place") + .setStartTimestamp(2687955) + .setEndTimestamp(2687991) + .setVersion(1) + .build() + ) + .build(), + CWALocationData.newBuilder() + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER) + .setVersion(1) + .build() + ), + Arguments.of( + "https://e.coronawarn.app?v=1#BAAREIAIAEJA2SLDMVRXEZLBNUQFG2DPOANA2TLBNFXCAU3UOJSWK5BAGENGKCABCJNTA" + + "WJQCMDAOKUGJDHD2AQBAYECVBSIZY6QGAIHANBAABDTWDCLFVYUEELTFE36JZQDE7PH7BJYG5IVQ3DIGXFE6TGK5TMEOPL" + + "2GLHCURE45J3RWI2XBVNFUPRN3QA7IJWF5OCTDEAIL5X3UA4V4GQEGEZDGNBCAYEACEABDAFA", + QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQARgK".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocation.newBuilder() + .setDescription("Icecream Shop") + .setAddress("Main Street 1") + .setVersion(1) + .build() + ) + .build(), + CWALocationData.newBuilder() + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER) + .setVersion(1) + .setDefaultCheckInLengthInMinutes(10) + .build() + ) + ) + } + + companion object { + const val CRYPTOGRAPHIC_SEED = "MTIzNA==" + const val PUB_KEY = + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base64UrlProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base64UrlProvider.kt new file mode 100644 index 000000000..2d92ae6bc --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base64UrlProvider.kt @@ -0,0 +1,82 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CrowdNotifierData +import de.rki.coronawarnapp.util.toProtoByteString +import okio.ByteString.Companion.decodeBase64 +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class Base64UrlProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { + return Stream.of( + Arguments.of( + "https://e.coronawarn.app?v=1#CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmoIARJgO" + + "MTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-vox" + + "Q1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGgQxMjM0IgQIARAC", + QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocation.newBuilder() + .setDescription("My Birthday Party") + .setAddress("at my place") + .setStartTimestamp(2687955) + .setEndTimestamp(2687991) + .setVersion(1) + .build() + ) + .build(), + CWALocationData.newBuilder() + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER) + .setVersion(1) + .build() + ), + Arguments.of( + "https://e.coronawarn.app?v=1#CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmoIARJgOMTa6eYSiaDv8lW1" + + "3xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ" + + "1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGgQxMjM0IgYIARABGAo", + QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQARgK".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocation.newBuilder() + .setDescription("Icecream Shop") + .setAddress("Main Street 1") + .setVersion(1) + .build() + ) + .build(), + CWALocationData.newBuilder() + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER) + .setVersion(1) + .setDefaultCheckInLengthInMinutes(10) + .build() + ) + ) + } + + companion object { + const val CRYPTOGRAPHIC_SEED = "MTIzNA==" + const val PUB_KEY = + "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9" + + "eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest2.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest2.kt deleted file mode 100644 index 5a767d467..000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest2.kt +++ /dev/null @@ -1,64 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins.qrcode - -import testhelpers.BaseTest - -class DefaultQRCodeVerifierTest2 : BaseTest() { - - /* Disabled, because new protobuf doesn't include signed traceLocation, we should write tests for the parsing of - QrCodePayload protobuf ... - - @Test - fun `protobuf decoding 1`() { - val signedTraceLocation = - TraceLocationOuterClass.SignedTraceLocation.parseFrom( - "BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVGRSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAFAAAESGGBCAEIDFJJ7KHRO3ZZ2SFMJSBXSUY2ZZKGOIZS27L2D6VPKTA57M6RZY3MBCARR7LXAA2BY3IGNTHNFFAJSMIXF6PP4TEB3I2C3D7P32QUZHVVER" - .decodeBase32().toByteArray() - ) - - signedTraceLocation.apply { - TraceLocationOuterClass.TraceLocation.parseFrom(location).apply { - guid shouldBe "3055331c-2306-43f3-9742-6d8fab54e848" - version shouldBe 1 - typeValue shouldBe 2 - description shouldBe "My Birthday Party" - address shouldBe "at my place" - startTimestamp shouldBe 2687955 - endTimestamp shouldBe 2687991 - defaultCheckInLengthInMinutes shouldBe 0 - } - signature.toByteArray().toByteString() - .base64() shouldBe "MEQCIGVKfqPF2851IrEyDeVMazlRnIzLX16H6r1TB37PRzjbAiBGP13ADQcbQZsztKUCZMRcvnv5Mgdo0LY/v3qFMnrUkQ==" - } - - signedTraceLocation.location.toByteArray().toByteString() - .base64() shouldBe "CiQzMDU1MzMxYy0yMzA2LTQzZjMtOTc0Mi02ZDhmYWI1NGU4NDgQARgCIhFNeSBCaXJ0aGRheSBQYXJ0eSoLYXQgbXkgcGxhY2Uw04ekATj3h6QBQAA=" - } - - @Test - fun `protobuf decoding 2`() { - val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom( - "BJHAUJDGMNQTQNDCGM3S2NRRMMYC2NDBG5RS2YRSMY4C2OBSGVRWCZDEGUYDMY3GCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDDARACEA2ZCTGOF2HH2RQU7ODZMCSUTUBBNQYM6AR4NG6FFLC6ISXWEOI5UARADO44YYH3U53ZYL6IYM5DWALXUESAJNWRGRL5KLNLS5BM54SHDDCA" - .decodeBase32().toByteArray() - ) - - signedTraceLocation.apply { - TraceLocationOuterClass.TraceLocation.parseFrom(location).apply { - guid shouldBe "fca84b37-61c0-4a7c-b2f8-825cadd506cf" - version shouldBe 1 - typeValue shouldBe 1 - description shouldBe "Icecream Shop" - address shouldBe "Main Street 1" - startTimestamp shouldBe 0 - endTimestamp shouldBe 0 - defaultCheckInLengthInMinutes shouldBe 10 - } - signature.toByteArray().toByteString() - .base64() shouldBe "MEQCIDWRTM4ujn1GFPuHlgpUnQIWwwzwI8abxSrF5Er2I5HaAiAbucxg+6d3nC/Iwzo7AXehJAS20TRX1S2rl0LO8kcYxA==" - } - - signedTraceLocation.location.toByteArray().toByteString() - .base64() shouldBe "CiRmY2E4NGIzNy02MWMwLTRhN2MtYjJmOC04MjVjYWRkNTA2Y2YQARgBIg1JY2VjcmVhbSBTaG9wKg1NYWluIFN0cmVldCAxMAA4AEAK" - } - - */ -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt index 71a1c8722..c01a15b2b 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt @@ -1,24 +1,91 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.PresenceTracingConfigContainer +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptor +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptor.PayloadEncoding +import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ArgumentsSource import testhelpers.BaseTest +@Suppress("BlockingMethodInNonBlockingContext") class QRCodeUriParserTest : BaseTest() { - fun createInstance() = QRCodeUriParser() + @MockK lateinit var configProvider: AppConfigProvider + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + coEvery { configProvider.getAppConfig() } returns mockk<ConfigData>().apply { + every { presenceTracing } returns PresenceTracingConfigContainer( + qrCodeDescriptors = listOf( + PresenceTracingQRCodeDescriptor.newBuilder() + .setVersionGroupIndex(0) + .setEncodedPayloadGroupIndex(1) + .setPayloadEncoding(PayloadEncoding.BASE64) + .setRegexPattern("https://e\\.coronawarn\\.app\\?v=(\\d+)\\#(.+)") + .build() + ) + ) + } + } + + fun createInstance() = QRCodeUriParser(configProvider) @ParameterizedTest - @ArgumentsSource(ValidUrlProvider::class) - fun `Valid URLs`(input: String) { - createInstance().getQrCodePayload(input) shouldNotBe null + @ArgumentsSource(Base64UrlProvider::class) + fun `Base64 Valid URLs`( + input: String, + expectedPayload: QRCodePayload, + expectedVendorData: CWALocationData + ) = runBlockingTest { + val qrCodePayload = createInstance().getQrCodePayload(input) + qrCodePayload shouldBe expectedPayload + CWALocationData.parseFrom(qrCodePayload.vendorData) shouldBe expectedVendorData + } + + @ParameterizedTest + @ArgumentsSource(Base32UrlProvider::class) + fun `Base32 Valid URLs`( + input: String, + expectedPayload: QRCodePayload, + expectedVendorData: CWALocationData + ) = runBlockingTest { + coEvery { configProvider.getAppConfig() } returns mockk<ConfigData>().apply { + every { presenceTracing } returns PresenceTracingConfigContainer( + qrCodeDescriptors = listOf( + PresenceTracingQRCodeDescriptor.newBuilder() + .setVersionGroupIndex(0) + .setEncodedPayloadGroupIndex(1) + .setPayloadEncoding(PayloadEncoding.BASE32) + .setRegexPattern("https://e\\.coronawarn\\.app\\?v=(\\d+)\\#(.+)") + .build() + ) + ) + } + + val qrCodePayload = createInstance().getQrCodePayload(input) + qrCodePayload shouldBe expectedPayload + CWALocationData.parseFrom(qrCodePayload.vendorData) shouldBe expectedVendorData } @ParameterizedTest @ArgumentsSource(InvalidUrlProvider::class) - fun `Invalid URLs`(input: String) { - createInstance().getQrCodePayload(input) shouldBe null + fun `Invalid URLs`(input: String) = runBlockingTest { + shouldThrow<InvalidQrCodeUriException> { + createInstance().getQrCodePayload(input) + } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt deleted file mode 100644 index f7e0bf42f..000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt +++ /dev/null @@ -1,38 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins.qrcode - -import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.ArgumentsProvider -import java.util.stream.Stream - -class ValidUrlProvider : ArgumentsProvider { - override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { - return Stream.of( - Arguments.of( - "HTTPS://E.CORONAWARN.APP/C1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBO" + - "J2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2" + - "SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI=" - ), - Arguments.of( - "https://e.coronawarn.app/c1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBO" + - "J2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2" + - "SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI=" - ), - Arguments.of( - "https://e.coronawarn.app/c1/BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVG" + - "RSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAF" + - "AAAESIGBDAEIIARVENF6QT6XZATJ5GSDHL77BCAGR6QKDEUJRP2RDCTKTS7QECWMFAEIIA47MT2EA7MQK" + - "GNQU2XCY3Y2ZOZXCILDPC65PBUO4JJHT5LQQWDQSA" - ), - Arguments.of( - "https://e.coronawarn.app/c1/BJHAUJDGMNQTQNDCGM3S2NRRMMYC2NDBG5RS2YRSMY4C2OBSGVRWCZDEG" + - "UYDMY3GCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEB" + - "NEPPKKTAAIH5BSV45EPOINHOASARJLYYSHNTUUHLNGVYUZXZEBWARBACD53WYEGYXYQS3STOFLSOVM3XX" + - "D5A5HKMFQR7WYYARKKVOFGYGHO" - ), - Arguments.of("https://e.coronawarn.app/c1/JBSWY3DPEBLW64TMMQQQ===="), - Arguments.of("https://e.coronawarn.app/c1/JBSWY3DPEBLW64TMMQQQ"), - Arguments.of("https://e.coronawarn.app/c1/JBSWY3DPEBLW64TMMQQQ========"), - ) - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/QrCodePayloadTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/QrCodePayloadTest.kt new file mode 100644 index 000000000..683a64563 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/QrCodePayloadTest.kt @@ -0,0 +1,67 @@ +package de.rki.coronawarnapp.eventregistration.events + +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.qrCodePayload +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.decodeBase64 +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class QrCodePayloadTest : BaseTest() { + + @Test + fun `Trace location to QrCodePayload 1`() { + val traceLocation = TraceLocation( + id = 1, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "My Birthday Party", + address = "at my place", + startDate = 2687955L.secondsToInstant(), + endDate = 2687991L.secondsToInstant(), + defaultCheckInLengthInMinutes = null, + cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, + cnPublicKey = PUB_KEY, + version = TraceLocation.VERSION + ) + + traceLocation.qrCodePayload() shouldBe + TraceLocationOuterClass.QRCodePayload.parseFrom(PAYLOAD_1.decodeBase64()!!.toByteArray()) + } + + @Test + fun `Trace location to QrCodePayload 2`() { + val traceLocation = TraceLocation( + id = 2, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER, + description = "Icecream Shop", + address = "Main Street 1", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 10, + cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, + cnPublicKey = PUB_KEY, + version = TraceLocation.VERSION + ) + + traceLocation.qrCodePayload() shouldBe + TraceLocationOuterClass.QRCodePayload.parseFrom(PAYLOAD_2.decodeBase64()!!.toByteArray()) + } + + companion object { + private const val PAYLOAD_1 = + "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKo" + + "ZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxe" + + "uFMZAIX2+6A5XhoEMTIzNCIECAEQAg==" + + private const val PAYLOAD_2 = + "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIR" + + "cyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK" + + private const val CRYPTOGRAPHIC_SEED = "MTIzNA==" + private const val PUB_KEY = + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0z" + + "K7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt new file mode 100644 index 000000000..bc1c62af2 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt @@ -0,0 +1,77 @@ +package de.rki.coronawarnapp.eventregistration.events + +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.traceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.decodeBase64 +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class TraceLocationIdTest : BaseTest() { + @Test + fun `locationId from qrCodePayloadBase64 - 1`() { + val qrCodePayloadBase64 = + "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKo" + + "ZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxe" + + "uFMZAIX2+6A5XhoEMTIzNCIECAEQAg==" + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + qrCodePayloadBase64.decodeBase64()!!.toByteArray() + ) + qrCodePayload.traceLocation().locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" + } + + @Test + fun `locationId from qrCodePayloadBase64 - 2`() { + val qrCodePayloadBase64 = + "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIR" + + "cyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK" + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + qrCodePayloadBase64.decodeBase64()!!.toByteArray() + ) + qrCodePayload.traceLocation().locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" + } + + @Test + fun `locationId from traceLocation - 1`() { + val traceLocation = TraceLocation( + id = 1, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "My Birthday Party", + address = "at my place", + startDate = 2687955L.secondsToInstant(), + endDate = 2687991L.secondsToInstant(), + defaultCheckInLengthInMinutes = null, + cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, + cnPublicKey = PUB_KEY, + version = TraceLocation.VERSION + ) + traceLocation.locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" + } + + @Test + fun `locationId from traceLocation - 2`() { + val traceLocation = TraceLocation( + id = 2, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER, + description = "Icecream Shop", + address = "Main Street 1", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 10, + cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, + cnPublicKey = PUB_KEY, + version = TraceLocation.VERSION + ) + + traceLocation.locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" + } + + companion object { + private const val CRYPTOGRAPHIC_SEED = "MTIzNA==" + private const val PUB_KEY = + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0z" + + "K7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUrlTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUrlTest.kt new file mode 100644 index 000000000..aee6965e7 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUrlTest.kt @@ -0,0 +1,62 @@ +package de.rki.coronawarnapp.eventregistration.events + +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runBlockingTest +import okio.ByteString.Companion.decodeBase64 +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class TraceLocationUrlTest : BaseTest() { + + @Test + fun `locationUrl 1`() = runBlockingTest { + val traceLocation = TraceLocation( + id = 1, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "My Birthday Party", + address = "at my place", + startDate = 2687955L.secondsToInstant(), + endDate = 2687991L.secondsToInstant(), + defaultCheckInLengthInMinutes = null, + cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, + cnPublicKey = PUB_KEY, + version = TraceLocation.VERSION + ) + + traceLocation.locationUrl shouldBe + "https://e.coronawarn.app?v=1#CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmoIAR" + + "JgOMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJ" + + "QkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGgQxMjM0IgQIARAC" + } + + @Test + fun `locationUrl 2`() = runBlockingTest { + val traceLocation = TraceLocation( + id = 2, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER, + description = "Icecream Shop", + address = "Main Street 1", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 10, + cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, + cnPublicKey = PUB_KEY, + version = TraceLocation.VERSION + ) + + traceLocation.locationUrl shouldBe + "https://e.coronawarn.app?v=1#CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmoIARJgOMTa6eYSiaDv8l" + + "W13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJQkEIujVwoCNK0MNG" + + "uDK1ayjGxeDc4UDGgQxMjM0IgYIARABGAo" + } + + companion object { + private const val CRYPTOGRAPHIC_SEED = "MTIzNA==" + private const val PUB_KEY = + "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9" + + "eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt index 77d37b628..bfbd07470 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutHandler +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.ActiveCheckInVH import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CameraPermissionVH import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.PastCheckInVH @@ -23,7 +24,6 @@ import io.mockk.verify import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest -import okio.ByteString import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -80,11 +80,12 @@ class CheckInsViewModelTest : BaseTest() { @Test fun `DeepLink verification`() = runBlockingTest { every { savedState.get<String>(any()) } returns null - every { qrCodeUriParser.getQrCodePayload(any()) } returns ByteString.EMPTY + coEvery { qrCodeUriParser.getQrCodePayload(any()) } returns + TraceLocationOuterClass.QRCodePayload.newBuilder().build() createInstance(deepLink = DEEP_LINK, scope = this).apply { events.getOrAwaitValue().shouldBeInstanceOf<CheckInEvent.ConfirmCheckIn>() - verify { + coVerify { savedState.get<String>(any()) qrCodeUriParser.getQrCodePayload(any()) savedState.set(any(), any<String>()) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ProtoBufKtTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ProtoBufKtTest.kt new file mode 100644 index 000000000..b1678a5b5 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ProtoBufKtTest.kt @@ -0,0 +1,28 @@ +package de.rki.coronawarnapp.util + +import com.google.protobuf.ByteString +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.toByteString +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class ProtoBufKtTest : BaseTest() { + + @Test + fun toProtoByteString() { + val okioByteString = KEY.toByteArray().toByteString() + + okioByteString.toProtoByteString() shouldBe ByteString.copyFromUtf8(KEY) + } + + @Test + fun toOkioByteString() { + val protoByteString = ByteString.copyFromUtf8(KEY) + + protoByteString.toOkioByteString() shouldBe KEY.toByteArray().toByteString() + } + + companion object { + private const val KEY = "No generated key" + } +} -- GitLab