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 75fe2040043c79c6bf15c8924f645f6b8bc96263..09353815926329df5bc8708202254f4a1786fd4a 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 @@ -8,6 +8,7 @@ import de.rki.coronawarnapp.appconfig.mapping.CWAConfigMapper import de.rki.coronawarnapp.appconfig.mapping.ExposureDetectionConfigMapper import de.rki.coronawarnapp.appconfig.mapping.ExposureWindowRiskCalculationConfigMapper import de.rki.coronawarnapp.appconfig.mapping.KeyDownloadParametersMapper +import de.rki.coronawarnapp.appconfig.mapping.LogUploadConfigMapper import de.rki.coronawarnapp.appconfig.mapping.SurveyConfigMapper import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl @@ -73,6 +74,10 @@ class AppConfigModule { fun analyticsMapper(mapper: AnalyticsConfigMapper): AnalyticsConfig.Mapper = mapper + @Provides + fun logUploadMapper(mapper: LogUploadConfigMapper): + LogUploadConfig.Mapper = mapper + companion object { private val HTTP_TIMEOUT_APPCONFIG = Duration.standardSeconds(10) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/LogUploadConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/LogUploadConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d2b9a41f0c815703b3b15ee2d1bab3ea603b314 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/LogUploadConfig.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.appconfig + +import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper + +interface LogUploadConfig { + + val safetyNetRequirements: SafetyNetRequirements + + interface Mapper : ConfigMapper<LogUploadConfig> +} 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 a55c67fc24a41d1c6d884ce5298a325ce5ee02e5..92a3d6849f65f8f840adc2c7da261670658721b1 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 @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig +import de.rki.coronawarnapp.appconfig.LogUploadConfig import de.rki.coronawarnapp.appconfig.SurveyConfig import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid @@ -20,4 +21,6 @@ interface ConfigMapping : val survey: SurveyConfig val analytics: AnalyticsConfig + + val logUpload: LogUploadConfig } 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 6c50b4cb724b4ef47b321eda5bdf788b3c186ff2..e9bf15f00f367c91d89bc5fca9e0d7c38cc16507 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 @@ -6,6 +6,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig +import de.rki.coronawarnapp.appconfig.LogUploadConfig import de.rki.coronawarnapp.appconfig.SurveyConfig import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid import timber.log.Timber @@ -18,7 +19,8 @@ class ConfigParser @Inject constructor( private val exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper, private val exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper, private val surveyConfigMapper: SurveyConfig.Mapper, - private val analyticsConfigMapper: AnalyticsConfig.Mapper + private val analyticsConfigMapper: AnalyticsConfig.Mapper, + private val logUploadConfigMapper: LogUploadConfig.Mapper, ) { fun parse(configBytes: ByteArray): ConfigMapping = try { @@ -30,7 +32,8 @@ class ConfigParser @Inject constructor( exposureDetectionConfig = exposureDetectionConfigMapper.map(it), exposureWindowRiskCalculationConfig = exposureWindowRiskCalculationConfigMapper.map(it), survey = surveyConfigMapper.map(it), - analytics = analyticsConfigMapper.map(it) + analytics = analyticsConfigMapper.map(it), + logUpload = logUploadConfigMapper.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 f7063cbff7fd53434f584136dfbdf603b0524ae0..9ebbe211688cb22f5222f1b29e9d2d2b64577004 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 @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig +import de.rki.coronawarnapp.appconfig.LogUploadConfig import de.rki.coronawarnapp.appconfig.SurveyConfig import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid @@ -15,7 +16,8 @@ data class DefaultConfigMapping( val exposureDetectionConfig: ExposureDetectionConfig, val exposureWindowRiskCalculationConfig: ExposureWindowRiskCalculationConfig, override val survey: SurveyConfig, - override val analytics: AnalyticsConfig + override val analytics: AnalyticsConfig, + override val logUpload: LogUploadConfig ) : ConfigMapping, CWAConfig by cwaConfig, KeyDownloadConfig by keyDownloadConfig, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/LogUploadConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/LogUploadConfigMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d30be7e16d471111f4bf239190da15c30d3f325 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/LogUploadConfigMapper.kt @@ -0,0 +1,38 @@ +package de.rki.coronawarnapp.appconfig.mapping + +import dagger.Reusable +import de.rki.coronawarnapp.appconfig.LogUploadConfig +import de.rki.coronawarnapp.appconfig.SafetyNetRequirements +import de.rki.coronawarnapp.appconfig.SafetyNetRequirementsContainer +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class LogUploadConfigMapper @Inject constructor() : LogUploadConfig.Mapper { + + override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): LogUploadConfig { + if (!rawConfig.hasErrorLogSharingParameters()) { + Timber.w("No error log sharing parameters found, returning defaults.") + return LogUploadConfigContainer() + } + return LogUploadConfigContainer( + safetyNetRequirements = rawConfig.mapSafetyNet() + ) + } + + private fun AppConfigAndroid.ApplicationConfigurationAndroid.mapSafetyNet(): SafetyNetRequirementsContainer { + return privacyPreservingAnalyticsParameters.ppac.let { + SafetyNetRequirementsContainer( + requireBasicIntegrity = it.requireBasicIntegrity, + requireCTSProfileMatch = it.requireCTSProfileMatch, + requireEvaluationTypeBasic = it.requireEvaluationTypeBasic, + requireEvaluationTypeHardwareBacked = it.requireEvaluationTypeHardwareBacked + ) + } + } + + data class LogUploadConfigContainer( + override val safetyNetRequirements: SafetyNetRequirements = SafetyNetRequirementsContainer() + ) : LogUploadConfig +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt index e4b7c3f1469951470ca05c7afd99f879ca70e6d0..c8a451d8003001bc7b7e40e6c82e299642b45d22 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt @@ -2,7 +2,7 @@ package de.rki.coronawarnapp.bugreporting import android.content.Context import com.google.gson.Gson -import de.rki.coronawarnapp.bugreporting.uploadhistory.UploadHistory +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.UploadHistory import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.preferences.FlowPreference import de.rki.coronawarnapp.util.preferences.clearAndNotify diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt index 542382df7aeae2ba283643c398c4b1c8fcff02d2..70cf264d396708e97412548538bd50aafa167d42 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.bugreporting import dagger.Module import dagger.Provides +import dagger.Reusable import de.rki.coronawarnapp.bugreporting.censors.BugCensor import de.rki.coronawarnapp.bugreporting.censors.DiaryLocationCensor import de.rki.coronawarnapp.bugreporting.censors.DiaryPersonCensor @@ -9,14 +10,54 @@ import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor import de.rki.coronawarnapp.bugreporting.censors.RegistrationTokenCensor import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebugLoggerScope import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadApiV1 +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadAuthApiV1 +import de.rki.coronawarnapp.environment.bugreporting.LogUploadHttpClient +import de.rki.coronawarnapp.environment.bugreporting.LogUploadServerUrl +import de.rki.coronawarnapp.environment.datadonation.DataDonationCDNHttpClient +import de.rki.coronawarnapp.environment.datadonation.DataDonationCDNServerUrl import de.rki.coronawarnapp.util.CWADebug import kotlinx.coroutines.CoroutineScope +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.protobuf.ProtoConverterFactory import timber.log.Timber import javax.inject.Singleton @Module class BugReportingSharedModule { + @Reusable + @Provides + fun logUploadApi( + @LogUploadHttpClient client: OkHttpClient, + @LogUploadServerUrl url: String, + protoConverterFactory: ProtoConverterFactory, + gsonConverterFactory: GsonConverterFactory + ): LogUploadApiV1 = Retrofit.Builder() + .client(client) + .baseUrl(url) + .addConverterFactory(protoConverterFactory) + .addConverterFactory(gsonConverterFactory) + .build() + .create(LogUploadApiV1::class.java) + + @Reusable + @Provides + fun logUploadAuthApi( + @DataDonationCDNHttpClient client: OkHttpClient, + @DataDonationCDNServerUrl url: String, + protoConverterFactory: ProtoConverterFactory, + gsonConverterFactory: GsonConverterFactory + ): LogUploadAuthApiV1 = Retrofit.Builder() + .client(client) + .baseUrl(url) + .addConverterFactory(protoConverterFactory) + .addConverterFactory(gsonConverterFactory) + .build() + .create(LogUploadAuthApiV1::class.java) + @Singleton @Provides fun debugLogger() = CWADebug.debugLogger diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharing.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExport.kt similarity index 90% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharing.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExport.kt index 31df4941d7ce7521bfc216685519d1509e9e867f..12d91f7ef912e45f33766f9838c2a8499e784c01 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharing.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExport.kt @@ -1,8 +1,9 @@ -package de.rki.coronawarnapp.bugreporting.debuglog.sharing +package de.rki.coronawarnapp.bugreporting.debuglog.export import android.content.ContentResolver import android.content.Intent import android.net.Uri +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter import de.rki.coronawarnapp.util.files.determineMimeType import okio.buffer import okio.sink @@ -12,7 +13,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class SAFLogSharing @Inject constructor() { +class SAFLogExport @Inject constructor() { private var lastId = 1 private val requestMap = mutableMapOf<Int, Request>() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotter.kt similarity index 90% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotter.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotter.kt index 1cb67b8deb4352f0fcbd6c54db2722ed38e80ace..8bf4d816bfdff47cc35fe8a7535243b206097d78 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotter.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.bugreporting.debuglog.sharing +package de.rki.coronawarnapp.bugreporting.debuglog.internal import android.content.Context import dagger.Reusable @@ -27,8 +27,9 @@ class LogSnapshotter @Inject constructor( */ fun snapshot(): Snapshot { Timber.tag(TAG).d("snapshot()") + snapshotDir.listFiles()?.forEach { - Timber.tag(TAG).w("Deleting stale snapshot: %s", it) + if (it.delete()) Timber.tag(TAG).w("Deleted stale snapshot: %s", it) } val now = timeStamper.nowUTC @@ -46,9 +47,7 @@ class LogSnapshotter @Inject constructor( return Snapshot(path = zipFile) } - data class Snapshot( - val path: File - ) { + data class Snapshot(val path: File) { fun delete() = path.delete() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt index 5ead8f2fd6c84190a44a3f36a9476dc05d190902..cad3fa356498e026688e470506e56296c256ed6a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt @@ -8,8 +8,8 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.bugreporting.BugReportingSettings import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger -import de.rki.coronawarnapp.bugreporting.debuglog.sharing.LogSnapshotter -import de.rki.coronawarnapp.bugreporting.debuglog.sharing.SAFLogSharing +import de.rki.coronawarnapp.bugreporting.debuglog.export.SAFLogExport +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.coroutine.DispatcherProvider @@ -27,7 +27,7 @@ class DebugLogViewModel @AssistedInject constructor( private val enfClient: ENFClient, bugReportingSettings: BugReportingSettings, private val logSnapshotter: LogSnapshotter, - private val safLogSharing: SAFLogSharing, + private val safLogExport: SAFLogExport, private val contentResolver: ContentResolver, ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { @@ -51,8 +51,8 @@ class DebugLogViewModel @AssistedInject constructor( }.asLiveData(context = dispatcherProvider.Default) val errorEvent = SingleLiveEvent<Throwable>() - val shareEvent = SingleLiveEvent<SAFLogSharing.Request>() - val logStoreResult = SingleLiveEvent<SAFLogSharing.Request.Result>() + val shareEvent = SingleLiveEvent<SAFLogExport.Request>() + val logStoreResult = SingleLiveEvent<SAFLogExport.Request.Result>() fun onPrivacyButtonPress() { routeToScreen.postValue(DebugLogNavigationEvents.NavigateToPrivacyFragment) @@ -82,15 +82,10 @@ class DebugLogViewModel @AssistedInject constructor( } } - fun onUploadLog() = launchWithProgress { - Timber.d("uploadLog()") - throw NotImplementedError("TODO") - } - fun onStoreLog() = launchWithProgress(finishProgressAction = false) { Timber.d("storeLog()") val snapshot = logSnapshotter.snapshot() - val shareRequest = safLogSharing.createSAFRequest(snapshot) + val shareRequest = safLogExport.createSAFRequest(snapshot) shareEvent.postValue(shareRequest) } @@ -100,7 +95,7 @@ class DebugLogViewModel @AssistedInject constructor( return@launchWithProgress } - val request = safLogSharing.getRequest(requestCode) + val request = safLogExport.getRequest(requestCode) if (request == null) { Timber.w("Unknown request with code $requestCode") return@launchWithProgress diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt index 4a776f13f98c0c7a0549ead9fb376d4388291e69..6aac39aad2378643f4bd87797f38c2fb34e66bb8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt @@ -3,10 +3,13 @@ package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload import android.os.Bundle import android.view.View import android.view.accessibility.AccessibilityEvent +import android.widget.Toast +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.BugreportingDebuglogUploadFragmentBinding import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.tryHumanReadableError import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.popBackStack @@ -20,12 +23,14 @@ class DebugLogUploadFragment : Fragment(R.layout.bugreporting_debuglog_upload_fr @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory private val vm: DebugLogUploadViewModel by cwaViewModels { viewModelFactory } private val binding: BugreportingDebuglogUploadFragmentBinding by viewBindingLazy() + private lateinit var uploadDialog: LogUploadBlockingDialog override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.apply { + uploadDialog = LogUploadBlockingDialog(requireContext()) - debugLogShareButton.setOnClickListener { + binding.apply { + uploadAction.setOnClickListener { vm.onUploadLog() } @@ -37,7 +42,23 @@ class DebugLogUploadFragment : Fragment(R.layout.bugreporting_debuglog_upload_fr } vm.routeToScreen.observe2(this) { - doNavigate(it) + when (it) { + null -> popBackStack() + else -> doNavigate(it) + } + } + + vm.errorEvent.observe2(this) { + AlertDialog.Builder(requireContext()).apply { + val errorForHumans = it.tryHumanReadableError(requireContext()) + setTitle(errorForHumans.title ?: getString(R.string.errors_generic_headline)) + setMessage(errorForHumans.description) + }.show() + } + + vm.uploadInProgress.observe2(this) { uploadDialog.setState(it) } + vm.uploadSuccess.observe2(this) { + Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt index 0f08dc57c0858b022d14d2fba0fcc1a0cea4c310..1f058f22ae5daa60cc0edaf43782e53264a10944 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt @@ -1,21 +1,35 @@ package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload +import androidx.lifecycle.MutableLiveData import androidx.navigation.NavDirections import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.ui.SingleLiveEvent +import de.rki.coronawarnapp.bugreporting.debuglog.upload.SnapshotUploader import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory class DebugLogUploadViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, + private val snapshotUploader: SnapshotUploader ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + val routeToScreen: SingleLiveEvent<NavDirections?> = SingleLiveEvent() + val uploadInProgress = MutableLiveData(false) + val errorEvent = SingleLiveEvent<Throwable>() + val uploadSuccess = SingleLiveEvent<String>() - val routeToScreen = SingleLiveEvent<NavDirections>() - - fun onUploadLog() { - // TODO Implement Uploading + fun onUploadLog() = launch { + uploadInProgress.postValue(true) + try { + snapshotUploader.uploadSnapshot() + uploadSuccess.postValue("\uD83D\uDC4D") + routeToScreen.postValue(null) + } catch (e: Throwable) { + errorEvent.postValue(e) + } finally { + uploadInProgress.postValue(false) + } } fun onPrivacyButtonPress() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/LogUploadBlockingDialog.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/LogUploadBlockingDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..07c7289b695d34fc2f25ba4bb98b7bf14544ec9f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/LogUploadBlockingDialog.kt @@ -0,0 +1,23 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import de.rki.coronawarnapp.R + +class LogUploadBlockingDialog(val context: Context) { + + private val dialog by lazy { + AlertDialog.Builder(context).apply { + setCancelable(false) + setView(R.layout.bugreporting_debuglog_upload_dialog) + }.create() + } + + fun setState(show: Boolean) { + if (show && !dialog.isShowing) { + dialog.show() + } else if (!show && dialog.isShowing) { + dialog.dismiss() + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/HistoryItemAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/HistoryItemAdapter.kt similarity index 58% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/HistoryItemAdapter.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/HistoryItemAdapter.kt index 940dc79f79ef1d8918c83f2433e1c2947819a3dd..969b307edf350297b18bfc3090774b8b5b72f5c3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/HistoryItemAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/HistoryItemAdapter.kt @@ -1,13 +1,17 @@ -package de.rki.coronawarnapp.bugreporting.uploadhistory.ui +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context import android.view.ViewGroup import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.bugreporting.uploadhistory.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload import de.rki.coronawarnapp.databinding.BugreportingUploadHistoryItemBinding import de.rki.coronawarnapp.ui.lists.BaseAdapter import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone import de.rki.coronawarnapp.util.lists.BindableVH import org.joda.time.format.DateTimeFormat +import timber.log.Timber class HistoryItemAdapter : BaseAdapter<HistoryItemAdapter.CachedKeyViewHolder>() { @@ -36,6 +40,23 @@ class HistoryItemAdapter : BaseAdapter<HistoryItemAdapter.CachedKeyViewHolder>() ) -> Unit = { item, _ -> title.text = FORMATTER.print(item.uploadedAt.toUserTimeZone()) description.text = "ID ${item.id}" + itemView.setOnClickListener { + try { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip( + ClipData.newPlainText( + context.getString(R.string.debugging_debuglog_share_log_title), + """ + ${context.getString(R.string.debugging_debuglog_share_log_title)} + ${title.text} + ${description.text} + """.trimIndent() + ) + ) + } catch (e: Throwable) { + Timber.e(e, "Failed to copy ID to clipboard.") + } + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryFragment.kt similarity index 95% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryFragment.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryFragment.kt index 2a8a68daee045046c296603bda19fcddc131abe9..a608c49a4547607b6d9e6b43249bcaf42429444e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryFragment.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.bugreporting.uploadhistory.ui +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history import android.os.Bundle import android.view.View diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryModule.kt similarity index 90% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryModule.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryModule.kt index e5948de68962f9e6b64b1cb266ad67bfe4035cba..7ecbd6b53ece2cf03d47022633f75a3e1a21fb70 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryModule.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.bugreporting.uploadhistory.ui +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history import dagger.Binds import dagger.Module diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryViewModel.kt similarity index 87% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryViewModel.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryViewModel.kt index 45041e40cce6ff592e361930a1f0d7e575eeade2..93d6191cb3d580b873b522771439ed0b840b41c4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryViewModel.kt @@ -1,11 +1,11 @@ -package de.rki.coronawarnapp.bugreporting.uploadhistory.ui +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.bugreporting.BugReportingSettings -import de.rki.coronawarnapp.bugreporting.uploadhistory.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploader.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploader.kt new file mode 100644 index 0000000000000000000000000000000000000000..b2c1dac5df52dc5dac5f970de091243340d8954a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploader.kt @@ -0,0 +1,58 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload + +import de.rki.coronawarnapp.bugreporting.BugReportingSettings +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadServer +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadAuthorizer +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SnapshotUploader @Inject constructor( + private val snapshotter: LogSnapshotter, + private val uploadServer: LogUploadServer, + private val authorizer: LogUploadAuthorizer, + private val bugReportingSettings: BugReportingSettings +) { + + suspend fun uploadSnapshot(): LogUpload { + Timber.tag(TAG).v("uploadSnapshot()") + + val authorizedOtp = authorizer.getAuthorizedOTP().also { + Timber.tag(TAG).d("Authorized OTP obtained: %s", it) + } + + val snapshot = snapshotter.snapshot().also { + Timber.tag(TAG).d("Snapshot created: %s", it) + } + + val logUpload = try { + uploadServer.uploadLog(authorizedOtp, snapshot).also { + Timber.tag(TAG).d("Log uploaded: %s", it) + } + } finally { + snapshot.delete().also { + Timber.tag(TAG).d("Snapshot was deleted after upload: %b", it) + } + } + + bugReportingSettings.uploadHistory.update { oldHistory -> + val newLogs = oldHistory.logs.toMutableList() + if (newLogs.size >= 10) { + newLogs.removeFirst().also { + Timber.tag(TAG).d("Removed oldest entry from history: %s", it) + } + } + newLogs.add(logUpload) + oldHistory.copy(logs = newLogs) + } + + return logUpload + } + + companion object { + private const val TAG = "SnapshotUploader" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/LogUpload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/LogUpload.kt similarity index 60% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/LogUpload.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/LogUpload.kt index 798ea60b434c73f3810b5bb7fd378c7411606224..e5edc8b30101ce568dd0980d554355356e2510cf 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/LogUpload.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/LogUpload.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.bugreporting.uploadhistory +package de.rki.coronawarnapp.bugreporting.debuglog.upload.history import org.joda.time.Instant diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/UploadHistory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/UploadHistory.kt similarity index 52% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/UploadHistory.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/UploadHistory.kt index 6a51bfa21b5798453265917bcf3402c84f2ff4d1..fa8dc33641b885984586503f6ebfe2690c77b4a9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/UploadHistory.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/UploadHistory.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.bugreporting.uploadhistory +package de.rki.coronawarnapp.bugreporting.debuglog.upload.history data class UploadHistory( val logs: List<LogUpload> = emptyList() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..12223408be7053f0b72f1b934383610b097d892e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiV1.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server + +import com.google.gson.annotations.SerializedName +import okhttp3.MultipartBody +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +interface LogUploadApiV1 { + + @Multipart + @POST("/api/logs") + suspend fun uploadLog( + @Header("cwa-otp") otp: String, + @Part logZip: MultipartBody.Part + ): UploadResponse + + data class UploadResponse( + @SerializedName("id") val id: String, + @SerializedName("hash") val hash: String?, + @SerializedName("errorCode") val errorCode: String? + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServer.kt new file mode 100644 index 0000000000000000000000000000000000000000..4cc673a1dca2acf4a692fb2ab56c7fedaaf9a7fa --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServer.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server + +import dagger.Lazy +import dagger.Reusable +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadOtp +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.files.determineMimeType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class LogUploadServer @Inject constructor( + private val uploadApiProvider: Lazy<LogUploadApiV1>, + private val timeStamper: TimeStamper +) { + + private val uploadApi: LogUploadApiV1 + get() = uploadApiProvider.get() + + suspend fun uploadLog(uploadOtp: LogUploadOtp, snapshot: LogSnapshotter.Snapshot): LogUpload { + val response = uploadApi.uploadLog( + otp = uploadOtp.otp, + logZip = MultipartBody.Part.createFormData( + name = "file", + filename = snapshot.path.name, + body = snapshot.path.asRequestBody(snapshot.path.determineMimeType().toMediaType()) + ) + ) + Timber.tag(TAG).d("Upload response: %s", response) + + return LogUpload(id = response.id, uploadedAt = timeStamper.nowUTC) + } + + companion object { + private const val TAG = "LogUploadServer" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..ef9ea5988795eccfa701d6bd790727d7c3ab69f7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth + +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtpRequestAndroid +import retrofit2.http.Body +import retrofit2.http.POST + +interface LogUploadAuthApiV1 { + + data class AuthResponse( + @SerializedName("expirationDate") val expirationDate: String + ) + + data class AuthError( + @SerializedName("errorCode") val errorCode: String? + ) + + @POST("version/v1/android/log") + suspend fun authOTP( + @Body requestBody: ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid + ): AuthResponse +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizer.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a0a77ce39de5095615995934125dbd28da67e2c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizer.kt @@ -0,0 +1,63 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth + +import dagger.Lazy +import dagger.Reusable +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation +import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtp +import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtpRequestAndroid +import kotlinx.coroutines.flow.first +import org.joda.time.Instant +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject + +@Reusable +class LogUploadAuthorizer @Inject constructor( + private val authApiProvider: Lazy<LogUploadAuthApiV1>, + private val deviceAttestation: DeviceAttestation, + private val configProvider: AppConfigProvider +) { + + private val authApi: LogUploadAuthApiV1 + get() = authApiProvider.get() + + suspend fun getAuthorizedOTP(otp: UUID = UUID.randomUUID()): LogUploadOtp { + Timber.tag(TAG).d("getAuthorizedOTP() trying to authorize %s", otp) + + val elsOtp = ElsOtp.ELSOneTimePassword.newBuilder().apply { + setOtp(otp.toString()) + }.build() + + val appConfig = configProvider.currentConfig.first() + + val attestationRequest = object : DeviceAttestation.Request { + override val configData: ConfigData = appConfig + override val checkDeviceTime: Boolean = false + override val scenarioPayload: ByteArray = elsOtp.toByteArray() + } + val attestionResult = deviceAttestation.attest(attestationRequest) + Timber.tag(TAG).d("Attestation passed, requesting authorization from server for %s", attestionResult) + + attestionResult.requirePass(appConfig.logUpload.safetyNetRequirements) + + val elsRequest = ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid.newBuilder().apply { + authentication = attestionResult.accessControlProtoBuf + payload = elsOtp + }.build() + + // TODO This was written without backend available, retest. + val authResponse = authApi.authOTP(elsRequest).also { + Timber.tag(TAG).v("Auth response received: %s", it) + } + + return LogUploadOtp(otp = otp.toString(), expirationDate = Instant.parse(authResponse.expirationDate)).also { + Timber.tag(TAG).d("%s created", it) + } + } + + companion object { + private const val TAG = "LogUploadOtpServer" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadOtp.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadOtp.kt new file mode 100644 index 0000000000000000000000000000000000000000..8c9532da72554a2533d82b6a3aa78187a5087b69 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadOtp.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth + +import org.joda.time.Instant + +data class LogUploadOtp( + val otp: String, + val expirationDate: Instant +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt index a35fb01fba3801ff429094d5c863dc25478f1dab..dcf91e1f6f93e69204fb752aacc52d38168133aa 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt @@ -4,6 +4,7 @@ import de.rki.coronawarnapp.appconfig.SafetyNetRequirements import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException.Type import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid import okio.ByteString.Companion.toByteString +import timber.log.Timber internal data class AttestationContainer( private val ourSalt: ByteArray, @@ -16,6 +17,8 @@ internal data class AttestationContainer( }.build() override fun requirePass(reqs: SafetyNetRequirements) { + Timber.v("requirePass(%s)", reqs) + if (reqs.requireBasicIntegrity && !report.basicIntegrity) { throw SafetyNetException( Type.BASIC_INTEGRITY_REQUIRED, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt index e29f43cc5c8ea7baca9a7cd4b3f4a409ce4e2b83..3e39b6d5075d56c1e8f146d99047c01b90accc54 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt @@ -51,26 +51,11 @@ class CWASafetyNet @Inject constructor( throw SafetyNetException(Type.PLAY_SERVICES_VERSION_MISMATCH, "Google Play Services too old.") } - appConfigProvider.getAppConfig().apply { - if (deviceTimeState == ConfigData.DeviceTimeState.ASSUMED_CORRECT) { - throw SafetyNetException(Type.DEVICE_TIME_UNVERIFIED, "Device time is unverified") - } - if (deviceTimeState == ConfigData.DeviceTimeState.INCORRECT) { - throw SafetyNetException(Type.DEVICE_TIME_INCORRECT, "Device time is incorrect") - } - } - - 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 (!skip24hCheck && timeSinceOnboarding < Duration.standardHours(24)) { - throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "Time since first reliable timestamp <24h") + if (request.checkDeviceTime) { + Timber.tag(TAG).d("Checking device time.") + requireCorrectDeviceTime(request.configData) + } else { + Timber.tag(TAG).d("Device time check not required.") } val salt = generateSalt() @@ -105,6 +90,32 @@ class CWASafetyNet @Inject constructor( return AttestationContainer(salt, report) } + private suspend fun requireCorrectDeviceTime(suppliedConfig: ConfigData?) { + val configData = suppliedConfig ?: appConfigProvider.getAppConfig() + + configData.apply { + if (deviceTimeState == ConfigData.DeviceTimeState.ASSUMED_CORRECT) { + throw SafetyNetException(Type.DEVICE_TIME_UNVERIFIED, "Device time is unverified") + } + if (deviceTimeState == ConfigData.DeviceTimeState.INCORRECT) { + throw SafetyNetException(Type.DEVICE_TIME_INCORRECT, "Device time is incorrect") + } + } + + 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 (!skip24hCheck && timeSinceOnboarding < Duration.standardHours(24)) { + throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "Time since first reliable timestamp <24h") + } + } + companion object { private const val TAG = "CWASafetyNet" } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt index 6222c05f6c04605ec0e68b27c5a570cb8bf96be4..945f7ef47deb26608595a11193d3c3d66d96c3ad 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.datadonation.safetynet +import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.SafetyNetRequirements import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid @@ -13,6 +14,13 @@ interface DeviceAttestation { suspend fun attest(request: Request): Result interface Request { + + val configData: ConfigData? + get() = null + + val checkDeviceTime: Boolean + get() = true + /** * e.g. for EventSurvey, a UUID, base64 encoded. */ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt index b83bd416320695e997e6f5e36bfda2a3888614ab..b99710e6d4a6bd5a07a5bb7d08931aecf4594746 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.environment import dagger.Module +import de.rki.coronawarnapp.environment.bugreporting.BugReportingServerModule import de.rki.coronawarnapp.environment.datadonation.DataDonationCDNModule import de.rki.coronawarnapp.environment.download.DownloadCDNModule import de.rki.coronawarnapp.environment.submission.SubmissionCDNModule @@ -11,7 +12,8 @@ import de.rki.coronawarnapp.environment.verification.VerificationCDNModule DownloadCDNModule::class, SubmissionCDNModule::class, VerificationCDNModule::class, - DataDonationCDNModule::class + DataDonationCDNModule::class, + BugReportingServerModule::class ] ) class EnvironmentModule diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt index 76bfaa4a444f2fdf833d31b60efe3f1de711923a..1476022658f27fc6711345379d875e055c8098d3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt @@ -7,6 +7,7 @@ import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DATA_DONATION import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DOWNLOAD +import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.LOG_UPLOAD import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.SAFETYNET_API_KEY import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.SUBMISSION import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.USE_EUR_KEY_PKGS @@ -33,6 +34,7 @@ class EnvironmentSetup @Inject constructor( DOWNLOAD("DOWNLOAD_CDN_URL"), VERIFICATION_KEYS("PUB_KEYS_SIGNATURE_VERIFICATION"), DATA_DONATION("DATA_DONATION_CDN_URL"), + LOG_UPLOAD("LOG_UPLOAD_SERVER_URL"), SAFETYNET_API_KEY("SAFETYNET_API_KEY") } @@ -123,6 +125,9 @@ class EnvironmentSetup @Inject constructor( val safetyNetApiKey: String get() = getEnvironmentValue(SAFETYNET_API_KEY).asString + val logUploadServerUrl: String + get() = getEnvironmentValue(LOG_UPLOAD).asString + companion object { private const val PKEY_CURRENT_ENVINROMENT = "environment.current" } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..652903409aa6e11fc50990477e90b5a77bfa4d28 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModule.kt @@ -0,0 +1,28 @@ +package de.rki.coronawarnapp.environment.bugreporting + +import dagger.Module +import dagger.Provides +import dagger.Reusable +import de.rki.coronawarnapp.environment.BaseEnvironmentModule +import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.http.HttpClientDefault +import okhttp3.OkHttpClient +import javax.inject.Singleton + +@Module +class BugReportingServerModule : BaseEnvironmentModule() { + + @Reusable + @LogUploadHttpClient + @Provides + fun cdnHttpClient(@HttpClientDefault defaultHttpClient: OkHttpClient): OkHttpClient = + defaultHttpClient.newBuilder().build() + + @Singleton + @LogUploadServerUrl + @Provides + fun provideBugReportingServerUrl(environment: EnvironmentSetup): String { + val url = environment.logUploadServerUrl + return requireValidUrl(url) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadHttpClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadHttpClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..039fd9ef69478b3a464e3ff7061281a72eebb7a6 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadHttpClient.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.environment.bugreporting + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class LogUploadHttpClient diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadServerUrl.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadServerUrl.kt new file mode 100644 index 0000000000000000000000000000000000000000..932c084aa72c7476b86d585d6894c25e87f198c4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadServerUrl.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.environment.bugreporting + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class LogUploadServerUrl diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt index 9082a6018ae3a0061bbdfb049661b47c8cfd5c98..d80f14654c58ce92655b071703e815822d1a9d9b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt @@ -7,7 +7,7 @@ import dagger.multibindings.IntoMap import de.rki.coronawarnapp.bugreporting.debuglog.ui.DebugLogFragmentModule import de.rki.coronawarnapp.bugreporting.debuglog.ui.legal.DebugLogLegalModule import de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.DebugLogUploadFragmentModule -import de.rki.coronawarnapp.bugreporting.uploadhistory.ui.LogUploadHistoryModule +import de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history.LogUploadHistoryModule import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_dialog.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_dialog.xml new file mode 100644 index 0000000000000000000000000000000000000000..7a719f9e6982e7503cc6c138eb329dd4ce278869 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_dialog.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <ProgressBar + android:id="@+id/progress_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + style="@style/Widget.AppCompat.ProgressBar" + android:layout_marginTop="16dp" + android:layout_marginBottom="16dp" + android:progressTint="@color/colorAccentTintIcon" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/progress_message" + style="@style/body1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:text="@string/debugging_debuglog_share_log_title" + app:layout_constraintBottom_toBottomOf="@+id/progress_indicator" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@+id/progress_indicator" + app:layout_constraintTop_toTopOf="@+id/progress_indicator" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml index db9ac122d831b73eafe4462148217b80694f99c3..236c03b46084c06bf16e5abe51d1809b155e74c8 100644 --- a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml @@ -24,7 +24,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:layout_marginBottom="@dimen/spacing_small" - app:layout_constraintBottom_toTopOf="@id/debug_log_share_button" + app:layout_constraintBottom_toTopOf="@id/upload_action" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/toolbar"> @@ -125,7 +125,7 @@ </ScrollView> <android.widget.Button - android:id="@+id/debug_log_share_button" + android:id="@+id/upload_action" style="@style/buttonPrimary" android:layout_width="0dp" android:layout_height="wrap_content" diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml index 54d5d5f1b066470162689e2f6e514020550a759c..f9a3c78a3a47fa9780ae2c173d4246d38d66c808 100644 --- a/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml +++ b/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml @@ -3,7 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:background="?selectableItemBackground"> <TextView android:id="@+id/title" @@ -23,6 +24,7 @@ android:layout_height="wrap_content" android:layout_marginTop="4dp" android:layout_marginBottom="16dp" + android:textIsSelectable="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/title" app:layout_constraintStart_toStartOf="@id/title" diff --git a/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml b/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml index 40cf09352152cdeeae077ace3141b38048ff4d62..3ae19f36bc16dedab8b84a46e54bdb24e113bbe3 100644 --- a/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml +++ b/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml @@ -23,7 +23,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:text="@string/submission_status_card_title_fetching" + android:text="@string/submission_test_result_pending_steps_waiting_body" app:layout_constraintBottom_toBottomOf="@+id/progress_indicator" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml index 718e593f8862ccfcb8f28982da6e0a08ede9ac9d..3f864197f4d746404191f1e46ff49d39f8d020f4 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -588,7 +588,7 @@ </fragment> <fragment android:id="@+id/logUploadHistoryFragment" - android:name="de.rki.coronawarnapp.bugreporting.uploadhistory.ui.LogUploadHistoryFragment" + android:name="de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history.LogUploadHistoryFragment" android:label="LogUploadHistoryFragment" tools:layout="@layout/bugreporting_upload_history_fragment" /> <fragment 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 46082ad4a64534c09ffb7b569c17bff9572ab56a..ab457e8182cffade0934f233d9fd08a9edecacb8 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 @@ -6,6 +6,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig +import de.rki.coronawarnapp.appconfig.LogUploadConfig import de.rki.coronawarnapp.appconfig.SurveyConfig import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe @@ -28,6 +29,7 @@ class ConfigParserTest : BaseTest() { @MockK lateinit var exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper @MockK lateinit var surveyConfigMapper: SurveyConfig.Mapper @MockK lateinit var analyticsConfigMapper: AnalyticsConfig.Mapper + @MockK lateinit var logUploadConfigMapper: LogUploadConfig.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") @@ -42,6 +44,7 @@ class ConfigParserTest : BaseTest() { every { exposureWindowRiskCalculationConfigMapper.map(any()) } returns mockk() every { surveyConfigMapper.map(any()) } returns mockk() every { analyticsConfigMapper.map(any()) } returns mockk() + every { logUploadConfigMapper.map(any()) } returns mockk() appConfig171.exists() shouldBe true appConfig180.exists() shouldBe true @@ -53,7 +56,8 @@ class ConfigParserTest : BaseTest() { exposureDetectionConfigMapper = exposureDetectionConfigMapper, exposureWindowRiskCalculationConfigMapper = exposureWindowRiskCalculationConfigMapper, surveyConfigMapper = surveyConfigMapper, - analyticsConfigMapper = analyticsConfigMapper + analyticsConfigMapper = analyticsConfigMapper, + logUploadConfigMapper = logUploadConfigMapper ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt index ccfd7db38dfbdd8f6dee4d5a3a36fae138583fdb..7fec96f255ee4de159f184d69c4b8e8426a9eeaa 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt @@ -1,8 +1,8 @@ package de.rki.coronawarnapp.bugreporting import android.content.Context -import de.rki.coronawarnapp.bugreporting.uploadhistory.LogUpload -import de.rki.coronawarnapp.bugreporting.uploadhistory.UploadHistory +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.UploadHistory import de.rki.coronawarnapp.util.serialization.SerializationModule import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharingTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExportTest.kt similarity index 89% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharingTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExportTest.kt index eb323f351d0bdd530bb575518706fbf5ddbe731f..d7716d1931e92db753827a4b2983257ce7bcc9c7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharingTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExportTest.kt @@ -1,6 +1,7 @@ -package de.rki.coronawarnapp.bugreporting.debuglog.sharing +package de.rki.coronawarnapp.bugreporting.debuglog.export import android.content.ContentResolver +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.mockk.MockKAnnotations @@ -14,7 +15,7 @@ import testhelpers.BaseIOTest import timber.log.Timber import java.io.File -class SAFLogSharingTest : BaseIOTest() { +class SAFLogExportTest : BaseIOTest() { @MockK lateinit var contentResolver: ContentResolver @@ -40,7 +41,7 @@ class SAFLogSharingTest : BaseIOTest() { Timber.uprootAll() } - private fun createInstance() = SAFLogSharing() + private fun createInstance() = SAFLogExport() @Test fun `request creation and write`() { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotterTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotterTest.kt similarity index 97% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotterTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotterTest.kt index f823097df42336229e965cff9951b9a82ef3263c..b4ccfd775f08b740cb3f13e00e4ee8e1b7c3384f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotterTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotterTest.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.bugreporting.debuglog.sharing +package de.rki.coronawarnapp.bugreporting.debuglog.internal import android.content.Context import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploaderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploaderTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..83d4946117e51104c6b3cfc5af4e2f32fed5c492 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploaderTest.kt @@ -0,0 +1,100 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload + +import de.rki.coronawarnapp.bugreporting.BugReportingSettings +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.UploadHistory +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadServer +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadAuthorizer +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadOtp +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.verify +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.preferences.mockFlowPreference +import java.io.IOException + +class SnapshotUploaderTest : BaseTest() { + + @MockK lateinit var snapshotter: LogSnapshotter + @MockK lateinit var uploadServer: LogUploadServer + @MockK lateinit var authorizer: LogUploadAuthorizer + @MockK lateinit var bugReportingSettings: BugReportingSettings + @MockK lateinit var snapshot: LogSnapshotter.Snapshot + + private val logUploadOtp = LogUploadOtp( + otp = "otp", + expirationDate = Instant.EPOCH + ) + + private val uploadHistoryPref = mockFlowPreference(UploadHistory()) + + private val expectedLogUpload = LogUpload( + id = "123e4567-e89b-12d3-a456-426652340000", + uploadedAt = Instant.parse("2020-08-20T23:00:00.000Z") + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { bugReportingSettings.uploadHistory } returns uploadHistoryPref + + coEvery { authorizer.getAuthorizedOTP(otp = any()) } returns logUploadOtp + coEvery { snapshotter.snapshot() } returns snapshot + coEvery { uploadServer.uploadLog(logUploadOtp, snapshot) } returns expectedLogUpload + + every { snapshot.delete() } returns true + } + + private fun createInstance() = SnapshotUploader( + snapshotter = snapshotter, + uploadServer = uploadServer, + authorizer = authorizer, + bugReportingSettings = bugReportingSettings + ) + + @Test + fun `upload a snapshot`() = runBlockingTest { + val instance = createInstance() + instance.uploadSnapshot() shouldBe expectedLogUpload + + uploadHistoryPref.value shouldBe UploadHistory(logs = listOf(expectedLogUpload)) + } + + @Test + fun `snapshots are deleted on errors too`() = runBlockingTest { + coEvery { uploadServer.uploadLog(logUploadOtp, snapshot) } throws IOException() + + val instance = createInstance() + + shouldThrow<IOException> { + instance.uploadSnapshot() + } + verify { snapshot.delete() } + } + + @Test + fun `upload history is capped at 10`() = runBlockingTest { + val existingEntries = (1..10L).map { LogUpload(id = "$it", Instant.ofEpochMilli(it)) } + uploadHistoryPref.update { UploadHistory(logs = existingEntries) } + + val instance = createInstance() + instance.uploadSnapshot() shouldBe expectedLogUpload + + uploadHistoryPref.value shouldBe UploadHistory( + logs = existingEntries.subList( + 1, + 10 + ) + listOf(expectedLogUpload) + ) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..3fd06b0bdf5ba30d9f165014f305e0fde8db4408 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiTest.kt @@ -0,0 +1,132 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server + +import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule +import de.rki.coronawarnapp.environment.download.DownloadCDNModule +import de.rki.coronawarnapp.exception.http.CwaWebException +import de.rki.coronawarnapp.http.HttpModule +import de.rki.coronawarnapp.util.files.determineMimeType +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import kotlinx.coroutines.runBlocking +import okhttp3.ConnectionSpec +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import testhelpers.extensions.toJsonResponse +import java.io.File +import java.util.concurrent.TimeUnit + +class LogUploadApiTest : BaseIOTest() { + + private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) + private lateinit var webServer: MockWebServer + private lateinit var serverAddress: String + private val otp = "15cff19f-af26-41bc-94f2-c1a65075e894" + private val expectedId = "11111111-af26-41bc-94f2-000000000000" + private val expectedHash = "22222222-af26-41bc-94f2-000000000000" + private val testSnapshotFile = File(testDir, "snapshot.zip") + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + webServer = MockWebServer() + webServer.start() + serverAddress = "http://${webServer.hostName}:${webServer.port}" + + testDir.mkdirs() + testSnapshotFile.writeText("We needed this months ago.") + testSnapshotFile.exists() shouldBe true + } + + @AfterEach + fun teardown() { + webServer.shutdown() + testDir.deleteRecursively() + } + + private fun createAPI(): LogUploadApiV1 { + val httpModule = HttpModule() + val defaultHttpClient = httpModule.defaultHttpClient() + val gsonConverterFactory = httpModule.provideGSONConverter() + val protoConverterFactory = httpModule.provideProtoConverter() + + val cdnHttpClient = DownloadCDNModule() + .cdnHttpClient(defaultHttpClient) + .newBuilder() + .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS)) + .build() + + return BugReportingSharedModule().logUploadApi( + client = cdnHttpClient, + url = serverAddress, + gsonConverterFactory = gsonConverterFactory, + protoConverterFactory = protoConverterFactory + ) + } + + @Test + fun `happy upload`(): Unit = runBlocking { + """ + { + id : "$expectedId", + hash: "$expectedHash" + } + """.toJsonResponse().apply { webServer.enqueue(this) } + + val api = createAPI() + api.uploadLog( + otp = otp, + logZip = MultipartBody.Part.createFormData( + name = "file", + filename = testSnapshotFile.name, + body = testSnapshotFile.asRequestBody(testSnapshotFile.determineMimeType().toMediaType()) + ) + ) + + webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { + path shouldBe "/api/logs" + val boundary = this.headers["Content-Type"]!!.replace("multipart/form-data; boundary=", "") + body.readUtf8().replace("\r\n", "\n") shouldBe """ + --$boundary + Content-Disposition: form-data; name="file"; filename="snapshot.zip" + Content-Type: application/zip + Content-Length: 26 + + We needed this months ago. + --$boundary-- + + """.trimIndent() + } + } + + @Test + fun `server returns 500`(): Unit = runBlocking { + """ + { + id : "$expectedId", + hash: "$expectedHash" + } + """.toJsonResponse().apply { webServer.enqueue(MockResponse().setResponseCode(500)) } + + val api = createAPI() + + shouldThrow<CwaWebException> { + api.uploadLog( + otp = otp, + logZip = MultipartBody.Part.createFormData( + name = "file", + filename = testSnapshotFile.name, + body = testSnapshotFile.asRequestBody(testSnapshotFile.determineMimeType().toMediaType()) + ) + ) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ecd46ea090b546a1f439c070fae6633847173fbb --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServerTest.kt @@ -0,0 +1,66 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server + +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadOtp +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.runBlocking +import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import java.io.File + +class LogUploadServerTest : BaseIOTest() { + + private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) + @MockK private lateinit var timeStamper: TimeStamper + @MockK private lateinit var uploadApiV1: LogUploadApiV1 + private val uploadOtp = LogUploadOtp( + otp = "1", + expirationDate = Instant.EPOCH.plus(Duration.standardDays(1)) + ) + private val snapshot = LogSnapshotter.Snapshot( + path = File(testDir, "snapshot.zip") + ) + private val uploadResponse = LogUploadApiV1.UploadResponse( + id = "123", + hash = null, + errorCode = null + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + coEvery { uploadApiV1.uploadLog(any(), any()) } returns uploadResponse + every { timeStamper.nowUTC } returns Instant.ofEpochMilli(1234) + } + + @AfterEach + fun teardown() { + testDir.deleteRecursively() + } + + fun createInstance() = LogUploadServer( + timeStamper = timeStamper, + uploadApiProvider = { uploadApiV1 } + ) + + @Test + fun `log upload`() = runBlocking { + val instance = createInstance() + + instance.uploadLog(uploadOtp = uploadOtp, snapshot = snapshot) shouldBe LogUpload( + id = "123", + uploadedAt = Instant.ofEpochMilli(1234) + ) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..46c4a59e9e75e3aca26e6b4b81030744f78e27e1 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt @@ -0,0 +1,104 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth + +import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule +import de.rki.coronawarnapp.environment.download.DownloadCDNModule +import de.rki.coronawarnapp.exception.http.CwaWebException +import de.rki.coronawarnapp.http.HttpModule +import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtp +import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtpRequestAndroid +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import kotlinx.coroutines.runBlocking +import okhttp3.ConnectionSpec +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.extensions.toJsonResponse +import java.util.concurrent.TimeUnit + +class LogUploadAuthApiTest : BaseTest() { + + private lateinit var webServer: MockWebServer + private lateinit var serverAddress: String + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + webServer = MockWebServer() + webServer.start() + serverAddress = "http://${webServer.hostName}:${webServer.port}" + } + + @AfterEach + fun teardown() { + webServer.shutdown() + } + + private fun createAPI(): LogUploadAuthApiV1 { + val httpModule = HttpModule() + val defaultHttpClient = httpModule.defaultHttpClient() + val gsonConverterFactory = httpModule.provideGSONConverter() + val protoConverterFactory = httpModule.provideProtoConverter() + + val cdnHttpClient = DownloadCDNModule() + .cdnHttpClient(defaultHttpClient) + .newBuilder() + .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS)) + .build() + + return BugReportingSharedModule().logUploadAuthApi( + client = cdnHttpClient, + url = serverAddress, + gsonConverterFactory = gsonConverterFactory, + protoConverterFactory = protoConverterFactory + ) + } + + @Test + fun `test auth`(): Unit = runBlocking { + """ + { + expirationDate : "2020-08-20T14:00:00.000Z" + } + """.toJsonResponse().apply { webServer.enqueue(this) } + + val elsPayload = ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid.newBuilder() + .setPayload(ElsOtp.ELSOneTimePassword.newBuilder().setOtp("15cff19f-af26-41bc-94f2-c1a65075e894")) + .setAuthentication(PpacAndroid.PPACAndroid.newBuilder().setSafetyNetJws("abc").setSalt("def")) + .build() + + val api = createAPI() + api.authOTP(requestBody = elsPayload) + + webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { + path shouldBe "/version/v1/android/log" + body.readByteArray() shouldBe elsPayload.toByteArray() + } + } + + @Test + fun `server returns 500`(): Unit = runBlocking { + """ + { + errorCode: "Nope" + } + """.toJsonResponse().apply { webServer.enqueue(MockResponse().setResponseCode(500)) } + + val elsPayload = ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid.newBuilder() + .setPayload(ElsOtp.ELSOneTimePassword.newBuilder().setOtp("15cff19f-af26-41bc-94f2-c1a65075e894")) + .setAuthentication(PpacAndroid.PPACAndroid.newBuilder().setSafetyNetJws("abc").setSalt("def")) + .build() + + val api = createAPI() + + shouldThrow<CwaWebException> { + api.authOTP(requestBody = elsPayload) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b2a8b3c9fa39ca76780f3ce666666641dd33f53b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizerTest.kt @@ -0,0 +1,85 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth + +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.LogUploadConfig +import de.rki.coronawarnapp.appconfig.SafetyNetRequirements +import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.slot +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import java.io.File +import java.util.UUID + +class LogUploadAuthorizerTest : BaseIOTest() { + + private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) + + @MockK private lateinit var authApiV1: LogUploadAuthApiV1 + @MockK private lateinit var deviceAttestation: DeviceAttestation + @MockK private lateinit var configProvider: AppConfigProvider + @MockK private lateinit var attestationResult: DeviceAttestation.Result + @MockK private lateinit var configData: ConfigData + @MockK private lateinit var logUploadConfig: LogUploadConfig + @MockK private lateinit var safetyNetRequirements: SafetyNetRequirements + + private val attestationRequestSlot = slot<DeviceAttestation.Request>() + + private val uploadResponse = LogUploadAuthApiV1.AuthResponse( + expirationDate = Instant.EPOCH.toString() + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { configData.logUpload } returns logUploadConfig + every { logUploadConfig.safetyNetRequirements } returns safetyNetRequirements + + coEvery { authApiV1.authOTP(any()) } returns uploadResponse + coEvery { deviceAttestation.attest(capture(attestationRequestSlot)) } returns attestationResult + attestationResult.apply { + every { requirePass(safetyNetRequirements) } just Runs + every { accessControlProtoBuf } returns PpacAndroid.PPACAndroid.getDefaultInstance() + } + coEvery { configProvider.currentConfig } returns flowOf(configData) + } + + @AfterEach + fun teardown() { + testDir.deleteRecursively() + } + + fun createInstance() = LogUploadAuthorizer( + authApiProvider = { authApiV1 }, + deviceAttestation = deviceAttestation, + configProvider = configProvider + ) + + @Test + fun `otp generation`() = runBlockingTest { + val expectedOtp = UUID.fromString("15cff19f-af26-41bc-94f2-c1a65075e894") + val instance = createInstance() + + instance.getAuthorizedOTP(otp = expectedOtp).apply { + otp shouldBe expectedOtp.toString() + expirationDate shouldBe Instant.EPOCH + } + + attestationRequestSlot.captured.configData shouldBe configData + attestationRequestSlot.captured.checkDeviceTime shouldBe false + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt index 45debcbe03e1a8677e5a02cbeb8a92856c899910..fae3d1cedacb466af3cbfdcc9f4cea702038a6f1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt @@ -17,6 +17,8 @@ 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.kotest.matchers.shouldNotBe +import io.mockk.Called import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -257,9 +259,35 @@ class CWASafetyNetTest : BaseTest() { exception.type shouldBe SafetyNetException.Type.TIME_SINCE_ONBOARDING_UNVERIFIED } + @Test + fun `device time checks can be disabled via request`() = runBlockingTest { + every { appConfigData.deviceTimeState } returns ConfigData.DeviceTimeState.ASSUMED_CORRECT + every { timeStamper.nowUTC } returns Instant.EPOCH + + val request = TestAttestationRequest( + "Computer says no.".toByteArray(), + checkDeviceTime = false + ) + createInstance().attest(request) shouldNotBe null + } + + @Test + fun `the request can contain a app config that should be used`() = runBlockingTest { + val request = TestAttestationRequest( + "Computer says no.".toByteArray(), + configData = appConfigData + ) + createInstance().attest(request) shouldNotBe null + + coVerify { appConfigProvider wasNot Called } + } + data class TestAttestationRequest( - override val scenarioPayload: ByteArray + override val scenarioPayload: ByteArray, + override val configData: ConfigData? = null, + override val checkDeviceTime: Boolean = true ) : DeviceAttestation.Request { + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestationTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..0e637c3c5dcff600ac28730a5140acd691a35d45 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestationTest.kt @@ -0,0 +1,17 @@ +package de.rki.coronawarnapp.datadonation.safetynet + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class DeviceAttestationTest : BaseTest() { + + @Test + fun `request has sane defaults`() { + val impl = object : DeviceAttestation.Request { + override val scenarioPayload: ByteArray = "".toByteArray() + } + impl.checkDeviceTime shouldBe true + impl.configData shouldBe null + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt index d3eb6d4aed8530a325264a5c608bd4e91fc4c2ca..62534f7290aa26c8cc7948ecfc0b71cd7b3166f1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt @@ -65,7 +65,8 @@ class EnvironmentSetupTest : BaseTest() { verificationCdnUrl shouldBe "https://verification-${env.rawKey}" appConfigVerificationKey shouldBe "12345678-${env.rawKey}" safetyNetApiKey shouldBe "placeholder-${env.rawKey}" - dataDonationCdnUrl shouldBe "https://placeholder-${env.rawKey}" + dataDonationCdnUrl shouldBe "https://datadonation-${env.rawKey}" + logUploadServerUrl shouldBe "https://logupload-${env.rawKey}" } } } @@ -121,8 +122,9 @@ class EnvironmentSetupTest : BaseTest() { EnvironmentSetup.EnvKey.DOWNLOAD.rawKey shouldBe "DOWNLOAD_CDN_URL" EnvironmentSetup.EnvKey.VERIFICATION_KEYS.rawKey shouldBe "PUB_KEYS_SIGNATURE_VERIFICATION" EnvironmentSetup.EnvKey.DATA_DONATION.rawKey shouldBe "DATA_DONATION_CDN_URL" + EnvironmentSetup.EnvKey.LOG_UPLOAD.rawKey shouldBe "LOG_UPLOAD_SERVER_URL" EnvironmentSetup.EnvKey.SAFETYNET_API_KEY.rawKey shouldBe "SAFETYNET_API_KEY" - EnvironmentSetup.EnvKey.values().size shouldBe 7 + EnvironmentSetup.EnvKey.values().size shouldBe 8 } companion object { @@ -141,7 +143,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-PROD", "DOWNLOAD_CDN_URL": "https://download-PROD", "VERIFICATION_CDN_URL": "https://verification-PROD", - "DATA_DONATION_CDN_URL": "https://placeholder-PROD", + "DATA_DONATION_CDN_URL": "https://datadonation-PROD", + "LOG_UPLOAD_SERVER_URL": "https://logupload-PROD", "SAFETYNET_API_KEY": "placeholder-PROD", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-PROD" }, @@ -150,7 +153,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-DEV", "DOWNLOAD_CDN_URL": "https://download-DEV", "VERIFICATION_CDN_URL": "https://verification-DEV", - "DATA_DONATION_CDN_URL": "https://placeholder-DEV", + "DATA_DONATION_CDN_URL": "https://datadonation-DEV", + "LOG_UPLOAD_SERVER_URL": "https://logupload-DEV", "SAFETYNET_API_KEY": "placeholder-DEV", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-DEV" }, @@ -159,7 +163,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-INT", "DOWNLOAD_CDN_URL": "https://download-INT", "VERIFICATION_CDN_URL": "https://verification-INT", - "DATA_DONATION_CDN_URL": "https://placeholder-INT", + "DATA_DONATION_CDN_URL": "https://datadonation-INT", + "LOG_UPLOAD_SERVER_URL": "https://logupload-INT", "SAFETYNET_API_KEY": "placeholder-INT", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-INT" }, @@ -168,7 +173,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-WRU", "DOWNLOAD_CDN_URL": "https://download-WRU", "VERIFICATION_CDN_URL": "https://verification-WRU", - "DATA_DONATION_CDN_URL": "https://placeholder-WRU", + "DATA_DONATION_CDN_URL": "https://datadonation-WRU", + "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU", "SAFETYNET_API_KEY": "placeholder-WRU", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU" }, @@ -177,7 +183,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-WRU-XD", "DOWNLOAD_CDN_URL": "https://download-WRU-XD", "VERIFICATION_CDN_URL": "https://verification-WRU-XD", - "DATA_DONATION_CDN_URL": "https://placeholder-WRU-XD", + "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XD", + "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XD", "SAFETYNET_API_KEY": "placeholder-WRU-XD", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XD" }, @@ -186,7 +193,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-WRU-XA", "DOWNLOAD_CDN_URL": "https://download-WRU-XA", "VERIFICATION_CDN_URL": "https://verification-WRU-XA", - "DATA_DONATION_CDN_URL": "https://placeholder-WRU-XA", + "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XA", + "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XA", "SAFETYNET_API_KEY": "placeholder-WRU-XA", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XA" }, @@ -195,7 +203,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-LOCAL", "DOWNLOAD_CDN_URL": "https://download-LOCAL", "VERIFICATION_CDN_URL": "https://verification-LOCAL", - "DATA_DONATION_CDN_URL": "https://placeholder-LOCAL", + "DATA_DONATION_CDN_URL": "https://datadonation-LOCAL", + "LOG_UPLOAD_SERVER_URL": "https://logupload-LOCAL", "SAFETYNET_API_KEY": "placeholder-LOCAL", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-LOCAL" } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModuleTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModuleTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ef3488e935830a29da318b1712d7c3ae34d36c52 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModuleTest.kt @@ -0,0 +1,50 @@ +package de.rki.coronawarnapp.environment.bugreporting + +import de.rki.coronawarnapp.environment.EnvironmentSetup +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest + +class BugReportingServerModuleTest : BaseIOTest() { + + private val validUrl = "https://logupload" + private val inValidUrl = "http://invalid" + + @MockK lateinit var environmentSetup: EnvironmentSetup + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + } + + private fun createModule() = BugReportingServerModule() + + @Test + fun `sideeffect free instantiation`() { + shouldNotThrowAny { + createModule() + } + } + + @Test + fun `valid downloaded URL comes from environment`() { + every { environmentSetup.logUploadServerUrl } returns validUrl + val module = createModule() + module.provideBugReportingServerUrl(environmentSetup) shouldBe validUrl + } + + @Test + fun `invalid downloaded URL comes from environment`() { + every { environmentSetup.logUploadServerUrl } returns inValidUrl + val module = createModule() + shouldThrowAny { + module.provideBugReportingServerUrl(environmentSetup) shouldBe validUrl + } + } +} diff --git a/prod_environments.json b/prod_environments.json index 314c1c69483fa3a3f609a7913150a6b449efa95b..9cd0aaa9c3de612c39e495bb2f90885b167fb7cc 100644 --- a/prod_environments.json +++ b/prod_environments.json @@ -5,6 +5,7 @@ "DOWNLOAD_CDN_URL": "https://svc90.main.px.t-online.de", "VERIFICATION_CDN_URL": "https://verification.coronawarn.app", "DATA_DONATION_CDN_URL": "https://data.coronawarn.app", + "LOG_UPLOAD_SERVER_URL": "https://placeholder", "SAFETYNET_API_KEY": "placeholder", "PUB_KEYS_SIGNATURE_VERIFICATION": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" }