diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt index 82434300f4e..efc4455a3af 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt @@ -8,10 +8,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 + @Binds abstract fun qrCodeVerifier(qrCodeVerifier: DefaultQRCodeVerifier): QRCodeVerifier diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt index 46c7905290d..248a3c2df81 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt @@ -39,6 +39,12 @@ class CheckInRepository @Inject constructor( checkInDao.update(checkIn.toEntity()) } } + + fun clear() { + appScope.launch { + checkInDao.deleteAll() + } + } } private fun TraceLocationCheckInEntity.toCheckIn() = CheckIn( 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 new file mode 100644 index 00000000000..23fe7f823ea --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt @@ -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): List +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformer.kt new file mode 100644 index 00000000000..84bd29c8f09 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformer.kt @@ -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): List { + 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() + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt index 58fe31e808a..fffe53cd31a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt @@ -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() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt index e2db6aacade..3da7b683d06 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt @@ -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 /* registration token & test result*/ + ): Pair suspend fun testResult(registrationToken: String): TestResult @@ -26,6 +37,7 @@ interface Playbook { val registrationToken: String, val temporaryExposureKeys: List, val consentToFederation: Boolean, - val visitedCountries: List + val visitedCountries: List, + val checkIns: List ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/server/SubmissionServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/server/SubmissionServer.kt index ab874597196..344b175e137 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/server/SubmissionServer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/server/SubmissionServer.kt @@ -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 @@ -25,7 +26,8 @@ class SubmissionServer @Inject constructor( val authCode: String, val keyList: List, val consentToFederation: Boolean, - val visitedCountries: List + val visitedCountries: List, + val checkIns: List ) suspend fun submitKeysToServer( @@ -48,6 +50,7 @@ class SubmissionServer @Inject constructor( .setRequestPadding(ByteString.copyFromUtf8(fakeKeyPadding)) .setConsentToFederation(data.consentToFederation) .addAllVisitedCountries(data.visitedCountries) + .addAllCheckIns(data.checkIns) .build() api.submitKeys( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt index 8232893d488..db90d36d198 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt @@ -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 @@ -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 { @@ -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() @@ -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) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformerTest.kt new file mode 100644 index 00000000000..7a7058e969a --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformerTest.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt index f201b17a6ca..3793333202b 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt @@ -86,7 +86,8 @@ class DefaultPlaybookTest : BaseTest() { registrationToken = "token", temporaryExposureKeys = listOf(), consentToFederation = true, - visitedCountries = listOf("DE") + visitedCountries = listOf("DE"), + checkIns = emptyList() ) ) @@ -107,7 +108,8 @@ class DefaultPlaybookTest : BaseTest() { registrationToken = "token", temporaryExposureKeys = listOf(), consentToFederation = true, - visitedCountries = listOf("DE") + visitedCountries = listOf("DE"), + checkIns = emptyList() ) ) } catch (e: Exception) { @@ -126,7 +128,8 @@ class DefaultPlaybookTest : BaseTest() { registrationToken = "token", temporaryExposureKeys = listOf(), consentToFederation = true, - visitedCountries = listOf("DE") + visitedCountries = listOf("DE"), + checkIns = emptyList() ) ) } catch (e: Exception) { @@ -146,7 +149,8 @@ class DefaultPlaybookTest : BaseTest() { registrationToken = "token", temporaryExposureKeys = listOf(), consentToFederation = true, - visitedCountries = listOf("DE") + visitedCountries = listOf("DE"), + checkIns = emptyList() ) ) } @@ -260,7 +264,8 @@ class DefaultPlaybookTest : BaseTest() { registrationToken = "token", temporaryExposureKeys = listOf(), consentToFederation = true, - visitedCountries = listOf("DE") + visitedCountries = listOf("DE"), + checkIns = emptyList() ) ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/server/SubmissionServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/server/SubmissionServerTest.kt index 6f2aa826cf9..1bd51734af3 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/server/SubmissionServerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/server/SubmissionServerTest.kt @@ -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) @@ -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) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt index f49adc5f87d..81e9e2f5354 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt @@ -4,6 +4,8 @@ import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData 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 @@ -59,6 +61,8 @@ class SubmissionTaskTest : BaseTest() { @MockK lateinit var appConfigData: ConfigData @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + @MockK lateinit var checkInsTransformer: CheckInsTransformer + @MockK lateinit var checkInRepository: CheckInRepository private lateinit var settingSymptomsPreference: FlowPreference @@ -109,6 +113,10 @@ class SubmissionTaskTest : BaseTest() { every { autoSubmission.updateMode(any()) } just Runs every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(1)) + + every { checkInRepository.allCheckIns } returns flowOf(emptyList()) + every { checkInRepository.clear() } just Runs + every { checkInsTransformer.transform(any()) } returns emptyList() } private fun createTask() = SubmissionTask( @@ -121,7 +129,9 @@ class SubmissionTaskTest : BaseTest() { timeStamper = timeStamper, autoSubmission = autoSubmission, testResultAvailableNotificationService = testResultAvailableNotificationService, - analyticsKeySubmissionCollector = analyticsKeySubmissionCollector + analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, + checkInsRepository = checkInRepository, + checkInsTransformer = checkInsTransformer ) @Test @@ -149,14 +159,17 @@ class SubmissionTaskTest : BaseTest() { settingSymptomsPreference.value tekHistoryCalculations.transformToKeyHistoryInExternalFormat(listOf(tek), userSymptoms) + checkInRepository.allCheckIns + checkInsTransformer.transform(any()) appConfigProvider.getAppConfig() playbook.submit( Playbook.SubmissionData( - "regtoken", - listOf(transformedKey), - true, - listOf("NL") + registrationToken = "regtoken", + temporaryExposureKeys = listOf(transformedKey), + consentToFederation = true, + visitedCountries = listOf("NL"), + checkIns = emptyList() ) ) @@ -164,6 +177,7 @@ class SubmissionTaskTest : BaseTest() { analyticsKeySubmissionCollector.reportSubmittedInBackground() tekHistoryStorage.clear() + checkInRepository.clear() submissionSettings.symptoms settingSymptomsPreference.update(match { it.invoke(mockk()) == null }) @@ -213,15 +227,17 @@ class SubmissionTaskTest : BaseTest() { appConfigProvider.getAppConfig() playbook.submit( Playbook.SubmissionData( - "regtoken", - listOf(transformedKey), - true, - listOf("NL") + registrationToken = "regtoken", + temporaryExposureKeys = listOf(transformedKey), + consentToFederation = true, + visitedCountries = listOf("NL"), + checkIns = emptyList() ) ) } coVerify(exactly = 0) { tekHistoryStorage.clear() + checkInRepository.clear() settingSymptomsPreference.update(any()) shareTestResultNotificationService.cancelSharePositiveTestResultNotification() autoSubmission.updateMode(any()) @@ -250,10 +266,11 @@ class SubmissionTaskTest : BaseTest() { coVerifySequence { playbook.submit( Playbook.SubmissionData( - "regtoken", - listOf(transformedKey), - true, - listOf("DE") + registrationToken = "regtoken", + temporaryExposureKeys = listOf(transformedKey), + consentToFederation = true, + visitedCountries = listOf("DE"), + checkIns = emptyList() ) ) }