Skip to content
Snippets Groups Projects
Unverified Commit 0e01dae5 authored by Kolya Opahle's avatar Kolya Opahle Committed by GitHub
Browse files

PPA - Enhance dev menu (EXPOSUREAPP-5215) #2400


* Added the ability to skip the CWASafetyNet 24H check
Added reportProblem call to Analytics error handling to provide testers with a way to inspect PPA Errors

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

* Fixed CWASafetyNetTest

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

* Remove tinting, it's just test menu, and was more confusing for testers.

* Typo

* Skip check should only influence 24H check, not valid time check. Add debug output for time values.

* Add test to make sure the setting is only in effect on tester builds.

Co-authored-by: default avatarMatthias Urhahn <matthias.urhahn@sap.com>
Co-authored-by: default avatarharambasicluka <64483219+harambasicluka@users.noreply.github.com>
Co-authored-by: default avatarRalf Gehrer <ralfgehrer@users.noreply.github.com>
parent e6ba957c
No related branches found
No related tags found
No related merge requests found
Showing with 99 additions and 8 deletions
......@@ -161,6 +161,14 @@ class DataDonationTestFragment : Fragment(R.layout.fragment_test_datadonation),
surveyExceptionSimulationButton.setOnClickListener { vm.showSurveyErrorDialog() }
}
vm.isSafetyNetTimeCheckSkipped.observe2(this) {
binding.disableSafetynetToggle.isChecked = it
}
binding.disableSafetynetToggle.setOnClickListener {
vm.toggleSkipSafetyNetTimeCheck()
}
}
private fun RadioGroup.addRadioButton(text: String) {
......
......@@ -15,6 +15,7 @@ import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException
import de.rki.coronawarnapp.datadonation.storage.OTPRepository
import de.rki.coronawarnapp.datadonation.survey.SurveyException
import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData
import de.rki.coronawarnapp.storage.TestSettings
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.ui.SingleLiveEvent
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
......@@ -32,7 +33,8 @@ class DataDonationTestFragmentViewModel @AssistedInject constructor(
private val lastAnalyticsSubmissionLogger: LastAnalyticsSubmissionLogger,
private val cwaSafetyNet: CWASafetyNet,
otpRepository: OTPRepository,
private val appConfigProvider: AppConfigProvider
private val appConfigProvider: AppConfigProvider,
private val testSettings: TestSettings
) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
val infoEvents = SingleLiveEvent<String>()
......@@ -52,6 +54,9 @@ class DataDonationTestFragmentViewModel @AssistedInject constructor(
private val lastAnalyticsDataInternal = MutableStateFlow<LastAnalyticsSubmission?>(null)
val lastAnalyticsData = lastAnalyticsDataInternal.asLiveData(context = dispatcherProvider.Default)
val isSafetyNetTimeCheckSkipped = testSettings.skipSafetyNetTimeCheck.flow
.asLiveData(context = dispatcherProvider.Default)
val otp: String = otpRepository.otpAuthorizationResult?.toString() ?: "No OTP generated and authorized yet"
val surveyConfig = appConfigProvider.currentConfig
......@@ -154,6 +159,10 @@ class DataDonationTestFragmentViewModel @AssistedInject constructor(
}
}
fun toggleSkipSafetyNetTimeCheck() {
testSettings.skipSafetyNetTimeCheck.update { !it }
}
fun selectSafetyNetExceptionType(type: SafetyNetException.Type) {
currentSafetyNetExceptionTypeInternal.value = type
}
......
......@@ -4,7 +4,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:ignore="HardcodedText">
<LinearLayout
......@@ -84,7 +83,6 @@
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/fake_correct_devicetime_toggle"
android:layout_width="match_parent"
app:thumbTint="@color/colorAccent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/spacing_tiny"
android:layout_weight="1"
......
......@@ -257,6 +257,29 @@
app:layout_constraintTop_toBottomOf="@id/survey_exception_simulation_radio_group" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
style="@style/Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_tiny"
android:orientation="vertical">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/disable_safetynet_toggle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/spacing_tiny"
android:layout_weight="1"
android:text="Skip 24H SafetyNet Check" />
<TextView
style="@style/TextAppearance.AppCompat.Caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_tiny"
android:text="This disables the 24H time since valid time check for SafetyNet attestation" />
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/last_analytics_container"
style="@style/Card"
......
......@@ -3,6 +3,7 @@ package de.rki.coronawarnapp.datadonation.analytics
import androidx.annotation.VisibleForTesting
import de.rki.coronawarnapp.appconfig.AnalyticsConfig
import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.bugreporting.reportProblem
import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule
import de.rki.coronawarnapp.datadonation.analytics.server.DataDonationAnalyticsServer
import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings
......@@ -62,6 +63,10 @@ class Analytics @Inject constructor(
return true
} catch (err: Exception) {
err.reportProblem(
tag = TAG,
info = "An error occurred during analytics submission"
)
Timber.e(err, "Error during analytics submission")
return false
}
......@@ -112,7 +117,7 @@ class Analytics @Inject constructor(
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
suspend fun stopDueToNoAnalyticsConfig(analyticsConfig: AnalyticsConfig): Boolean {
fun stopDueToNoAnalyticsConfig(analyticsConfig: AnalyticsConfig): Boolean {
return !analyticsConfig.analyticsEnabled
}
......@@ -122,7 +127,7 @@ class Analytics @Inject constructor(
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
suspend fun stopDueToProbabilityToSubmit(analyticsConfig: AnalyticsConfig): Boolean {
fun stopDueToProbabilityToSubmit(analyticsConfig: AnalyticsConfig): Boolean {
val submitRoll = Random.nextDouble(0.0, 1.0)
return submitRoll > analyticsConfig.probabilityToSubmit
}
......@@ -193,6 +198,7 @@ class Analytics @Inject constructor(
settings.analyticsEnabled.flow
companion object {
private val TAG = Analytics::class.java.simpleName
private const val LAST_SUBMISSION_MIN_AGE_HOURS = 23
private const val ONBOARDING_DELAY_HOURS = 24
......
......@@ -6,6 +6,8 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.appconfig.ConfigData
import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException.Type
import de.rki.coronawarnapp.main.CWASettings
import de.rki.coronawarnapp.storage.TestSettings
import de.rki.coronawarnapp.util.CWADebug
import de.rki.coronawarnapp.util.HashExtensions
import de.rki.coronawarnapp.util.HashExtensions.toSHA256
import de.rki.coronawarnapp.util.TimeStamper
......@@ -27,7 +29,8 @@ class CWASafetyNet @Inject constructor(
private val appConfigProvider: AppConfigProvider,
private val googleApiVersion: GoogleApiVersion,
private val cwaSettings: CWASettings,
private val timeStamper: TimeStamper
private val timeStamper: TimeStamper,
private val testSettings: TestSettings
) : DeviceAttestation {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
......@@ -55,10 +58,16 @@ class CWASafetyNet @Inject constructor(
}
}
val skip24hCheck = CWADebug.isDeviceForTestersBuild && testSettings.skipSafetyNetTimeCheck.value
val nowUTC = timeStamper.nowUTC
val firstReliableTimeStamp = cwaSettings.firstReliableDeviceTime
val timeSinceOnboarding = Duration(firstReliableTimeStamp, nowUTC)
Timber.d("firstReliableTimeStamp=%s, now=%s", firstReliableTimeStamp, nowUTC)
Timber.d("skip24hCheck=%b, timeSinceOnboarding=%dh", skip24hCheck, timeSinceOnboarding.standardHours)
if (firstReliableTimeStamp == Instant.EPOCH) {
throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "No first reliable timestamp available")
} else if (Duration(firstReliableTimeStamp, timeStamper.nowUTC) < Duration.standardHours(24)) {
} else if (!skip24hCheck && timeSinceOnboarding < Duration.standardHours(24)) {
throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "Time since first reliable timestamp <24h")
}
......
......@@ -35,6 +35,11 @@ class TestSettings @Inject constructor(
writer = FlowPreference.gsonWriter(gson)
)
val skipSafetyNetTimeCheck = prefs.createFlowPreference(
key = "safetynet.skip.timecheck",
defaultValue = false
)
enum class FakeExposureWindowTypes {
@SerializedName("DISABLED")
DISABLED,
......
......@@ -7,10 +7,13 @@ import de.rki.coronawarnapp.environment.EnvironmentSetup
import de.rki.coronawarnapp.main.CWASettings
import de.rki.coronawarnapp.server.protocols.internal.ppdd.EdusOtp
import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid
import de.rki.coronawarnapp.storage.TestSettings
import de.rki.coronawarnapp.util.CWADebug
import de.rki.coronawarnapp.util.HashExtensions.Format.BASE64
import de.rki.coronawarnapp.util.HashExtensions.toSHA256
import de.rki.coronawarnapp.util.TimeStamper
import de.rki.coronawarnapp.util.gplay.GoogleApiVersion
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations
......@@ -20,6 +23,7 @@ import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.mockkObject
import kotlinx.coroutines.test.runBlockingTest
import okio.ByteString.Companion.decodeBase64
import org.joda.time.Duration
......@@ -28,6 +32,7 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import testhelpers.preferences.mockFlowPreference
import java.security.SecureRandom
import kotlin.random.Random
......@@ -44,6 +49,7 @@ class CWASafetyNetTest : BaseTest() {
@MockK lateinit var appConfigProvider: AppConfigProvider
@MockK lateinit var appConfigData: ConfigData
@MockK lateinit var testSettings: TestSettings
private val defaultPayload = "Computer says no.".toByteArray()
private val firstSalt = "LMK0jFCu/lOzl07ZHmtOqQ==".decodeBase64()!!
......@@ -52,6 +58,9 @@ class CWASafetyNetTest : BaseTest() {
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
mockkObject(CWADebug)
every { CWADebug.isDeviceForTestersBuild } returns false
every { environmentSetup.safetyNetApiKey } returns "very safe"
coEvery { safetyNetClientWrapper.attest(any()) } returns clientReport
every { secureRandom.nextBytes(any()) } answers {
......@@ -75,6 +84,8 @@ class CWASafetyNetTest : BaseTest() {
every { cwaSettings.firstReliableDeviceTime } returns Instant.EPOCH.plus(Duration.standardDays(7))
every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardDays(8))
every { testSettings.skipSafetyNetTimeCheck } returns mockFlowPreference(false)
}
@AfterEach
......@@ -89,7 +100,8 @@ class CWASafetyNetTest : BaseTest() {
appConfigProvider = appConfigProvider,
googleApiVersion = googleApiVersion,
timeStamper = timeStamper,
cwaSettings = cwaSettings
cwaSettings = cwaSettings,
testSettings = testSettings
)
@Test
......@@ -208,6 +220,27 @@ class CWASafetyNetTest : BaseTest() {
exception.type shouldBe SafetyNetException.Type.TIME_SINCE_ONBOARDING_UNVERIFIED
}
@Test
fun `24h since onboarding can be skipped on deviceForTester builds`() = runBlockingTest {
every { timeStamper.nowUTC } returns Instant.EPOCH
shouldThrow<SafetyNetException> {
createInstance().attest(TestAttestationRequest("Computer says no.".toByteArray()))
}.type shouldBe SafetyNetException.Type.TIME_SINCE_ONBOARDING_UNVERIFIED
every { testSettings.skipSafetyNetTimeCheck } returns mockFlowPreference(true)
shouldThrow<SafetyNetException> {
createInstance().attest(TestAttestationRequest("Computer says no.".toByteArray()))
}.type shouldBe SafetyNetException.Type.TIME_SINCE_ONBOARDING_UNVERIFIED
every { CWADebug.isDeviceForTestersBuild } returns true
shouldNotThrowAny {
createInstance().attest(TestAttestationRequest("Computer says no.".toByteArray()))
}
}
@Test
fun `first reliable devicetime timestamp needs to be set`() = runBlockingTest {
every { cwaSettings.firstReliableDeviceTime } returns Instant.EPOCH
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment