Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

Add check-Ins to submission payload (EXPOSUREAPP-5656) #2602

Merged
merged 19 commits into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package de.rki.coronawarnapp.eventregistration.checkins.qrcode

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import de.rki.coronawarnapp.eventregistration.checkins.qrcode.DefaultQRCodeVerif
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 {

@Binds
abstract fun checkInsTransformer(transformer: DefaultCheckInsTransformer): CheckInsTransformer
mtwalli marked this conversation as resolved.
Show resolved Hide resolved

@Binds
abstract fun qrCodeVerifier(qrCodeVerifier: DefaultQRCodeVerifier): QRCodeVerifier

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ class CheckInRepository @Inject constructor(
checkInDao.update(checkIn.toEntity())
}
}

fun clear() {
appScope.launch {
checkInDao.deleteAll()
}
}
}

private fun TraceLocationCheckInEntity.toCheckIn() = CheckIn(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.rki.coronawarnapp.eventregistration.checkins

import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass

interface CheckInsTransformer {
fun transform(checkIns: List<CheckIn>): List<CheckInOuterClass.CheckIn>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package de.rki.coronawarnapp.eventregistration.checkins

import com.google.protobuf.ByteString
import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass
import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds
import javax.inject.Inject

class DefaultCheckInsTransformer @Inject constructor() :
CheckInsTransformer {
override fun transform(checkIns: List<CheckIn>): List<CheckInOuterClass.CheckIn> {
return checkIns.map { checkIn ->
val traceLocation = TraceLocationOuterClass.TraceLocation.newBuilder()
.setGuid(checkIn.guid)
.setVersion(checkIn.version)
.setType(TraceLocationOuterClass.TraceLocationType.forNumber(checkIn.type))
.setDescription(checkIn.description)
.setAddress(checkIn.address)
.setStartTimestamp(checkIn.traceLocationStart?.seconds ?: 0L)
.setEndTimestamp(checkIn.traceLocationEnd?.seconds ?: 0L)
.setDefaultCheckInLengthInMinutes(checkIn.defaultCheckInLengthInMinutes ?: 0)
.build()

val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.newBuilder()
.setLocation(traceLocation)
.setSignature(ByteString.copyFrom(checkIn.signature.toByteArray()))
.build()

CheckInOuterClass.CheckIn.newBuilder()
.setSignedLocation(signedTraceLocation)
.setStartIntervalNumber(checkIn.checkInStart.seconds.toInt())
.setEndIntervalNumber(checkIn.checkInEnd?.seconds?.toInt() ?: 0)
// TODO .setTransmissionRiskLevel()
.build()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ class DefaultPlaybook @Inject constructor(
authCode = authCode,
keyList = data.temporaryExposureKeys,
consentToFederation = data.consentToFederation,
visitedCountries = data.visitedCountries
visitedCountries = data.visitedCountries,
checkIns = data.checkIns
)
submissionServer.submitKeysToServer(serverSubmissionData)
coroutineScope.launch { followUpPlaybooks() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
package de.rki.coronawarnapp.playbook

import de.rki.coronawarnapp.server.protocols.external.exposurenotification.TemporaryExposureKeyExportOuterClass
import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass
import de.rki.coronawarnapp.util.formatter.TestResult
import de.rki.coronawarnapp.verification.server.VerificationKeyType

/**
* The concept of Plausible Deniability aims to hide the existence of a positive test result by always using a defined “playbook pattern” of requests to the Verification Server and CWA Backend so it is impossible for an attacker to identify which communication was done.
* The “playbook pattern” represents a well-defined communication pattern consisting of dummy requests and real requests.
* To hide that a real request was done, the device does multiple of these requests over a longer period of time according to the previously defined communication pattern statistically similar to all apps so it is not possible to infer by observing the traffic if the requests under concern are real or the fake ones.
* The concept of Plausible Deniability aims to hide the existence of a positive test result by always using a defined
* “playbook pattern” of requests to the Verification Server and CWA Backend so it is impossible for an attacker to
* identify which communication was done.
*
* The “playbook pattern” represents a well-defined communication pattern consisting of fake requests and real
* requests.
*
* To hide that a real request was done, the device does multiple of these requests over a longer period of time
* according to the previously defined communication pattern statistically similar to all apps so it is not possible to
* infer by observing the traffic if the requests under concern are real or the fake ones.
*/
interface Playbook {

/**
* @return pair of Registration token [String] & [TestResult]
*/
suspend fun initialRegistration(
key: String,
keyType: VerificationKeyType
): Pair<String, TestResult> /* registration token & test result*/
): Pair<String, TestResult>

suspend fun testResult(registrationToken: String): TestResult

Expand All @@ -26,6 +37,7 @@ interface Playbook {
val registrationToken: String,
val temporaryExposureKeys: List<TemporaryExposureKeyExportOuterClass.TemporaryExposureKey>,
val consentToFederation: Boolean,
val visitedCountries: List<String>
val visitedCountries: List<String>,
val checkIns: List<CheckInOuterClass.CheckIn>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.google.protobuf.ByteString
import dagger.Lazy
import de.rki.coronawarnapp.server.protocols.external.exposurenotification.TemporaryExposureKeyExportOuterClass.TemporaryExposureKey
import de.rki.coronawarnapp.server.protocols.internal.SubmissionPayloadOuterClass.SubmissionPayload
import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass

import de.rki.coronawarnapp.util.PaddingTool.requestPadding
import kotlinx.coroutines.Dispatchers
Expand All @@ -25,7 +26,8 @@ class SubmissionServer @Inject constructor(
val authCode: String,
val keyList: List<TemporaryExposureKey>,
val consentToFederation: Boolean,
val visitedCountries: List<String>
val visitedCountries: List<String>,
val checkIns: List<CheckInOuterClass.CheckIn>
)

suspend fun submitKeysToServer(
Expand All @@ -48,6 +50,7 @@ class SubmissionServer @Inject constructor(
.setRequestPadding(ByteString.copyFromUtf8(fakeKeyPadding))
.setConsentToFederation(data.consentToFederation)
.addAllVisitedCountries(data.visitedCountries)
.addAllCheckIns(data.checkIns)
.build()

api.submitKeys(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package de.rki.coronawarnapp.submission.task
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
import de.rki.coronawarnapp.eventregistration.checkins.CheckInsTransformer
import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException
import de.rki.coronawarnapp.notification.ShareTestResultNotificationService
import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService
Expand Down Expand Up @@ -37,6 +39,8 @@ class SubmissionTask @Inject constructor(
private val timeStamper: TimeStamper,
private val shareTestResultNotificationService: ShareTestResultNotificationService,
private val testResultAvailableNotificationService: TestResultAvailableNotificationService,
private val checkInsRepository: CheckInRepository,
private val checkInsTransformer: CheckInsTransformer,
private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
) : Task<DefaultProgress, SubmissionTask.Result> {

Expand Down Expand Up @@ -137,11 +141,17 @@ class SubmissionTask @Inject constructor(
)
Timber.tag(TAG).d("Transformed keys with symptoms %s from %s to %s", symptoms, keys, transformedKeys)

val checkIns = checkInsRepository.allCheckIns.first()
val transformedCheckIns = checkInsTransformer.transform(checkIns)

Timber.tag(TAG).d("Transformed CheckIns from: %s to: %s", checkIns, transformedCheckIns)

val submissionData = Playbook.SubmissionData(
registrationToken,
transformedKeys,
true,
getSupportedCountries()
registrationToken = registrationToken,
temporaryExposureKeys = transformedKeys,
consentToFederation = true,
visitedCountries = getSupportedCountries(),
checkIns = transformedCheckIns
)

checkCancel()
Expand All @@ -154,6 +164,7 @@ class SubmissionTask @Inject constructor(

Timber.tag(TAG).d("Submission successful, deleting submission data.")
tekHistoryStorage.clear()
checkInsRepository.clear()
submissionSettings.symptoms.update { null }

autoSubmission.updateMode(AutoSubmission.Mode.DISABLED)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package de.rki.coronawarnapp.eventregistration.checkins

import com.google.protobuf.ByteString
import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
import io.kotest.matchers.shouldBe
import org.joda.time.Instant
import org.junit.jupiter.api.Test
import testhelpers.BaseTest

class DefaultCheckInsTransformerTest : BaseTest() {

private val checkInTransformer = DefaultCheckInsTransformer()

@Test
fun `transform check-ins`() {
val checkIn1 = CheckIn(
id = 0,
guid = "3055331c-2306-43f3-9742-6d8fab54e848",
version = 1,
type = 2,
description = "description1",
address = "address1",
traceLocationStart = Instant.ofEpochMilli(2687955 * 1_000L),
traceLocationEnd = Instant.ofEpochMilli(2687991 * 1_000L),
defaultCheckInLengthInMinutes = 10,
signature = "signature1",
checkInStart = Instant.ofEpochMilli(2687955 * 1_000L),
checkInEnd = Instant.ofEpochMilli(2687991 * 1_000L),
targetCheckInEnd = null,
createJournalEntry = true
)

val checkIn2 = CheckIn(
id = 1,
guid = "fca84b37-61c0-4a7c-b2f8-825cadd506cf",
version = 1,
type = 1,
description = "description2",
address = "address2",
traceLocationStart = null,
traceLocationEnd = null,
defaultCheckInLengthInMinutes = 20,
signature = "signature2",
checkInStart = Instant.ofEpochMilli(2687955 * 1_000L),
checkInEnd = null,
targetCheckInEnd = null,
createJournalEntry = false
)

val outCheckIns = checkInTransformer.transform(
listOf(
checkIn1,
checkIn2
)
)
outCheckIns.size shouldBe 2

outCheckIns[0].apply {
signedLocation.apply {
location.apply {
guid shouldBe "3055331c-2306-43f3-9742-6d8fab54e848"
version shouldBe 1
type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER
description shouldBe "description1"
address shouldBe "address1"
startTimestamp shouldBe 2687955
endTimestamp shouldBe 2687991
defaultCheckInLengthInMinutes shouldBe 10
}
signature shouldBe ByteString.copyFrom("signature1".toByteArray())
}
startIntervalNumber shouldBe 2687955
endIntervalNumber shouldBe 2687991
// TODO transmissionRiskLevel shouldBe
}

outCheckIns[1].apply {
signedLocation.apply {
location.apply {
guid shouldBe "fca84b37-61c0-4a7c-b2f8-825cadd506cf"
version shouldBe 1
type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER
description shouldBe "description2"
address shouldBe "address2"
startTimestamp shouldBe 0
endTimestamp shouldBe 0
defaultCheckInLengthInMinutes shouldBe 20
}
signature shouldBe ByteString.copyFrom("signature2".toByteArray())
}
startIntervalNumber shouldBe 2687955
endIntervalNumber shouldBe 0
// TODO transmissionRiskLevel shouldBe
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ class DefaultPlaybookTest : BaseTest() {
registrationToken = "token",
temporaryExposureKeys = listOf(),
consentToFederation = true,
visitedCountries = listOf("DE")
visitedCountries = listOf("DE"),
checkIns = emptyList()
)
)

Expand All @@ -107,7 +108,8 @@ class DefaultPlaybookTest : BaseTest() {
registrationToken = "token",
temporaryExposureKeys = listOf(),
consentToFederation = true,
visitedCountries = listOf("DE")
visitedCountries = listOf("DE"),
checkIns = emptyList()
)
)
} catch (e: Exception) {
Expand All @@ -126,7 +128,8 @@ class DefaultPlaybookTest : BaseTest() {
registrationToken = "token",
temporaryExposureKeys = listOf(),
consentToFederation = true,
visitedCountries = listOf("DE")
visitedCountries = listOf("DE"),
checkIns = emptyList()
)
)
} catch (e: Exception) {
Expand All @@ -146,7 +149,8 @@ class DefaultPlaybookTest : BaseTest() {
registrationToken = "token",
temporaryExposureKeys = listOf(),
consentToFederation = true,
visitedCountries = listOf("DE")
visitedCountries = listOf("DE"),
checkIns = emptyList()
)
)
}
Expand Down Expand Up @@ -260,7 +264,8 @@ class DefaultPlaybookTest : BaseTest() {
registrationToken = "token",
temporaryExposureKeys = listOf(),
consentToFederation = true,
visitedCountries = listOf("DE")
visitedCountries = listOf("DE"),
checkIns = emptyList()
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ class SubmissionServerTest : BaseTest() {
authCode = "testAuthCode",
keyList = listOf(googleKeyList),
consentToFederation = true,
visitedCountries = listOf("DE")
visitedCountries = listOf("DE"),
checkIns = emptyList()
)
server.submitKeysToServer(submissionData)

Expand Down Expand Up @@ -144,7 +145,8 @@ class SubmissionServerTest : BaseTest() {
authCode = "39ec4930-7a1f-4d5d-921f-bfad3b6f1269",
keyList = listOf(googleKeyList),
consentToFederation = true,
visitedCountries = listOf("DE")
visitedCountries = listOf("DE"),
checkIns = emptyList()
)
webServer.enqueue(MockResponse().setBody("{}"))
server.submitKeysToServer(submissionData)
Expand Down
Loading