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

Self Report Submission config param (EXPOSUREAPP-14187) #5681

Merged
merged 4 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -15,6 +15,7 @@ import de.rki.coronawarnapp.appconfig.mapping.ExposureWindowRiskCalculationConfi
import de.rki.coronawarnapp.appconfig.mapping.KeyDownloadParametersMapper
import de.rki.coronawarnapp.appconfig.mapping.LogUploadConfigMapper
import de.rki.coronawarnapp.appconfig.mapping.PresenceTracingConfigMapper
import de.rki.coronawarnapp.appconfig.mapping.SelfReportSubmissionConfigMapper
import de.rki.coronawarnapp.appconfig.mapping.SurveyConfigMapper
import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient
import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl
Expand Down Expand Up @@ -103,6 +104,11 @@ object AppConfigModule {

@Binds
fun covidCertificateConfigMapper(mapper: CovidCertificateConfigMapper): CovidCertificateConfig.Mapper

@Binds
fun selfReportSubmissionConfigMapper(
mapper: SelfReportSubmissionConfigMapper
): SelfReportSubmissionConfig.Mapper
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package de.rki.coronawarnapp.appconfig

import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper
import java.time.Duration

interface SelfReportSubmissionConfig {
val common: SelfReportSubmissionCommon

val ppac: SafetyNetRequirements

interface Mapper : ConfigMapper<SelfReportSubmissionConfig>
}

interface SelfReportSubmissionCommon {
val timeSinceOnboardingInHours: Duration
val timeBetweenSubmissionsInDays: Duration
}

data class SelfReportSubmissionCommonContainer(
override val timeSinceOnboardingInHours: Duration = DEFAULT_HOURS,
override val timeBetweenSubmissionsInDays: Duration = DEFAULT_DAYS
) : SelfReportSubmissionCommon {
companion object {
val DEFAULT_HOURS: Duration = Duration.ofHours(24)
val DEFAULT_DAYS: Duration = Duration.ofDays(90)
}
}

data class SelfReportSubmissionConfigContainer(
override val common: SelfReportSubmissionCommon,
override val ppac: SafetyNetRequirements
) : SelfReportSubmissionConfig {
companion object {
val DEFAULT = SelfReportSubmissionConfigContainer(
common = SelfReportSubmissionCommonContainer(),
ppac = SafetyNetRequirementsContainer()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
import de.rki.coronawarnapp.appconfig.LogUploadConfig
import de.rki.coronawarnapp.appconfig.PresenceTracingConfig
import de.rki.coronawarnapp.appconfig.SelfReportSubmissionConfig
import de.rki.coronawarnapp.appconfig.SurveyConfig

interface ConfigMapping :
Expand All @@ -23,4 +24,5 @@ interface ConfigMapping :
val presenceTracing: PresenceTracingConfig
val coronaTestParameters: CoronaTestConfig
val covidCertificateParameters: CovidCertificateConfig
val selfReportSubmission: SelfReportSubmissionConfig
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
import de.rki.coronawarnapp.appconfig.LogUploadConfig
import de.rki.coronawarnapp.appconfig.PresenceTracingConfig
import de.rki.coronawarnapp.appconfig.SelfReportSubmissionConfig
import de.rki.coronawarnapp.appconfig.SurveyConfig
import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
import timber.log.Timber
Expand All @@ -28,6 +29,7 @@ class ConfigParser @Inject constructor(
private val presenceTracingConfigMapper: PresenceTracingConfig.Mapper,
private val coronaTestConfigMapper: CoronaTestConfig.Mapper,
private val covidCertificateConfigMapper: CovidCertificateConfig.Mapper,
private val selfReportSubmissionConfigMapper: SelfReportSubmissionConfig.Mapper
) {

fun parse(configBytes: ByteArray): ConfigMapping = try {
Expand All @@ -43,6 +45,7 @@ class ConfigParser @Inject constructor(
presenceTracing = presenceTracingConfigMapper.map(it),
coronaTestParameters = coronaTestConfigMapper.map(it),
covidCertificateParameters = covidCertificateConfigMapper.map(it),
selfReportSubmission = selfReportSubmissionConfigMapper.map(it),
)
}
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
import de.rki.coronawarnapp.appconfig.LogUploadConfig
import de.rki.coronawarnapp.appconfig.PresenceTracingConfig
import de.rki.coronawarnapp.appconfig.SelfReportSubmissionConfig
import de.rki.coronawarnapp.appconfig.SurveyConfig

data class DefaultConfigMapping(
Expand All @@ -22,6 +23,7 @@ data class DefaultConfigMapping(
override val presenceTracing: PresenceTracingConfig,
override val coronaTestParameters: CoronaTestConfig,
override val covidCertificateParameters: CovidCertificateConfig,
override val selfReportSubmission: SelfReportSubmissionConfig,
) : ConfigMapping,
CWAConfig by cwaConfig,
KeyDownloadConfig by keyDownloadConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package de.rki.coronawarnapp.appconfig.mapping

import dagger.Reusable
import de.rki.coronawarnapp.appconfig.SafetyNetRequirementsContainer
import de.rki.coronawarnapp.appconfig.SelfReportSubmissionCommonContainer

import de.rki.coronawarnapp.appconfig.SelfReportSubmissionConfig
import de.rki.coronawarnapp.appconfig.SelfReportSubmissionConfigContainer
import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
import de.rki.coronawarnapp.server.protocols.internal.v2.PpddSrsParameters
import timber.log.Timber
import java.time.Duration
import javax.inject.Inject

@Reusable
class SelfReportSubmissionConfigMapper @Inject constructor() : SelfReportSubmissionConfig.Mapper {
override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): SelfReportSubmissionConfig = try {
when {
!rawConfig.hasSelfReportParameters() -> {
Timber.d("No SelfReportParameters -> set to default")
SelfReportSubmissionConfigContainer.DEFAULT
}
else -> rawConfig.selfReportParameters.map()
}
} catch (e: Exception) {
Timber.d(e, "SelfReportSubmissionConfigMapper failed -> returning default")
SelfReportSubmissionConfigContainer.DEFAULT
}

fun PpddSrsParameters.PPDDSelfReportSubmissionParametersAndroid.map(): SelfReportSubmissionConfigContainer {
val common = if (hasCommon()) {
SelfReportSubmissionCommonContainer(
timeSinceOnboardingInHours = if (common.timeSinceOnboardingInHours <= 0) {
Timber.d("Faulty timeSinceOnboardingInHours -> set to default")
SelfReportSubmissionCommonContainer.DEFAULT_HOURS
} else {
Duration.ofHours(common.timeSinceOnboardingInHours.toLong())
},
timeBetweenSubmissionsInDays = if (common.timeBetweenSubmissionsInDays <= 0) {
Timber.d("Faulty timeBetweenSubmissionsInDays -> set to default")
SelfReportSubmissionCommonContainer.DEFAULT_DAYS
} else {
Duration.ofHours(common.timeBetweenSubmissionsInDays.toLong())
}
)
} else {
Timber.d("No Common -> set to default")
SelfReportSubmissionCommonContainer()
}

val ppac = if (hasPpac()) {
SafetyNetRequirementsContainer(
requireBasicIntegrity = ppac.requireBasicIntegrity,
requireCTSProfileMatch = ppac.requireCTSProfileMatch,
requireEvaluationTypeBasic = ppac.requireEvaluationTypeBasic,
requireEvaluationTypeHardwareBacked = ppac.requireEvaluationTypeHardwareBacked
)
} else {
Timber.d("No Ppac -> set to default")
SafetyNetRequirementsContainer()
}

return SelfReportSubmissionConfigContainer(
common = common,
ppac = ppac
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,74 @@ package de.rki.coronawarnapp.srs.core

import dagger.Reusable
import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.appconfig.ConfigData.DeviceTimeState
import de.rki.coronawarnapp.main.CWASettings
import de.rki.coronawarnapp.srs.core.error.SrsSubmissionException
import de.rki.coronawarnapp.srs.core.error.SrsSubmissionException.ErrorCode
import de.rki.coronawarnapp.srs.core.storage.SrsSubmissionSettings
import de.rki.coronawarnapp.util.TimeStamper
import kotlinx.coroutines.flow.first
import timber.log.Timber
import java.time.Duration
import javax.inject.Inject

@Reusable
class SrsLocalChecker @Inject constructor(
private val srsSubmissionSettings: SrsSubmissionSettings,
private val appConfigProvider: AppConfigProvider
private val appConfigProvider: AppConfigProvider,
private val cwaSettings: CWASettings,
private val timeStamper: TimeStamper,
) {

/**
* Check SRS local time prerequisites
* throws an error if it fails, otherwise does nothing
* @throws SrsSubmissionException
*/
suspend fun check() {
// TODo
throw SrsSubmissionException(SrsSubmissionException.ErrorCode.SUBMISSION_TOO_EARLY)
val appConfig = appConfigProvider.getAppConfig()
val deviceTimeState = appConfig.deviceTimeState
val selfReportSubmissionCommon = appConfig.selfReportSubmission.common

if (deviceTimeState == DeviceTimeState.INCORRECT) {
Timber.e("DeviceTime INCORRECT")
throw SrsSubmissionException(ErrorCode.DEVICE_TIME_INCORRECT)
}

if (deviceTimeState == DeviceTimeState.ASSUMED_CORRECT) {
Timber.e("DeviceTime ASSUMED_CORRECT")
throw SrsSubmissionException(ErrorCode.DEVICE_TIME_UNVERIFIED)
}

val reliableDuration = Duration.between(
cwaSettings.firstReliableDeviceTime.first(),
timeStamper.nowUTC
)
val onboardingInHours = selfReportSubmissionCommon.timeSinceOnboardingInHours
if (reliableDuration < onboardingInHours) {
Timber.e(
"Time since onboarding is unverified reliableDuration=%s, configDuration=%s",
reliableDuration,
onboardingInHours
)
throw SrsSubmissionException(ErrorCode.TIME_SINCE_ONBOARDING_UNVERIFIED)
}

val durationSinceSubmission = Duration.between(
srsSubmissionSettings.getMostRecentSubmissionTime(),
timeStamper.nowUTC
)

val submissionsInDays = selfReportSubmissionCommon.timeBetweenSubmissionsInDays
if (durationSinceSubmission < submissionsInDays) {
Timber.e(
"Submission is too early durationSinceSubmission=%s, configDuration=%s",
durationSinceSubmission,
submissionsInDays
)
throw SrsSubmissionException(ErrorCode.SUBMISSION_TOO_EARLY)
}

Timber.d("Local prerequisites are met -> Congratulations!")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
import de.rki.coronawarnapp.appconfig.LogUploadConfig
import de.rki.coronawarnapp.appconfig.PresenceTracingConfig
import de.rki.coronawarnapp.appconfig.SelfReportSubmissionConfig
import de.rki.coronawarnapp.appconfig.SurveyConfig
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
Expand All @@ -36,6 +37,7 @@ class ConfigParserTest : BaseTest() {
@MockK lateinit var presenceTracingConfigMapper: PresenceTracingConfig.Mapper
@MockK lateinit var coronaTestConfigMapper: CoronaTestConfig.Mapper
@MockK lateinit var covidCertificateConfigMapper: CovidCertificateConfig.Mapper
@MockK lateinit var selfReportSubmissionConfigMapper: SelfReportSubmissionConfig.Mapper

private val appConfig171 = File("src/test/resources/appconfig_1_7_1.bin")
private val appConfig180 = File("src/test/resources/appconfig_1_8_0.bin")
Expand All @@ -54,6 +56,7 @@ class ConfigParserTest : BaseTest() {
every { presenceTracingConfigMapper.map(any()) } returns mockk()
every { coronaTestConfigMapper.map(any()) } returns mockk()
every { covidCertificateConfigMapper.map(any()) } returns mockk()
every { selfReportSubmissionConfigMapper.map(any()) } returns mockk()

appConfig171.exists() shouldBe true
appConfig180.exists() shouldBe true
Expand All @@ -70,6 +73,7 @@ class ConfigParserTest : BaseTest() {
presenceTracingConfigMapper = presenceTracingConfigMapper,
coronaTestConfigMapper = coronaTestConfigMapper,
covidCertificateConfigMapper = covidCertificateConfigMapper,
selfReportSubmissionConfigMapper = selfReportSubmissionConfigMapper,
)

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package de.rki.coronawarnapp.srs.core

import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.appconfig.ConfigData
import de.rki.coronawarnapp.appconfig.SelfReportSubmissionConfigContainer
import de.rki.coronawarnapp.main.CWASettings
import de.rki.coronawarnapp.srs.core.error.SrsSubmissionException
import de.rki.coronawarnapp.srs.core.storage.SrsSubmissionSettings
import de.rki.coronawarnapp.util.TimeStamper
import io.kotest.assertions.throwables.shouldNotThrow
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

import testhelpers.BaseTest
import java.time.Instant

internal class SrsLocalCheckerTest : BaseTest() {

@MockK lateinit var srsSubmissionSettings: SrsSubmissionSettings
@MockK lateinit var appConfigProvider: AppConfigProvider
@MockK lateinit var cwaSettings: CWASettings
@MockK lateinit var timeStamper: TimeStamper

@BeforeEach
fun setUp() {
MockKAnnotations.init(this)
every { timeStamper.nowUTC } returns Instant.parse("2022-11-02T14:01:22Z")
every { cwaSettings.firstReliableDeviceTime } returns flowOf(Instant.parse("2022-10-02T14:01:22Z"))
coEvery { srsSubmissionSettings.getMostRecentSubmissionTime() } returns
Instant.parse("2022-08-02T14:01:22Z")
coEvery { appConfigProvider.getAppConfig() } returns config()
}

@Test
fun `check pass`() = runTest {
shouldNotThrow<SrsSubmissionException> {
instance().check()
}
}

@Test
fun `device time is incorrect`() = runTest {
coEvery { appConfigProvider.getAppConfig() } returns config(ConfigData.DeviceTimeState.INCORRECT)
shouldThrow<SrsSubmissionException> {
instance().check()
}.errorCode shouldBe SrsSubmissionException.ErrorCode.DEVICE_TIME_INCORRECT
}

@Test
fun `device time is assumed correct`() = runTest {
coEvery { appConfigProvider.getAppConfig() } returns config(ConfigData.DeviceTimeState.ASSUMED_CORRECT)
shouldThrow<SrsSubmissionException> {
instance().check()
}.errorCode shouldBe SrsSubmissionException.ErrorCode.DEVICE_TIME_UNVERIFIED
}

@Test
fun `Time since onboarding is unverified`() = runTest {
every { cwaSettings.firstReliableDeviceTime } returns flowOf(Instant.parse("2022-11-02T10:01:22Z"))
shouldThrow<SrsSubmissionException> {
instance().check()
}.errorCode shouldBe SrsSubmissionException.ErrorCode.TIME_SINCE_ONBOARDING_UNVERIFIED
}

@Test
fun `Time since last submission is too early`() = runTest {
coEvery { srsSubmissionSettings.getMostRecentSubmissionTime() } returns Instant.parse("2022-11-02T10:01:22Z")
shouldThrow<SrsSubmissionException> {
instance().check()
}.errorCode shouldBe SrsSubmissionException.ErrorCode.SUBMISSION_TOO_EARLY
}

private fun instance() = SrsLocalChecker(
srsSubmissionSettings = srsSubmissionSettings,
appConfigProvider = appConfigProvider,
cwaSettings = cwaSettings,
timeStamper = timeStamper,
)

private fun config(
state: ConfigData.DeviceTimeState = ConfigData.DeviceTimeState.CORRECT
) = mockk<ConfigData>().apply {
every { selfReportSubmission } returns SelfReportSubmissionConfigContainer.DEFAULT
every { deviceTimeState } returns state
}
}