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

Srs plausible deniability (EXPOSUREAPP-14314) #5714

Merged
merged 24 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5c0d5ef
Rename
mtwalli Nov 15, 2022
3e4b2bc
Fake OTP auth
mtwalli Nov 15, 2022
13116fa
Pass paddingTool
mtwalli Nov 15, 2022
cb8e4f6
Merge branch 'release/3.0.x' into feature/14314-plausible-deniability
mtwalli Nov 15, 2022
acd1ab5
Log
mtwalli Nov 15, 2022
a7963ba
Cover fake requests
mtwalli Nov 15, 2022
534510f
Cover fake requests
mtwalli Nov 15, 2022
41fb774
Remove comment
mtwalli Nov 15, 2022
a63bfda
Update SrsAuthorizationServerTest.kt
mtwalli Nov 15, 2022
23d86f5
Analytics fake key submission
mtwalli Nov 15, 2022
c452faf
lint
mtwalli Nov 16, 2022
c5fdd7a
Update AnalyticsTest.kt
mtwalli Nov 16, 2022
547c812
Merge branch 'release/3.0.x' into feature/14314-plausible-deniability
mtwalli Nov 16, 2022
eec9b19
Padding
mtwalli Nov 16, 2022
9b08b4b
Merge branch 'feature/14314-plausible-deniability' of https://github.…
mtwalli Nov 16, 2022
211ca22
Lint
mtwalli Nov 16, 2022
8c36ad7
lint
mtwalli Nov 16, 2022
4f6dbdd
Merge branch 'release/3.0.x' into feature/14314-plausible-deniability
mtwalli Nov 16, 2022
5a53683
Merge branch 'release/3.0.x' into feature/14314-plausible-deniability
mtwalli Nov 16, 2022
7e1192b
Maps plausibleDeniabilityParameters
mtwalli Nov 16, 2022
8e32d71
Log
mtwalli Nov 16, 2022
c608265
Merge branch 'release/3.0.x' into feature/14314-plausible-deniability
mtwalli Nov 16, 2022
7e331b6
Merge branch 'release/3.0.x' into feature/14314-plausible-deniability
mtwalli Nov 16, 2022
e978b9b
Merge branch 'release/3.0.x' into feature/14314-plausible-deniability
mtwalli Nov 16, 2022
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
Expand Up @@ -10,6 +10,11 @@ interface AnalyticsConfig {
val hoursSinceTestResultToSubmitKeySubmissionMetadata: Int
val probabilityToSubmitNewExposureWindows: Double
val analyticsEnabled: Boolean
val plausibleDeniabilityParameters: PlausibleDeniabilityParameters

interface PlausibleDeniabilityParameters {
val probabilityOfFakeKeySubmission: Double
}

interface Mapper : ConfigMapper<AnalyticsConfig>
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.rki.coronawarnapp.appconfig

import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass
.PresenceTracingPlausibleDeniabilityParameters.NumberOfFakeCheckInsFunctionParametersOrBuilder
import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingPlausibleDeniabilityParameters.NumberOfFakeCheckInsFunctionParametersOrBuilder

data class PlausibleDeniabilityParametersContainer(
val checkInSizesInBytes: List<Int> = emptyList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ interface SelfReportSubmissionConfig {
interface SelfReportSubmissionCommon {
val timeSinceOnboardingInHours: Duration
val timeBetweenSubmissionsInDays: Duration
val plausibleDeniabilityParameters: SrsPlausibleDeniabilityParameters
}

data class SrsPlausibleDeniabilityParameters(
val minRequestPaddingBytes: Int = 0,
val maxRequestPaddingBytes: Int = 0
)

data class SelfReportSubmissionCommonContainer(
override val timeSinceOnboardingInHours: Duration = DEFAULT_HOURS,
override val timeBetweenSubmissionsInDays: Duration = DEFAULT_DAYS
override val timeBetweenSubmissionsInDays: Duration = DEFAULT_DAYS,
override val plausibleDeniabilityParameters: SrsPlausibleDeniabilityParameters = SrsPlausibleDeniabilityParameters()
) : SelfReportSubmissionCommon {
companion object {
val DEFAULT_HOURS: Duration = Duration.ofHours(24)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import dagger.Reusable
import de.rki.coronawarnapp.appconfig.AnalyticsConfig
import de.rki.coronawarnapp.appconfig.SafetyNetRequirements
import de.rki.coronawarnapp.appconfig.SafetyNetRequirementsContainer
import de.rki.coronawarnapp.appconfig.mapping.AnalyticsConfigMapper.PlausibleDeniabilityParametersContainer
import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
import de.rki.coronawarnapp.server.protocols.internal.v2.PpddPpaParameters.PPDDPrivacyPreservingAnalyticsParametersCommon
import timber.log.Timber
import javax.inject.Inject

Expand All @@ -22,7 +24,8 @@ class AnalyticsConfigMapper @Inject constructor() : AnalyticsConfig.Mapper {
hoursSinceTestRegistrationToSubmitTestResultMetadata = 0,
hoursSinceTestResultToSubmitKeySubmissionMetadata = 0,
probabilityToSubmitNewExposureWindows = 0.0,
analyticsEnabled = false
analyticsEnabled = false,
plausibleDeniabilityParameters = PlausibleDeniabilityParametersContainer()
)
}

Expand Down Expand Up @@ -55,7 +58,8 @@ class AnalyticsConfigMapper @Inject constructor() : AnalyticsConfig.Mapper {
.hoursSinceTestRegistrationToSubmitTestResultMetadata,
probabilityToSubmitNewExposureWindows = it
.probabilityToSubmitExposureWindows,
analyticsEnabled = true
analyticsEnabled = true,
plausibleDeniabilityParameters = it.mapPlausibleDeniabilityParameters()
)
}

Expand All @@ -65,6 +69,19 @@ class AnalyticsConfigMapper @Inject constructor() : AnalyticsConfig.Mapper {
override val hoursSinceTestRegistrationToSubmitTestResultMetadata: Int,
override val hoursSinceTestResultToSubmitKeySubmissionMetadata: Int,
override val probabilityToSubmitNewExposureWindows: Double,
override val analyticsEnabled: Boolean
override val analyticsEnabled: Boolean,
override val plausibleDeniabilityParameters: AnalyticsConfig.PlausibleDeniabilityParameters,
) : AnalyticsConfig

data class PlausibleDeniabilityParametersContainer(
override val probabilityOfFakeKeySubmission: Double = 0.0
) : AnalyticsConfig.PlausibleDeniabilityParameters
}

private fun PPDDPrivacyPreservingAnalyticsParametersCommon.mapPlausibleDeniabilityParameters() = when {
hasPlausibleDeniabilityParameters() -> PlausibleDeniabilityParametersContainer(
probabilityOfFakeKeySubmission = plausibleDeniabilityParameters.probabilityOfFakeKeySubmission
)

else -> PlausibleDeniabilityParametersContainer()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import de.rki.coronawarnapp.appconfig.SelfReportSubmissionCommonContainer

import de.rki.coronawarnapp.appconfig.SelfReportSubmissionConfig
import de.rki.coronawarnapp.appconfig.SelfReportSubmissionConfigContainer
import de.rki.coronawarnapp.appconfig.SrsPlausibleDeniabilityParameters
import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
import de.rki.coronawarnapp.server.protocols.internal.v2.PpddSrsParameters
import timber.log.Timber
Expand All @@ -20,6 +21,7 @@ class SelfReportSubmissionConfigMapper @Inject constructor() : SelfReportSubmiss
Timber.d("No SelfReportParameters -> set to default")
SelfReportSubmissionConfigContainer.DEFAULT
}

else -> rawConfig.selfReportParameters.map()
}
} catch (e: Exception) {
Expand All @@ -41,6 +43,16 @@ class SelfReportSubmissionConfigMapper @Inject constructor() : SelfReportSubmiss
SelfReportSubmissionCommonContainer.DEFAULT_DAYS
} else {
Duration.ofHours(common.timeBetweenSubmissionsInDays.toLong())
},

