diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt index a5c5ad2e053..688629b9dcf 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt @@ -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 @@ -103,6 +104,11 @@ object AppConfigModule { @Binds fun covidCertificateConfigMapper(mapper: CovidCertificateConfigMapper): CovidCertificateConfig.Mapper + + @Binds + fun selfReportSubmissionConfigMapper( + mapper: SelfReportSubmissionConfigMapper + ): SelfReportSubmissionConfig.Mapper } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/SelfReportSubmissionConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/SelfReportSubmissionConfig.kt new file mode 100644 index 00000000000..c5cc4b74255 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/SelfReportSubmissionConfig.kt @@ -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 +} + +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() + ) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt index 3712a012545..351a49cdaa9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt @@ -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 : @@ -23,4 +24,5 @@ interface ConfigMapping : val presenceTracing: PresenceTracingConfig val coronaTestParameters: CoronaTestConfig val covidCertificateParameters: CovidCertificateConfig + val selfReportSubmission: SelfReportSubmissionConfig } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt index 9c8efcba4a0..06a98192384 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt @@ -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 @@ -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 { @@ -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) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt index 4242182a913..cb1f4a5d84c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt @@ -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( @@ -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, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/SelfReportSubmissionConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/SelfReportSubmissionConfigMapper.kt new file mode 100644 index 00000000000..3106888cf76 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/SelfReportSubmissionConfigMapper.kt @@ -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 + ) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/srs/core/SrsLocalChecker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/srs/core/SrsLocalChecker.kt index da71cc9cac9..a818b005d80 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/srs/core/SrsLocalChecker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/srs/core/SrsLocalChecker.kt @@ -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!") } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt index f8480e27391..ed4f1b36601 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt @@ -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 @@ -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") @@ -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 @@ -70,6 +73,7 @@ class ConfigParserTest : BaseTest() { presenceTracingConfigMapper = presenceTracingConfigMapper, coronaTestConfigMapper = coronaTestConfigMapper, covidCertificateConfigMapper = covidCertificateConfigMapper, + selfReportSubmissionConfigMapper = selfReportSubmissionConfigMapper, ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/srs/core/SrsLocalCheckerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/srs/core/SrsLocalCheckerTest.kt new file mode 100644 index 00000000000..89cad4018d0 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/srs/core/SrsLocalCheckerTest.kt @@ -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 { + instance().check() + } + } + + @Test + fun `device time is incorrect`() = runTest { + coEvery { appConfigProvider.getAppConfig() } returns config(ConfigData.DeviceTimeState.INCORRECT) + shouldThrow { + 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 { + 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 { + 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 { + 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().apply { + every { selfReportSubmission } returns SelfReportSubmissionConfigContainer.DEFAULT + every { deviceTimeState } returns state + } +}