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 <[email protected]>

* 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 <[email protected]>

* Linting

Signed-off-by: Kolya Opahle <[email protected]>

* Initial Code for background noise worker

Signed-off-by: Kolya Opahle <[email protected]>

* First implementation of noise background worker

Signed-off-by: Kolya Opahle <[email protected]>

* Linting

Signed-off-by: Kolya Opahle <[email protected]>

* 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 <[email protected]>

* - adjust fake key size
- remove temporary comment

* Moved build work calls to own file to fix linting

Signed-off-by: Kolya Opahle <[email protected]>

* - 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 <[email protected]>
  • Loading branch information
2 people authored and jakobmoellerdev committed Aug 7, 2020
1 parent fa47d31 commit 12257dc
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

0 comments on commit 12257dc

Please sign in to comment.