Skip to content
Snippets Groups Projects
Unverified Commit 4c0101be authored by Matthias Urhahn's avatar Matthias Urhahn Committed by GitHub
Browse files

QR Code parsing (DEV/EXPOSUREAPP-5841) (#2622)

* Adopt new signedTraceLocationFormat

* QR Code decoding adjustment.

* Adjust failing test & refactoring.

* Fix typo.

* Remove duplicate data class and re-use DefaultTraceLocation.
Adjust to new signature/location bytearray encoding.

* Remove extra interface for TraceLocation and use the dataclass directly.

* Package refactoring

* Fix package import.

* Fix package import in nav graph

* Fix additional package imports.

* Fix additional package imports.
parent 2fee6d34
No related branches found
No related tags found
No related merge requests found
Showing
with 360 additions and 303 deletions
//package de.rki.coronawarnapp.eventregistration.checkins.qrcode
//
//import com.google.protobuf.ByteString
//import de.rki.coronawarnapp.environment.EnvironmentSetup
//import de.rki.coronawarnapp.eventregistration.common.decodeBase32
//import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
//import de.rki.coronawarnapp.util.security.SignatureValidation
//import io.kotest.assertions.throwables.shouldNotThrowAny
//import io.kotest.assertions.throwables.shouldThrow
//import io.kotest.matchers.shouldBe
//import io.mockk.MockKAnnotations
//import io.mockk.every
//import io.mockk.impl.annotations.MockK
//import kotlinx.coroutines.test.runBlockingTest
//import okio.ByteString.Companion.toByteString
//import org.joda.time.Instant
//import org.junit.Before
//import org.junit.Ignore
//import org.junit.Test
//import org.junit.runner.RunWith
//import org.junit.runners.JUnit4
//import testhelpers.BaseTestInstrumentation
//
//@RunWith(JUnit4::class)
//@Ignore("Until format is clarified")
//class DefaultQRCodeVerifierTest : BaseTestInstrumentation() {
//
// @MockK lateinit var environmentSetup: EnvironmentSetup
// private lateinit var qrCodeVerifier: QRCodeVerifier
//
// @Before
// fun setUp() {
// MockKAnnotations.init(this)
// every { environmentSetup.appConfigVerificationKey } returns PUB_KEY
// qrCodeVerifier = DefaultQRCodeVerifier(SignatureValidation(environmentSetup))
// }
//
// @Test
// fun verifyEventSuccess() = runBlockingTest {
// val time = 2687960 * 1_000L
// val instant = Instant.ofEpochMilli(time)
// shouldNotThrowAny {
// val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT.decodeBase32().toByteArray())
// verifyResult.apply {
// singedTraceLocation.location.description shouldBe "CWA Launch Party"
// verifyResult.isBeforeStartTime(instant) shouldBe false
// verifyResult.isAfterEndTime(instant) shouldBe false
// }
// }
// }
//
// @Test
// fun verifyEventStartTimeWaning() = runBlockingTest {
// val time = 2687940 * 1_000L
// val instant = Instant.ofEpochMilli(time)
// shouldNotThrowAny {
// val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT.decodeBase32().toByteArray())
// verifyResult.apply {
// singedTraceLocation.location.description shouldBe "CWA Launch Party"
// }
// verifyResult.isBeforeStartTime(instant) shouldBe true
// verifyResult.isAfterEndTime(instant) shouldBe false
// }
// }
//
// @Test
// fun verifyEventEndTimeWarning() = runBlockingTest {
// val instant = Instant.now()
// shouldNotThrowAny {
// val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT.decodeBase32().toByteArray())
// verifyResult.apply {
// singedTraceLocation.location.description shouldBe "CWA Launch Party"
// }
// verifyResult.isBeforeStartTime(instant) shouldBe false
// verifyResult.isAfterEndTime(instant) shouldBe true
// }
// }
//
// @Test
// fun verifyEventWithInvalidKey() = runBlockingTest {
// every { environmentSetup.appConfigVerificationKey } returns INVALID_PUB_KEY
// shouldThrow<InvalidQRCodeSignatureException> {
// qrCodeVerifier.verify(ENCODED_EVENT.decodeBase32().toByteArray())
// }
// }
//
// @Test
// fun eventHasMalformedData() = runBlockingTest {
// shouldThrow<InvalidQRCodeDataException> {
// qrCodeVerifier.verify(INVALID_ENCODED_EVENT.decodeBase32().toByteArray())
// }
// }
//
// @Test
// fun decodingTest1() = runBlockingTest {
// val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.newBuilder().apply {
// signature = ByteString.copyFromUtf8(
// "MEYCIQCNSNL6E/XyCaemkM6//CIBo+goZKJi/URimqcvwIKzCgIhAOfZPRAfZBRmwpq4sbxrLs3EhY3i914aO4lJ59XCFhwk"
// )
// location = TraceLocationOuterClass.TraceLocation.parseFrom(
// "BISDGMBVGUZTGMLDFUZDGMBWFU2DGZRTFU4TONBSFU3GIODGMFRDKNDFHA2DQEABDABCEEKNPEQEE2LSORUGIYLZEBIGC4TUPEVAWYLUEBWXSIDQNRQWGZJQ2OD2IAJY66D2IAKAAA"
// .decodeBase32().toByteArray()
// )
// }.build()
//
// val base32 = signedTraceLocation.toByteArray().toByteString().base32()
//
// shouldNotThrowAny {
// val verifyResult = qrCodeVerifier.verify(base32.decodeBase32().toByteArray())
//
// verifyResult.apply {
// singedTraceLocation.location.description shouldBe "My Birthday Party"
// signedTraceLocation.signature shouldBe "MEYCIQCNSNL6E/XyCaemkM6//CIBo+goZKJi/URimqcvwIKzCgIhAOfZPRAfZBRmwpq4sbxrLs3EhY3i914aO4lJ59XCFhwk"
// }
// }
// }
//
// @Test
// fun decodingTest2() = runBlockingTest {
// val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.newBuilder().apply {
// signature = ByteString.copyFromUtf8(
// "MEUCIFpHvUqYAIP0Mq86R7kNO4EgRSvGJHbOlDraauKZvkgbAiEAh93bBDYviEtym4q5Oqzd7j6Dp1MLCP7YwCKlVcU2DHc="
// )
// location = TraceLocationOuterClass.TraceLocation.parseFrom(
// "BISGMY3BHA2GEMZXFU3DCYZQFU2GCN3DFVRDEZRYFU4DENLDMFSGINJQGZRWMEABDAASEDKJMNSWG4TFMFWSAU3IN5YCUDKNMFUW4ICTORZGKZLUEAYTAABYABAAU"
// .decodeBase32().toByteArray()
// )
// }.build()
//
// val base32 = signedTraceLocation.toByteArray().toByteString().base32()
//
// shouldNotThrowAny {
// val verifyResult = qrCodeVerifier.verify(base32.decodeBase32().toByteArray())
//
// verifyResult.apply {
// singedTraceLocation.location.description shouldBe "Icecream Shop"
// signedTraceLocation.signature shouldBe "MEUCIFpHvUqYAIP0Mq86R7kNO4EgRSvGJHbOlDraauKZvkgbAiEAh93bBDYviEtym4q5Oqzd7j6Dp1MLCP7YwCKlVcU2DHc="
// }
// }
// }
//
// companion object {
//
// private const val INVALID_PUB_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg" +
// "3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg=="
//
// private const val PUB_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEafIKZOiRPuJWjKOUmKv7OTJWTyii" +
// "4oCQLcGn3FgYoLQaJIvAM3Pl7anFDPPY/jxfqqrLyGc0f6hWQ9JPR3QjBw=="
//
// private const val INVALID_ENCODED_EVENT =
// "BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHRKCC7CTDWGQ" +
// "4JCO7RVZSWVIMQK4UPA.GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3U" +
// "U3P6SXOL7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ"
//
// private const val ENCODED_EVENT =
// "BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBOJ2" +
// "HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGC" +
// "PUZ2RQACAYEJ3HQYMAFFBU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU" +
// "7TYERH23B746RQTABO3CTI="
// }
//}
package de.rki.coronawarnapp.eventregistration.checkins.qrcode
import android.os.Bundle
import android.os.Parcel
import de.rki.coronawarnapp.environment.EnvironmentSetup
import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
import de.rki.coronawarnapp.util.base32
import de.rki.coronawarnapp.util.decodeBase32
import de.rki.coronawarnapp.util.security.SignatureValidation
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.test.runBlockingTest
import okio.ByteString.Companion.toByteString
import org.joda.time.Instant
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import testhelpers.BaseTestInstrumentation
@RunWith(JUnit4::class)
class TraceLocationVerifierTest : BaseTestInstrumentation() {
@MockK lateinit var environmentSetup: EnvironmentSetup
private lateinit var traceLocationQRCodeVerifier: TraceLocationQRCodeVerifier
@Before
fun setUp() {
MockKAnnotations.init(this)
every { environmentSetup.appConfigVerificationKey } returns PUB_KEY
traceLocationQRCodeVerifier = TraceLocationQRCodeVerifier(SignatureValidation(environmentSetup))
}
@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"
verifyResult.isBeforeStartTime(instant) shouldBe false
verifyResult.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,
signature = verifyResult.signedTraceLocation.signature.toByteArray().toByteString()
)
verifyResult.verifiedTraceLocation shouldBe expectedTraceLocation
val bundle = Bundle().apply {
putParcelable("test", verifyResult.verifiedTraceLocation)
}
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"
}
verifyResult.isBeforeStartTime(instant) shouldBe true
verifyResult.isAfterEndTime(instant) shouldBe false
}
}
@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"
}
verifyResult.isBeforeStartTime(instant) shouldBe false
verifyResult.isAfterEndTime(instant) shouldBe true
}
}
@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 decodingTest1() = runBlockingTest {
val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom(
ENCODED_EVENT1.decodeBase32().toByteArray()
)
val expectedSignature =
"MEQCIGVKfqPF2851IrEyDeVMazlRnIzLX16H6r1TB37PRzjbAiBGP13ADQcbQZsztKUCZMRcvnv5Mgdo0LY/v3qFMnrUkQ=="
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
}
}
}
@Test
fun decodingTest2() = runBlockingTest {
val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom(
ENCODED_EVENT2.decodeBase32().toByteArray()
)
val expectedSignature =
"MEQCIDWRTM4ujn1GFPuHlgpUnQIWwwzwI8abxSrF5Er2I5HaAiAbucxg+6d3nC/Iwzo7AXehJAS20TRX1S2rl0LO8kcYxA=="
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
}
}
}
@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 = TraceLocationVerifyResult(
signedTraceLocation = signedTraceLocation,
traceLocation = traceLocation
).verifiedTraceLocation
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,
signature = signedTraceLocation.signature.toByteArray().toByteString()
)
}
}
companion object {
// "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=="
}
}
......@@ -16,7 +16,7 @@ object TraceLocationDatabaseData {
startDate = Instant.parse("2021-01-01T12:00:00.000Z"),
endDate = Instant.parse("2021-01-01T18:00:00.000Z"),
defaultCheckInLengthInMinutes = null,
signature = "signature1"
signatureBase64 = "signature1"
)
val testTraceLocation2 = TraceLocationEntity(
......@@ -28,6 +28,6 @@ object TraceLocationDatabaseData {
startDate = null,
endDate = null,
defaultCheckInLengthInMinutes = 15,
signature = "signature2"
signatureBase64 = "signature2"
)
}
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkins
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifyResult
import de.rki.coronawarnapp.eventregistration.common.decodeBase32
import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.matchers.shouldBe
import org.joda.time.Instant
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import testhelpers.BaseTestInstrumentation
@RunWith(JUnit4::class)
@Ignore("FIXME: Provide new encoded signed trace location samples")
class VerifiedTraceLocationKtTest : BaseTestInstrumentation() {
@Test
fun testVerifiedTraceLocationMapping() {
shouldNotThrowAny {
val signedTraceLocation =
TraceLocationOuterClass.SignedTraceLocation.parseFrom(
DECODED_TRACE_LOCATION.decodeBase32().toByteArray()
)
val verifiedTraceLocation =
QRCodeVerifyResult(singedTraceLocation = signedTraceLocation).toVerifiedTraceLocation()
verifiedTraceLocation shouldBe VerifiedTraceLocation(
guid = "Yc48RFi/hfyXKlF4DEDs/w==",
start = Instant.parse("1970-02-01T02:39:15.000Z"),
end = Instant.parse("1970-02-01T02:39:51.000Z"),
defaultCheckInLengthInMinutes = 30,
description = "CWA Launch Party"
)
}
}
companion object {
private const val DECODED_TRACE_LOCATION =
"BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBOJ2HSGGTQ6SACIHXQ6SAC" +
"KA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2SQCEEAJAUCJSQJ7WDM6" +
"75MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI="
}
}
......@@ -3,14 +3,14 @@ package de.rki.coronawarnapp.test.eventregistration.ui.createevent
import androidx.lifecycle.MutableLiveData
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.eventregistration.events.DefaultTraceLocation
import de.rki.coronawarnapp.eventregistration.events.TraceLocation
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository
import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
import okio.ByteString.Companion.toByteString
import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormat
import timber.log.Timber
......@@ -51,7 +51,7 @@ class CreateEventTestViewModel @AssistedInject constructor(
if (type == "Event") TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER
else TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER
val traceLocation = DefaultTraceLocation(
val traceLocation = TraceLocation(
UUID.randomUUID().toString(), // will be provided by the server when the endpoint is ready
traceLocationType,
description,
......@@ -59,7 +59,7 @@ class CreateEventTestViewModel @AssistedInject constructor(
startDate?.toInstant(),
endDate?.toInstant(),
defaultCheckInLengthInMinutes.toInt(),
"ServerSignature"
"ServerSignature".toByteArray().toByteString()
)
traceLocationRepository.addTraceLocation(traceLocation)
......
......@@ -5,7 +5,7 @@ import android.view.View
import androidx.fragment.app.Fragment
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.FragmentTestShowstoredeventsBinding
import de.rki.coronawarnapp.eventregistration.events.TraceLocation
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
import de.rki.coronawarnapp.util.di.AutoInject
import de.rki.coronawarnapp.util.ui.observe2
import de.rki.coronawarnapp.util.ui.viewBindingLazy
......
......@@ -2,14 +2,12 @@ package de.rki.coronawarnapp.eventregistration
import dagger.Binds
import dagger.Module
import de.rki.coronawarnapp.eventregistration.checkins.CheckInsTransformer
import de.rki.coronawarnapp.eventregistration.checkins.DefaultCheckInsTransformer
import de.rki.coronawarnapp.eventregistration.checkins.download.DownloadedCheckInsRepo
import de.rki.coronawarnapp.eventregistration.checkins.download.FakeDownloadedCheckInsRepo
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.DefaultQRCodeVerifier
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier
import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository
import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository
import de.rki.coronawarnapp.eventregistration.checkins.CheckInsTransformer
import de.rki.coronawarnapp.eventregistration.checkins.DefaultCheckInsTransformer
@Module
abstract class EventRegistrationModule {
......@@ -17,9 +15,6 @@ abstract class EventRegistrationModule {
@Binds
abstract fun checkInsTransformer(transformer: DefaultCheckInsTransformer): CheckInsTransformer
@Binds
abstract fun qrCodeVerifier(qrCodeVerifier: DefaultQRCodeVerifier): QRCodeVerifier
@Binds
abstract fun traceLocationRepository(defaultTraceLocationRepo: DefaultTraceLocationRepository):
TraceLocationRepository
......
......@@ -22,7 +22,7 @@ class DefaultCheckInsTransformer @Inject constructor() :
.build()
val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.newBuilder()
.setLocation(traceLocation)
.setLocation(traceLocation.toByteString())
.setSignature(ByteString.copyFrom(checkIn.signature.toByteArray()))
.build()
......
package de.rki.coronawarnapp.eventregistration.checkins.qrcode
import dagger.Reusable
import de.rki.coronawarnapp.eventregistration.common.decodeBase32
import de.rki.coronawarnapp.util.decodeBase32
import okio.ByteString
import timber.log.Timber
import java.net.URI
......
package de.rki.coronawarnapp.eventregistration.checkins.qrcode
interface QRCodeVerifier {
suspend fun verify(rawTraceLocation: ByteArray): QRCodeVerifyResult
}
package de.rki.coronawarnapp.eventregistration.events
package de.rki.coronawarnapp.eventregistration.checkins.qrcode
import android.os.Parcelable
import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity
import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
import kotlinx.parcelize.Parcelize
import okio.ByteString
import okio.ByteString.Companion.decodeBase64
import org.joda.time.Instant
const val TRACE_LOCATION_VERSION = 1
data class DefaultTraceLocation(
override val guid: String,
override val type: TraceLocationOuterClass.TraceLocationType,
override val description: String,
override val address: String,
override val startDate: Instant?,
override val endDate: Instant?,
override val defaultCheckInLengthInMinutes: Int?,
override val signature: String,
override val version: Int = TRACE_LOCATION_VERSION,
) : TraceLocation
@Parcelize
data class TraceLocation(
val guid: String,
val type: TraceLocationOuterClass.TraceLocationType,
val description: String,
val address: String,
val startDate: Instant?,
val endDate: Instant?,
val defaultCheckInLengthInMinutes: Int?,
val signature: ByteString,
val version: Int = TRACE_LOCATION_VERSION,
) : Parcelable
fun List<TraceLocationEntity>.toTraceLocations() = this.map { it.toTraceLocation() }
fun TraceLocationEntity.toTraceLocation() = DefaultTraceLocation(
fun TraceLocationEntity.toTraceLocation() = TraceLocation(
guid = guid,
type = type,
description = description,
......@@ -28,19 +33,6 @@ fun TraceLocationEntity.toTraceLocation() = DefaultTraceLocation(
startDate = startDate,
endDate = endDate,
defaultCheckInLengthInMinutes = defaultCheckInLengthInMinutes,
signature = signature,
signature = signatureBase64.decodeBase64()!!,
version = version
)
/*fun SignedEventOuterClass.SignedEvent.toHostedEvent(): TraceLocation =
DefaultTraceLocation(
guid = event.guid.toString(),
type = enumValues<TraceLocation.Type>()[type],
description = event.description,
address = "hardcodedLocation", // event.location,
// backend needs UNIX timestamp in seconds, so we have to multiply it by 1000 to get milliseconds
startDate = Instant.ofEpochMilli(event.start.toLong() * 1000),
endDate = Instant.ofEpochMilli(event.end.toLong() * 1000),
defaultCheckInLengthInMinutes = event.defaultCheckInLengthInMinutes,
signature = signature.toString()
)*/
......@@ -2,28 +2,27 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode
import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
import de.rki.coronawarnapp.util.security.SignatureValidation
import okio.ByteString.Companion.decodeBase64
import timber.log.Timber
import javax.inject.Inject
class DefaultQRCodeVerifier @Inject constructor(
class TraceLocationQRCodeVerifier @Inject constructor(
private val signatureValidation: SignatureValidation
) : QRCodeVerifier {
) {
override suspend fun verify(rawTraceLocation: ByteArray): QRCodeVerifyResult {
Timber.tag(TAG).v("Verifying: %s", rawTraceLocation)
fun verify(rawTraceLocation: ByteArray): TraceLocationVerifyResult {
Timber.v("Verifying: %s", rawTraceLocation)
val signedTraceLocation = try {
TraceLocationOuterClass.SignedTraceLocation.parseFrom(rawTraceLocation)
} catch (e: Exception) {
throw InvalidQRCodeDataException(cause = e, message = "QR-code data could not be parsed.")
}
Timber.tag(TAG).d("Parsed to signed location: %s", signedTraceLocation)
Timber.d("Parsed to signed location: %s", signedTraceLocation)
val isValid = try {
signatureValidation.hasValidSignature(
signedTraceLocation.location.toByteArray(),
sequenceOf(signedTraceLocation.signature.toStringUtf8().decodeBase64()!!.toByteArray())
sequenceOf(signedTraceLocation.signature.toByteArray())
)
} catch (e: Exception) {
throw InvalidQRCodeDataException(cause = e, message = "Verification failed.")
......@@ -33,10 +32,13 @@ class DefaultQRCodeVerifier @Inject constructor(
throw InvalidQRCodeSignatureException(message = "QR-code did not match signature.")
}
return QRCodeVerifyResult(signedTraceLocation)
}
val traceLocation = TraceLocationOuterClass.TraceLocation.parseFrom(
signedTraceLocation.location
)
companion object {
private const val TAG = "DefaultQRCodeVerifier"
return TraceLocationVerifyResult(
signedTraceLocation = signedTraceLocation,
traceLocation = traceLocation
)
}
}
......@@ -2,18 +2,39 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode
import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds
import okio.ByteString.Companion.toByteString
import org.joda.time.Instant
import java.util.concurrent.TimeUnit
data class QRCodeVerifyResult(
val singedTraceLocation: TraceLocationOuterClass.SignedTraceLocation
data class TraceLocationVerifyResult(
val signedTraceLocation: TraceLocationOuterClass.SignedTraceLocation,
val traceLocation: TraceLocationOuterClass.TraceLocation
) {
fun isBeforeStartTime(now: Instant): Boolean {
val startTimestamp = singedTraceLocation.location.startTimestamp
val startTimestamp = traceLocation.startTimestamp
return startTimestamp != 0L && startTimestamp > now.seconds
}
fun isAfterEndTime(now: Instant): Boolean {
val endTimestamp = singedTraceLocation.location.endTimestamp
val endTimestamp = traceLocation.endTimestamp
return endTimestamp != 0L && endTimestamp < now.seconds
}
val verifiedTraceLocation: TraceLocation = TraceLocation(
guid = traceLocation.guid,
version = traceLocation.version,
type = traceLocation.type,
description = traceLocation.description,
address = traceLocation.address,
startDate = traceLocation.startTimestamp.toInstant(),
endDate = traceLocation.endTimestamp.toInstant(),
defaultCheckInLengthInMinutes = traceLocation.defaultCheckInLengthInMinutes,
signature = signedTraceLocation.signature.toByteArray().toByteString()
)
/**
* Converts time in seconds into [Instant]
*/
private fun Long.toInstant() =
if (this == 0L) null else Instant.ofEpochMilli(TimeUnit.SECONDS.toMillis(this))
}
package de.rki.coronawarnapp.eventregistration.events
import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
import org.joda.time.Instant
interface TraceLocation {
val guid: String
val version: Int
val type: TraceLocationOuterClass.TraceLocationType
val description: String
val address: String
val startDate: Instant?
val endDate: Instant?
val defaultCheckInLengthInMinutes: Int?
val signature: String
}
......@@ -3,7 +3,7 @@ package de.rki.coronawarnapp.eventregistration.storage.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import de.rki.coronawarnapp.eventregistration.events.TraceLocation
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
import org.joda.time.Instant
......@@ -18,7 +18,7 @@ data class TraceLocationEntity(
@ColumnInfo(name = "startDate") val startDate: Instant?,
@ColumnInfo(name = "endDate") val endDate: Instant?,
@ColumnInfo(name = "defaultCheckInLengthInMinutes") val defaultCheckInLengthInMinutes: Int?,
@ColumnInfo(name = "signature") val signature: String
@ColumnInfo(name = "signature") val signatureBase64: String
)
......@@ -31,6 +31,6 @@ fun TraceLocation.toTraceLocationEntity(): TraceLocationEntity =
startDate = startDate,
endDate = endDate,
defaultCheckInLengthInMinutes = defaultCheckInLengthInMinutes,
signature = signature,
signatureBase64 = signature.base64(),
version = version
)
package de.rki.coronawarnapp.eventregistration.storage.repo
import de.rki.coronawarnapp.eventregistration.events.TraceLocation
import de.rki.coronawarnapp.eventregistration.events.toTraceLocations
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.toTraceLocations
import de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase
import de.rki.coronawarnapp.eventregistration.storage.dao.TraceLocationDao
import de.rki.coronawarnapp.eventregistration.storage.entity.toTraceLocationEntity
......
package de.rki.coronawarnapp.eventregistration.storage.repo
import de.rki.coronawarnapp.eventregistration.events.TraceLocation
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
import kotlinx.coroutines.flow.Flow
interface TraceLocationRepository {
......
......@@ -65,7 +65,7 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
viewModel.verifyResult.observe2(this) {
doNavigate(
CheckInsFragmentDirections
.actionCheckInsFragmentToConfirmCheckInFragment(it.toVerifiedTraceLocation())
.actionCheckInsFragmentToConfirmCheckInFragment(it.verifiedTraceLocation)
)
}
}
......@@ -92,7 +92,8 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
@Suppress("MaxLineLength")
private val DEBUG_CHECKINS = listOf(
"https://e.coronawarn.app/c1/BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVGRSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAFAAAESIGBDAEIIARVENF6QT6XZATJ5GSDHL77BCAGR6QKDEUJRP2RDCTKTS7QECWMFAEIIA47MT2EA7MQKGNQU2XCY3Y2ZOZXCILDPC65PBUO4JJHT5LQQWDQSA"
"https://e.coronawarn.app/c1/BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVGRSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAFAAAESGGBCAEIDFJJ7KHRO3ZZ2SFMJSBXSUY2ZZKGOIZS27L2D6VPKTA57M6RZY3MBCARR7LXAA2BY3IGNTHNFFAJSMIXF6PP4TEB3I2C3D7P32QUZHVVER",
"https://e.coronawarn.app/c1/BJHAUJDGMNQTQNDCGM3S2NRRMMYC2NDBG5RS2YRSMY4C2OBSGVRWCZDEGUYDMY3GCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDDARACEA2ZCTGOF2HH2RQU7ODZMCSUTUBBNQYM6AR4NG6FFLC6ISXWEOI5UARADO44YYH3U53ZYL6IYM5DWALXUESAJNWRGRL5KLNLS5BM54SHDDCA",
)
}
}
......@@ -7,8 +7,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifyResult
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationQRCodeVerifier
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationVerifyResult
import de.rki.coronawarnapp.exception.ExceptionCategory
import de.rki.coronawarnapp.exception.reporting.report
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
......@@ -20,12 +20,12 @@ class CheckInsViewModel @AssistedInject constructor(
@Assisted private val savedState: SavedStateHandle,
@Assisted private val deepLink: String?,
dispatcherProvider: DispatcherProvider,
private val qrCodeVerifier: QRCodeVerifier,
private val traceLocationQRCodeVerifier: TraceLocationQRCodeVerifier,
private val qrCodeUriParser: QRCodeUriParser
) : CWAViewModel(dispatcherProvider) {
private val verifyResultData = MutableLiveData<QRCodeVerifyResult>()
val verifyResult: LiveData<QRCodeVerifyResult> = verifyResultData
private val verifyResultData = MutableLiveData<TraceLocationVerifyResult>()
val verifyResult: LiveData<TraceLocationVerifyResult> = verifyResultData
init {
deepLink?.let {
......@@ -45,7 +45,7 @@ class CheckInsViewModel @AssistedInject constructor(
val signedTraceLocation = qrCodeUriParser.getSignedTraceLocation(uri)
?: throw IllegalArgumentException("Invalid uri: $uri")
val verifyResult = qrCodeVerifier.verify(signedTraceLocation.toByteArray())
val verifyResult = traceLocationQRCodeVerifier.verify(signedTraceLocation.toByteArray())
Timber.i("verifyResult: $verifyResult")
verifyResultData.postValue(verifyResult)
} catch (e: Exception) {
......
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkins
import android.os.Parcelable
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifyResult
import kotlinx.parcelize.Parcelize
import okio.ByteString.Companion.toByteString
import org.joda.time.Instant
import java.util.concurrent.TimeUnit
@Parcelize
data class VerifiedTraceLocation(
val guid: String,
val description: String?,
val start: Instant?,
val end: Instant?,
val defaultCheckInLengthInMinutes: Int,
// TODO add required properties to confirm check-in
) : Parcelable
fun QRCodeVerifyResult.toVerifiedTraceLocation() =
with(singedTraceLocation.location) {
VerifiedTraceLocation(
guid = guid.toByteArray().toByteString().base64(),
start = startTimestamp.toInstant(),
end = endTimestamp.toInstant(),
description = description,
defaultCheckInLengthInMinutes = defaultCheckInLengthInMinutes
)
}
/**
* Converts time in seconds into [Instant]
*/
private fun Long.toInstant() =
if (this == 0L) null else Instant.ofEpochMilli(TimeUnit.SECONDS.toMillis(this))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment