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

Commit

Permalink
plausible deniability (#946)
Browse files Browse the repository at this point in the history
* Added requestPadding property to all VerficationService requests

* Added fake calls to the WebReqeustBuilder

* 馃毀 initial draft to enable plausible deniability

* Switched from SubmissionConstants to KeyType enum everywhere

* basic playbook implementation with fake and real requests

* Playbook
- ensure request pattern for playbooks is always the same

VerificationService
- apply padding to ensure equal request size (header & body)

SecurityHelper
- extract hash256 to HashHelper. This simplifies tests that use only the hash function (and therefore don麓t need to initialize SecurityHelper and its dependencies)

* Implemented random chance of dummy playbook execution on app open

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Playbook
- ignore exceptions for fake requests

SubmissionService
- add padding header to fake request for same header size

WebRequestBuilder
- include fake keys in upload (馃彈)

* DiagnosisKeyService: removed (low value & difficult to test)

SubmissionService & SubmitDiagnosisKeysTransaction
- inline playbook & backgroundNoise property to prevent issues during testing

DiagnosisKeyConstantsTest, SubmissionServiceTest, SubmitDiagnosisKeysTransactionTest & SubmissionViewModelTest
- adjusted to changes

* Dummy playbook will now be repeated and delayed randomly

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Linting

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Initial Code for background noise worker

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* First implementation of noise background worker

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Linting

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* PlaybookImpl
- ensure that fake requests are executed when real requests fail

SubmissionService & VerificationService
- adjust header name for padding

WebRequestBuilder
- add padding to dummy submission

* BackgroundNoise is now trigger-only

PlaybookImpl
- include follow-up executions after every playbook
- logging

SubmissionViewModel.kt, SubmissionService.kt, SubmitDiagnosisKeysTransaction.kt, MainActivity.kt, BackgroundNoisePeriodicWorker.kt, DiagnosisTestResultRetrievalPeriodicWorker.kt
- propagate context for coroutine

VerificationService
- ensure body size of 1000

* WebRequestBuilder.kt
- adjust fake key generation

PlaybookImplTest.kt
- remove unused server.enqueue

SubmissionService.kt 6 SubmitDiagnosisKeysTransaction.kt
- remove commented out code

* revert temporary changes to SubmissionResultPositiveOtherWarningFragment.kt

* Background job scheduling implemented

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* - adjust fake key size
- remove temporary comment

* Moved build work calls to own file to fix linting

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* - initialize coroutine scope within the playbook, revert passing it from outside
- remove experimental test dependency for coroutines

* - use single endpoint per server for fake requests
- reduce request size from 1000 to 250 for the verification server
- include dummy registration token in fake request to fulfill verification on server side
- prepare for randomized count of submitted keys
- always include headers cwa-authorization & cwa-header-padding for submission server

* - simplify empty header using constant

Co-authored-by: Kolya Opahle <k.opahle@sap.com>
  • Loading branch information
Fabian-K and kolyaopahle committed Aug 7, 2020
1 parent 53e457f commit f98ac2d
Show file tree
Hide file tree
Showing 34 changed files with 1,143 additions and 226 deletions.
1 change: 1 addition & 0 deletions Corona-Warn-App/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,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 @@ -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,36 @@
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.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() {
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,24 @@
package de.rki.coronawarnapp.http.playbook

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

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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package de.rki.coronawarnapp.http.playbook

import KeyExportFormat
import de.rki.coronawarnapp.http.WebRequestBuilder
import de.rki.coronawarnapp.service.submission.KeyType
import de.rki.coronawarnapp.service.submission.SubmissionConstants
import de.rki.coronawarnapp.util.formatter.TestResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.random.Random

class PlaybookImpl(
private val webRequestBuilder: WebRequestBuilder
) : Playbook {

private val uid = UUID.randomUUID().toString()
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)

override suspend fun initialRegistration(key: String, keyType: KeyType): String {
Timber.i("[$uid] New Initial Registration Playbook")

// real registration
val (registrationToken, exception) =
executeCapturingExceptions { webRequestBuilder.asyncGetRegistrationToken(key, keyType) }

// fake verification
ignoreExceptions { webRequestBuilder.asyncFakeVerification() }

// fake submission
ignoreExceptions { webRequestBuilder.asyncFakeSubmission() }

coroutineScope.launch { followUpPlaybooks() }

return registrationToken ?: propagateException(exception)
}

override suspend fun testResult(registrationToken: String): TestResult {
Timber.i("[$uid] New Test Result Playbook")

// real test result
val (testResult, exception) =
executeCapturingExceptions { webRequestBuilder.asyncGetTestResult(registrationToken) }

// fake verification
ignoreExceptions { webRequestBuilder.asyncFakeVerification() }

// fake submission
ignoreExceptions { webRequestBuilder.asyncFakeSubmission() }

coroutineScope.launch { followUpPlaybooks() }

return testResult?.let { TestResult.fromInt(it) }
?: propagateException(exception)
}

override suspend fun submission(
registrationToken: String,
keys: List<KeyExportFormat.TemporaryExposureKey>
) {
Timber.i("[$uid] New Submission Playbook")

// real auth code
val (authCode, exception) = executeCapturingExceptions {
webRequestBuilder.asyncGetTan(
registrationToken
)
}

// fake verification
ignoreExceptions { webRequestBuilder.asyncFakeVerification() }

// real submission
if (authCode != null) {
webRequestBuilder.asyncSubmitKeysToServer(authCode, keys)
coroutineScope.launch { followUpPlaybooks() }
} else {
webRequestBuilder.asyncFakeSubmission()
coroutineScope.launch { followUpPlaybooks() }
propagateException(exception)
}
}

private suspend fun dummy(launchFollowUp: Boolean) {
// fake verification
ignoreExceptions { webRequestBuilder.asyncFakeVerification() }

// fake verification
ignoreExceptions { webRequestBuilder.asyncFakeVerification() }

// fake submission
ignoreExceptions { webRequestBuilder.asyncFakeSubmission() }

if (launchFollowUp)
coroutineScope.launch { followUpPlaybooks() }
}

override suspend fun dummy() = dummy(true)

private suspend fun followUpPlaybooks() {
val runsToExecute = Random.nextInt(
SubmissionConstants.minNumberOfSequentialPlaybooks,
SubmissionConstants.maxNumberOfSequentialPlaybooks
)
Timber.i("[$uid] Follow Up: launching $runsToExecute follow up playbooks")

repeat(runsToExecute) {
val executionDelay = Random.nextInt(
SubmissionConstants.minDelayBetweenSequentialPlaybooks,
SubmissionConstants.maxDelayBetweenSequentialPlaybooks
)
Timber.i("[$uid] Follow Up: (${it + 1}/$runsToExecute) waiting $executionDelay[s]...")
delay(TimeUnit.SECONDS.toMillis(executionDelay.toLong()))

dummy(false)
}
Timber.i("[$uid] Follow Up: finished")
}

private suspend fun ignoreExceptions(body: suspend () -> Unit) {
try {
body.invoke()
} catch (e: Exception) {
Timber.d(e, "Ignoring dummy request exception")
}
}

private suspend fun <T> executeCapturingExceptions(body: suspend () -> T): Pair<T?, Exception?> {
return try {
val result = body.invoke()
result to null
} catch (e: Exception) {
null to e
}
}

private fun propagateException(exception: Exception?): Nothing {
throw exception ?: IllegalStateException()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ import com.google.gson.annotations.SerializedName

data class RegistrationRequest(
@SerializedName("registrationToken")
val registrationToken: String
val registrationToken: String? = null,
@SerializedName("requestPadding")
val requestPadding: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import com.google.gson.annotations.SerializedName

data class RegistrationTokenRequest(
@SerializedName("keyType")
val keyType: String,
val keyType: String? = null,
@SerializedName("key")
val key: String
val key: String? = null,
@SerializedName("requestPadding")
val requestPadding: String? = null
)
Loading

1 comment on commit f98ac2d

@carlolf
Copy link

@carlolf carlolf commented on f98ac2d Aug 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wee! Wenn ich als Interessierter versuche, den Code nachzuvollziehen, dann ist es sehr schwierig zu verstehen, welche Konzepte hinter Code-Artefakten stehen. Bspw: "Playbook". WTF ist das? Es sind 眉berhaupt keine Kommentare im Code vorhanden, die das Konstrukt beschreiben. Das ist einfach schlechtester Stil. (Hier nur zuf盲llig herausgegriffen. Nicht-Kommentierung findet sich erschrecklich h盲ufig).

Please sign in to comment.