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

Commit

Permalink
Merge pull request #1110 from corona-warn-app/RC/1.3.0
Browse files Browse the repository at this point in the history
RC/1.3.0 to Master
  • Loading branch information
jakobmoellerdev authored Sep 3, 2020
2 parents 75301e9 + 70279f0 commit fe9128f
Show file tree
Hide file tree
Showing 68 changed files with 1,636 additions and 406 deletions.
6 changes: 4 additions & 2 deletions Corona-Warn-App/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ android {
applicationId 'de.rki.coronawarnapp'
minSdkVersion 23
targetSdkVersion 29
versionCode 36
versionName "1.2.1"
versionCode 37
versionName "1.3.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

Expand Down Expand Up @@ -235,6 +235,7 @@ dependencies {
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.3.4'
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation 'androidx.annotation:annotation:1.1.0'

// QR
implementation('com.journeyapps:zxing-android-embedded:4.1.0') { transitive = false }
Expand All @@ -249,6 +250,7 @@ dependencies {
exclude group: 'com.google.protobuf'
}
testImplementation "io.mockk:mockk:1.10.0"
testImplementation "com.squareup.okhttp3:mockwebserver:4.8.0"
testImplementation 'org.hamcrest:hamcrest-library:2.2'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ class TestRiskLevelCalculation : Fragment() {
tracingViewModel.viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
// Database Reset
AppDatabase.getInstance(requireContext()).clearAllTables()
// Delete Database Instance
// Preference reset
SecurityHelper.resetSharedPrefs()
// Database Reset
AppDatabase.reset(requireContext())
// Export File Reset
FileStorageHelper.getAllFilesInKeyExportDirectory().forEach { it.delete() }

Expand All @@ -124,7 +124,7 @@ class TestRiskLevelCalculation : Fragment() {
}
RiskLevelTransaction.start()
Toast.makeText(
requireContext(), "Resetted, please fetch diagnosis keys from server again",
requireContext(), "Reset done, please fetch diagnosis keys from server again",
Toast.LENGTH_SHORT
).show()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class ErrorReportReceiver(private val activity: Activity) : BroadcastReceiver()
ErrorCodes.REPORTED_EXCEPTION_UNKNOWN_PROBLEM.code
)

message += "($apiStatusCode)"
message += " ($apiStatusCode)"
}

val errorTitle = context.resources.getString(R.string.errors_generic_details_headline)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,32 @@
package de.rki.coronawarnapp.http

import KeyExportFormat
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import de.rki.coronawarnapp.exception.ApplicationConfigurationCorruptException
import de.rki.coronawarnapp.exception.ApplicationConfigurationInvalidException
import de.rki.coronawarnapp.http.requests.RegistrationTokenRequest
import de.rki.coronawarnapp.http.requests.RegistrationRequest
import de.rki.coronawarnapp.http.requests.RegistrationTokenRequest
import de.rki.coronawarnapp.http.requests.TanRequestBody
import de.rki.coronawarnapp.http.service.DistributionService
import de.rki.coronawarnapp.http.service.SubmissionService
import de.rki.coronawarnapp.http.service.VerificationService
import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass.ApplicationConfiguration
import de.rki.coronawarnapp.service.diagnosiskey.DiagnosisKeyConstants
import de.rki.coronawarnapp.service.submission.KeyType
import de.rki.coronawarnapp.service.submission.SubmissionConstants
import de.rki.coronawarnapp.storage.FileStorageHelper
import de.rki.coronawarnapp.util.TimeAndDateExtensions.toServerFormat
import de.rki.coronawarnapp.util.ZipHelper.unzip
import de.rki.coronawarnapp.util.security.SecurityHelper
import de.rki.coronawarnapp.util.security.HashHelper
import de.rki.coronawarnapp.util.security.VerificationKeys
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.util.Date
import java.util.UUID
import kotlin.math.max

class WebRequestBuilder(
private val distributionService: DistributionService,
Expand Down Expand Up @@ -136,17 +139,24 @@ class WebRequestBuilder(

suspend fun asyncGetRegistrationToken(
key: String,
keyType: String
keyType: KeyType
): String = withContext(Dispatchers.IO) {
val keyStr = if (keyType == SubmissionConstants.QR_CODE_KEY_TYPE) {
SecurityHelper.hash256(key)
val keyStr = if (keyType == KeyType.GUID) {
HashHelper.hash256(key)
} else {
key
}

val paddingLength = when (keyType) {
KeyType.GUID -> SubmissionConstants.PADDING_LENGTH_BODY_REGISTRATION_TOKEN_GUID
KeyType.TELETAN -> SubmissionConstants.PADDING_LENGTH_BODY_REGISTRATION_TOKEN_TELETAN
}

verificationService.getRegistrationToken(
SubmissionConstants.REGISTRATION_TOKEN_URL,
"0",
RegistrationTokenRequest(keyType, keyStr)
requestPadding(SubmissionConstants.PADDING_LENGTH_HEADER_REGISTRATION_TOKEN),
RegistrationTokenRequest(keyType.name, keyStr, requestPadding(paddingLength))
).registrationToken
}

Expand All @@ -155,38 +165,91 @@ class WebRequestBuilder(
): Int = withContext(Dispatchers.IO) {
verificationService.getTestResult(
SubmissionConstants.TEST_RESULT_URL,
"0", RegistrationRequest(registrationToken)
"0",
requestPadding(SubmissionConstants.PADDING_LENGTH_HEADER_TEST_RESULT),
RegistrationRequest(
registrationToken,
requestPadding(SubmissionConstants.PADDING_LENGTH_BODY_TEST_RESULT)
)
).testResult
}

suspend fun asyncGetTan(
registrationToken: String
): String = withContext(Dispatchers.IO) {
verificationService.getTAN(
SubmissionConstants.TAN_REQUEST_URL, "0",
SubmissionConstants.TAN_REQUEST_URL,
"0",
requestPadding(SubmissionConstants.PADDING_LENGTH_HEADER_TAN),
TanRequestBody(
registrationToken
registrationToken,
requestPadding(SubmissionConstants.PADDING_LENGTH_BODY_TAN)
)
).tan
}

suspend fun asyncFakeVerification() = withContext(Dispatchers.IO) {
verificationService.getTAN(
SubmissionConstants.TAN_REQUEST_URL,
"1",
requestPadding(SubmissionConstants.PADDING_LENGTH_HEADER_TAN),
TanRequestBody(
registrationToken = SubmissionConstants.DUMMY_REGISTRATION_TOKEN,
requestPadding = requestPadding(SubmissionConstants.PADDING_LENGTH_BODY_TAN_FAKE)
)
)
}

suspend fun asyncSubmitKeysToServer(
authCode: String,
faked: Boolean,
keyList: List<KeyExportFormat.TemporaryExposureKey>
) = withContext(Dispatchers.IO) {
Timber.d("Writing ${keyList.size} Keys to the Submission Payload.")

val randomAdditions = 0 // prepare for random addition of keys
val fakeKeyCount =
max(SubmissionConstants.minKeyCountForSubmission + randomAdditions - keyList.size, 0)
val fakeKeyPadding = requestPadding(SubmissionConstants.fakeKeySize * fakeKeyCount)

val submissionPayload = KeyExportFormat.SubmissionPayload.newBuilder()
.addAllKeys(keyList)
.setPadding(ByteString.copyFromUtf8(fakeKeyPadding))
.build()
var fakeHeader = "0"
if (faked) fakeHeader = Math.random().toInt().toString()
submissionService.submitKeys(
DiagnosisKeyConstants.DIAGNOSIS_KEYS_SUBMISSION_URL,
authCode,
fakeHeader,
"0",
SubmissionConstants.EMPTY_HEADER,
submissionPayload
)
return@withContext
}

suspend fun asyncFakeSubmission() = withContext(Dispatchers.IO) {

val randomAdditions = 0 // prepare for random addition of keys
val fakeKeyCount = SubmissionConstants.minKeyCountForSubmission + randomAdditions

val fakeKeyPadding =
requestPadding(SubmissionConstants.fakeKeySize * fakeKeyCount)

val submissionPayload = KeyExportFormat.SubmissionPayload.newBuilder()
.setPadding(ByteString.copyFromUtf8(fakeKeyPadding))
.build()

submissionService.submitKeys(
DiagnosisKeyConstants.DIAGNOSIS_KEYS_SUBMISSION_URL,
SubmissionConstants.EMPTY_HEADER,
"1",
requestPadding(SubmissionConstants.PADDING_LENGTH_HEADER_SUBMISSION_FAKE),
submissionPayload
)
}

private fun requestPadding(length: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package de.rki.coronawarnapp.http.playbook

import de.rki.coronawarnapp.http.WebRequestBuilder
import de.rki.coronawarnapp.service.submission.SubmissionConstants
import de.rki.coronawarnapp.storage.LocalData
import de.rki.coronawarnapp.worker.BackgroundConstants
import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
import kotlin.random.Random

class BackgroundNoise {
companion object {
@Volatile
private var instance: BackgroundNoise? = null

fun getInstance(): BackgroundNoise {
return instance ?: synchronized(this) {
instance ?: BackgroundNoise().also {
instance = it
}
}
}
}

fun scheduleDummyPattern() {
if (BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK > 0)
BackgroundWorkScheduler.scheduleBackgroundNoisePeriodicWork()
}

suspend fun foregroundScheduleCheck() {
if (LocalData.isAllowedToSubmitDiagnosisKeys() == true) {
val chance = Random.nextFloat() * 100
if (chance < SubmissionConstants.probabilityToExecutePlaybookWhenOpenApp) {
PlaybookImpl(WebRequestBuilder.getInstance())
.dummy()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package de.rki.coronawarnapp.http.playbook

import KeyExportFormat
import de.rki.coronawarnapp.service.submission.KeyType
import de.rki.coronawarnapp.util.formatter.TestResult

/**
* 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.
*/
interface Playbook {

suspend fun initialRegistration(
key: String,
keyType: KeyType
): String /* registration token */

suspend fun testResult(
registrationToken: String
): TestResult

suspend fun submission(
registrationToken: String,
keys: List<KeyExportFormat.TemporaryExposureKey>
)

suspend fun dummy()
}
Loading

0 comments on commit fe9128f

Please sign in to comment.