plausibleDeniabilityParameters = if (common.hasPlausibleDeniabilityParameters()) {
SrsPlausibleDeniabilityParameters(
minRequestPaddingBytes = common.plausibleDeniabilityParameters.minRequestPaddingBytes,
maxRequestPaddingBytes = common.plausibleDeniabilityParameters.maxRequestPaddingBytes
)
} else {
Timber.d("No plausibleDeniabilityParameters -> set to default")
SrsPlausibleDeniabilityParameters()
}
)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation
import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException
import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException.Type.ATTESTATION_REQUEST_FAILED
import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException.Type.INTERNAL_ERROR
import de.rki.coronawarnapp.playbook.Playbook
import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData
import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaDataRequestAndroid
import de.rki.coronawarnapp.storage.OnboardingSettings
import de.rki.coronawarnapp.util.TimeStamper
import de.rki.coronawarnapp.util.reset.Resettable
import de.rki.coronawarnapp.util.security.RandomStrong
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
Expand All @@ -31,6 +33,7 @@ import javax.inject.Singleton
import kotlin.random.Random

@Singleton
@Suppress("LongParameterList")
class Analytics @Inject constructor(
private val dataDonationAnalyticsServer: DataDonationAnalyticsServer,
private val appConfigProvider: AppConfigProvider,
Expand All @@ -40,7 +43,9 @@ class Analytics @Inject constructor(
private val settings: AnalyticsSettings,
private val logger: LastAnalyticsSubmissionLogger,
private val timeStamper: TimeStamper,
private val onboardingSettings: OnboardingSettings
private val onboardingSettings: OnboardingSettings,
@RandomStrong private val randomSource: Random,
private val playbook: Playbook,
) : Resettable {
private val submissionLockoutMutex = Mutex()

Expand Down Expand Up @@ -123,9 +128,11 @@ class Analytics @Inject constructor(
val result = try {
// 6min, if attestation and/or submission takes longer than that,
// then we want to give modules still time to cleanup and get into a consistent state.
withTimeout(360_000) {
val analytics = withTimeout(360_000) {
trySubmission(configData.analytics, analyticsProto)
}
tryFakeKeySubmission(configData)
analytics
} catch (e: TimeoutCancellationException) {
Timber.tag(TAG).e(e, "trySubmission() timed out after 360s.")
Result(successful = false, shouldRetry = true)
Expand Down Expand Up @@ -153,6 +160,25 @@ class Analytics @Inject constructor(
return result
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
suspend fun tryFakeKeySubmission(configData: ConfigData) = runCatching {
val probability = configData
.analytics
.plausibleDeniabilityParameters
.probabilityOfFakeKeySubmission

val randomDouble = randomSource.nextDouble()
Timber.tag(TAG).d("randomDouble=%s, probability=%s", randomDouble, probability)
if (randomDouble <= probability) {
Timber.tag(TAG).d("Send fake key submission")
playbook.submitFake()
} else {
Timber.tag(TAG).d("Skip fake key submission")
}
}.onFailure {
Timber.tag(TAG).d("tryFakeKeySubmission -> ${it.localizedMessage}")
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun stopDueToNoAnalyticsConfig(analyticsConfig: AnalyticsConfig): Boolean {
return !analyticsConfig.analyticsEnabled
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.rki.coronawarnapp.srs.core.model

data class SrsAuthorizationFakeRequest(
val safetyNetJws: String,
val salt: String,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.rki.coronawarnapp.srs.core.playbook

import de.rki.coronawarnapp.srs.core.model.SrsAuthorizationFakeRequest
import de.rki.coronawarnapp.srs.core.model.SrsAuthorizationRequest
import de.rki.coronawarnapp.srs.core.model.SrsSubmissionPayload
import de.rki.coronawarnapp.srs.core.server.SrsAuthorizationServer
Expand All @@ -18,6 +19,10 @@ class SrsPlaybook @Inject constructor(
return srsAuthorizationServer.authorize(request)
}

suspend fun fakeAuthorize(request: SrsAuthorizationFakeRequest) {
srsAuthorizationServer.fakeAuthorize(request)
}

suspend fun submit(payLoad: SrsSubmissionPayload) {
srsSubmissionServer.submit(payLoad)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import de.rki.coronawarnapp.server.protocols.internal.ppdd.SrsOtp.SRSOneTimePass
import de.rki.coronawarnapp.srs.core.AndroidIdProvider
import de.rki.coronawarnapp.srs.core.SubmissionReporter
import de.rki.coronawarnapp.srs.core.error.SrsSubmissionException
import de.rki.coronawarnapp.srs.core.model.SrsAuthorizationFakeRequest
import de.rki.coronawarnapp.srs.core.model.SrsAuthorizationRequest
import de.rki.coronawarnapp.srs.core.model.SrsDeviceAttestationRequest
import de.rki.coronawarnapp.srs.core.model.SrsOtp
Expand Down Expand Up @@ -58,7 +59,16 @@ class SrsSubmissionRepository @Inject constructor(
val nowUtc = timeStamper.nowUTC
var srsOtp = currentOtp(nowUtc).also { OtpCensor.otp = it }
val attestResult = attest(appConfig, srsOtp, srsDevSettings.checkLocalPrerequisites())
if (!srsOtp.isValid(nowUtc)) {

if (srsOtp.isValid(nowUtc)) {
Timber.d("Otp is still valid -> fakePlaybookAuthorization")
playbook.fakeAuthorize(
SrsAuthorizationFakeRequest(
safetyNetJws = attestResult.report.jwsResult,
salt = String(attestResult.ourSalt),
)
)
} else {
Timber.d("Authorize new srsOtp=%s", srsOtp)
val expiresAt = playbook.authorize(
SrsAuthorizationRequest(
Expand All @@ -68,7 +78,6 @@ class SrsSubmissionRepository @Inject constructor(
androidId = androidIdProvider.getAndroidId()
)
)

srsOtp = srsOtp.copy(expiresAt = expiresAt)
srsSubmissionSettings.setOtp(srsOtp)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.srs.core.server

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.google.protobuf.ByteString
import javax.inject.Inject
import dagger.Lazy
import dagger.Reusable
Expand All @@ -13,14 +14,18 @@ import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid
import de.rki.coronawarnapp.server.protocols.internal.ppdd.SrsOtpRequestAndroid.SRSOneTimePasswordRequestAndroid
import de.rki.coronawarnapp.srs.core.error.SrsSubmissionException
import de.rki.coronawarnapp.srs.core.error.SrsSubmissionException.ErrorCode
import de.rki.coronawarnapp.srs.core.model.SrsAuthorizationFakeRequest
import de.rki.coronawarnapp.srs.core.model.SrsAuthorizationRequest
import de.rki.coronawarnapp.srs.core.model.SrsAuthorizationResponse
import de.rki.coronawarnapp.srs.core.storage.SrsDevSettings
import de.rki.coronawarnapp.tag
import de.rki.coronawarnapp.util.PaddingTool
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.serialization.BaseJackson
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import okhttp3.ResponseBody
import retrofit2.Response
import timber.log.Timber
import java.time.Instant
import java.time.OffsetDateTime
Expand All @@ -32,13 +37,14 @@ class SrsAuthorizationServer @Inject constructor(
private val dispatcherProvider: DispatcherProvider,
private val srsDevSettings: SrsDevSettings,
private val appConfigProvider: AppConfigProvider,
private val paddingTool: PaddingTool,
) {
private val api = srsAuthorizationApi.get()

suspend fun authorize(request: SrsAuthorizationRequest): Instant =
withContext(dispatcherProvider.IO) {
try {
authoriseRequest(request)
authorizeRequest(request)
} catch (e: Exception) {
throw when (e) {
is SrsSubmissionException -> e
Expand All @@ -51,7 +57,34 @@ class SrsAuthorizationServer @Inject constructor(
}
}

private suspend fun authoriseRequest(request: SrsAuthorizationRequest): Instant {
suspend fun fakeAuthorize(request: SrsAuthorizationFakeRequest): Result<Response<ResponseBody>> = runCatching {
Timber.tag(TAG).d("fakeAuthorize()")
val selfReportSubmission = appConfigProvider.currentConfig.first().selfReportSubmission
val min = selfReportSubmission.common.plausibleDeniabilityParameters.minRequestPaddingBytes
val max = selfReportSubmission.common.plausibleDeniabilityParameters.maxRequestPaddingBytes
val authPadding = ByteString.copyFrom(paddingTool.srsAuthPadding(min, max))

Timber.tag(TAG).d("authPadding=%s, min=%s, max=%s", authPadding, min, max)
val srsOtpRequest = SRSOneTimePasswordRequestAndroid.newBuilder()
.setAuthentication(
PpacAndroid.PPACAndroid.newBuilder()
.setSafetyNetJws(request.safetyNetJws)
.setSalt(request.salt)
.build()
)
.setRequestPadding(authPadding)
.build()

val headers = mapOf(
"Content-Type" to "application/x-protobuf",
"cwa-fake" to "1",
)
api.authenticate(headers, srsOtpRequest)
}.onFailure {
Timber.tag(TAG).d("fakeAuthorize() failed -> %s", it.localizedMessage)
}

private suspend fun authorizeRequest(request: SrsAuthorizationRequest): Instant {
Timber.tag(TAG).d("authorize(request=%s)", request)
val srsOtpRequest = SRSOneTimePasswordRequestAndroid.newBuilder()
.setPayload(
Expand All @@ -69,7 +102,10 @@ class SrsAuthorizationServer @Inject constructor(
)
.build()

val headers = mutableMapOf("Content-Type" to "application/x-protobuf").apply {
val headers = mutableMapOf(
"Content-Type" to "application/x-protobuf",
"cwa-fake" to "0",
).apply {
if (srsDevSettings.forceAndroidIdAcceptance()) {
Timber.tag(TAG).d("forceAndroidIdAcceptance is enabled")
put("cwa-ppac-android-accept-android-id", "1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class SrsSubmissionServer @Inject constructor(

suspend fun submit(payload: SrsSubmissionPayload) = withContext(dispatcherProvider.IO) {
try {
Timber.tag(TAG).d("submit()")
Timber.tag(TAG).d("submit(payload=%s)", payload)
submitPayload(payload)
} catch (e: Exception) {
throw when (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ class PaddingTool @Inject constructor(
return requestPadding(numberOfBytes)
}

fun srsAuthPadding(min: Int, max: Int): ByteArray {
if (min >= max) return byteArrayOf()
val n = sourceFast.nextInt(min, max)
return sourceFast.nextBytes(n)
}

companion object {
private val PADDING_ITEMS = ('A'..'Z') + ('a'..'z') + ('0'..'9')
private const val MIN_KEY_COUNT_FOR_SUBMISSION = 15 // Increased from 14 to 15 in purpose for CheckIn submission
Expand Down
Loading