2020-08-03 16:07:06 +00:00
/ *
* SPDX - FileCopyrightText : 2020 , microG Project Team
* SPDX - License - Identifier : Apache - 2.0
* /
package org.microg.gms.nearby.exposurenotification
2020-09-05 21:51:00 +00:00
import android.app.Activity
import android.app.PendingIntent
2020-12-13 14:37:07 +00:00
import android.bluetooth.BluetoothAdapter
import android.content.*
2021-04-28 18:08:19 +00:00
import android.location.LocationManager
2020-08-03 16:07:06 +00:00
import android.os.*
2021-02-09 14:49:21 +00:00
import android.util.Base64
2020-08-03 16:07:06 +00:00
import android.util.Log
2021-04-28 18:08:19 +00:00
import androidx.core.location.LocationManagerCompat
2020-10-12 19:25:34 +00:00
import androidx.lifecycle.Lifecycle
2020-12-27 16:09:51 +00:00
import androidx.lifecycle.LifecycleCoroutineScope
2020-10-12 19:25:34 +00:00
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
2020-08-03 16:07:06 +00:00
import com.google.android.gms.common.api.Status
2020-11-18 16:06:29 +00:00
import com.google.android.gms.nearby.exposurenotification.*
2020-08-03 16:07:06 +00:00
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
import com.google.android.gms.nearby.exposurenotification.internal.*
2021-01-06 11:11:57 +00:00
import kotlinx.coroutines.*
2020-08-03 16:07:06 +00:00
import org.json.JSONArray
import org.json.JSONObject
2020-09-05 21:51:00 +00:00
import org.microg.gms.common.PackageUtils
2020-08-24 08:12:49 +00:00
import org.microg.gms.nearby.exposurenotification.Constants.*
2021-02-09 14:49:21 +00:00
import org.microg.gms.nearby.exposurenotification.proto.TEKSignatureList
2020-08-03 16:07:06 +00:00
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyExport
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyProto
2021-08-19 15:18:17 +00:00
import org.microg.gms.utils.warnOnTransactionIssues
2020-09-27 09:33:02 +00:00
import java.io.File
import java.io.InputStream
2021-02-09 14:49:21 +00:00
import java.security.KeyFactory
2020-09-27 09:33:02 +00:00
import java.security.MessageDigest
2021-02-09 14:49:21 +00:00
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
import java.util.zip.ZipEntry
2020-09-27 09:33:02 +00:00
import java.util.zip.ZipFile
2020-11-18 16:06:29 +00:00
import kotlin.math.max
2020-09-06 12:59:41 +00:00
import kotlin.math.roundToInt
2020-09-27 09:33:02 +00:00
import kotlin.random.Random
2020-08-03 16:07:06 +00:00
2020-10-12 19:25:34 +00:00
class ExposureNotificationServiceImpl ( private val context : Context , private val lifecycle : Lifecycle , private val packageName : String ) : INearbyExposureNotificationService . Stub ( ) , LifecycleOwner {
2021-02-09 14:49:21 +00:00
// Table of back-end public keys, used to verify the signature of the diagnosed TEKs.
// The table is indexed by package names.
private val backendPubKeyForPackage = mapOf < String , String > (
2021-11-02 13:52:04 +00:00
" ch.admin.bag.dp3t.dev " to
" MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsFcEnOPY4AOAKkpv9HSdW2BrhUCWwL15Hpqu5zHaWy1Wno2KR8G6dYJ8QO0uZu1M6j8z6NGXFVZcpw7tYeXAqQ== " ,
" ch.admin.bag.dp3t.test " to
" MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsFcEnOPY4AOAKkpv9HSdW2BrhUCWwL15Hpqu5zHaWy1Wno2KR8G6dYJ8QO0uZu1M6j8z6NGXFVZcpw7tYeXAqQ== " ,
" ch.admin.bag.dp3t.abnahme " to
" MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsFcEnOPY4AOAKkpv9HSdW2BrhUCWwL15Hpqu5zHaWy1Wno2KR8G6dYJ8QO0uZu1M6j8z6NGXFVZcpw7tYeXAqQ== " ,
" ch.admin.bag.dp3t " to
" MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEK2k9nZ8guo7JP2ELPQXnUkqDyjjJmYmpt9Zy0HPsiGXCdI3SFmLr204KNzkuITppNV5P7+bXRxiiY04NMrEITg== " ,
2021-11-09 14:24:41 +00:00
// CWA, see https://github.com/corona-warn-app/cwa-documentation/issues/740#issuecomment-963223074
" de.rki.coronawarnapp " to
" MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg== " ,
// CCTG uses CWA infrastucture
" de.corona.tracing " to
" MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg== " ,
// CCTG-Test builds don't have access any staging infrastructure, so again CWA key
" de.corona.tracing.test " to
" MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg== " ,
2021-02-09 14:49:21 +00:00
)
2021-02-09 17:42:21 +00:00
// Back-end public key for this package
private val backendPublicKey = backendPubKeyForPackage [ packageName ] ?. let {
try {
KeyFactory . getInstance ( " EC " ) . generatePublic ( X509EncodedKeySpec ( Base64 . decode ( it , Base64 . DEFAULT ) ) )
} catch ( e : Exception ) {
Log . w ( TAG , " Failed to retrieve back-end public key for ${packageName} : " + e . message )
null
}
}
2021-02-09 14:49:21 +00:00
// Table of supported signature algorithms for the diagnosed TEKs.
// The table is indexed by ASN.1 OIDs as specified in https://tools.ietf.org/html/rfc5758#section-3.2
2021-11-02 13:52:04 +00:00
private val sigAlgoForOid = mapOf < String , Function0 < Signature > > (
" 1.2.840.10045.4.3.2 " to { Signature . getInstance ( " SHA256withECDSA " ) } ,
" 1.2.840.10045.4.3.4 " to { Signature . getInstance ( " SHA512withECDSA " ) } ,
2021-02-09 14:49:21 +00:00
)
2020-12-27 16:09:51 +00:00
private fun LifecycleCoroutineScope . launchSafely ( block : suspend CoroutineScope . ( ) -> Unit ) : Job = launchWhenStarted { try { block ( ) } catch ( e : Exception ) { Log . w ( TAG , " Error in coroutine " , e ) } }
2020-10-12 19:25:34 +00:00
override fun getLifecycle ( ) : Lifecycle = lifecycle
2020-09-05 21:51:00 +00:00
private fun pendingConfirm ( permission : String ) : PendingIntent {
2020-08-03 16:07:06 +00:00
val intent = Intent ( ACTION _CONFIRM )
intent . `package` = context . packageName
intent . putExtra ( KEY _CONFIRM _PACKAGE , packageName )
2020-09-05 21:51:00 +00:00
intent . putExtra ( KEY _CONFIRM _ACTION , permission )
intent . putExtra ( KEY _CONFIRM _RECEIVER , object : ResultReceiver ( null ) {
2020-08-03 16:07:06 +00:00
override fun onReceiveResult ( resultCode : Int , resultData : Bundle ? ) {
2020-09-05 21:51:00 +00:00
if ( resultCode == Activity . RESULT _OK ) {
2020-10-12 19:25:34 +00:00
tempGrantedPermissions . add ( packageName to permission )
2020-09-05 21:51:00 +00:00
}
2020-08-03 16:07:06 +00:00
}
} )
try {
intent . component = ComponentName ( context , context . packageManager . resolveActivity ( intent , 0 ) ?. activityInfo ?. name !! )
} catch ( e : Exception ) {
Log . w ( TAG , e )
2020-09-05 21:51:00 +00:00
}
Log . d ( TAG , " Pending: $intent " )
val pi = PendingIntent . getActivity ( context , permission . hashCode ( ) , intent , PendingIntent . FLAG _ONE _SHOT )
Log . d ( TAG , " Pending: $pi " )
return pi
}
2020-11-19 19:07:37 +00:00
private fun hasConfirmActivity ( ) : Boolean {
val intent = Intent ( ACTION _CONFIRM )
intent . `package` = context . packageName
return try {
context . packageManager . resolveActivity ( intent , 0 ) != null
} catch ( e : Exception ) {
false
}
}
2020-12-13 14:37:07 +00:00
private suspend fun confirmPermission ( permission : String , force : Boolean = false ) : Status {
2020-09-05 21:51:00 +00:00
return ExposureDatabase . with ( context ) { database ->
2020-11-19 19:07:37 +00:00
when {
tempGrantedPermissions . contains ( packageName to permission ) -> {
database . grantPermission ( packageName , PackageUtils . firstSignatureDigest ( context , packageName ) !! , permission )
tempGrantedPermissions . remove ( packageName to permission )
Status . SUCCESS
}
2020-12-13 14:37:07 +00:00
! force && database . hasPermission ( packageName , PackageUtils . firstSignatureDigest ( context , packageName ) !! , permission ) -> {
2020-11-19 19:07:37 +00:00
Status . SUCCESS
}
! hasConfirmActivity ( ) -> {
Status . SUCCESS
}
else -> {
Status ( RESOLUTION _REQUIRED , " Permission EN# $permission required. " , pendingConfirm ( permission ) )
}
2020-09-05 21:51:00 +00:00
}
2020-08-03 16:07:06 +00:00
}
}
2020-09-27 09:33:02 +00:00
override fun getVersion ( params : GetVersionParams ) {
2021-02-21 22:39:12 +00:00
params . callback . onResult ( Status . SUCCESS , VERSION _FULL )
2020-09-27 09:33:02 +00:00
}
override fun getCalibrationConfidence ( params : GetCalibrationConfidenceParams ) {
params . callback . onResult ( Status . SUCCESS , currentDeviceInfo . confidence )
}
2020-08-03 16:07:06 +00:00
override fun start ( params : StartParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-12-12 12:12:34 +00:00
val isAuthorized = ExposureDatabase . with ( context ) { it . isAppAuthorized ( packageName ) }
2020-12-13 14:37:07 +00:00
val adapter = BluetoothAdapter . getDefaultAdapter ( )
2020-12-12 12:12:34 +00:00
val status = if ( isAuthorized && ExposurePreferences ( context ) . enabled ) {
Status . SUCCESS
2020-12-13 14:37:07 +00:00
} else if ( adapter == null ) {
Status ( FAILED _NOT _SUPPORTED , " No Bluetooth Adapter available. " )
2020-12-12 12:12:34 +00:00
} else {
2020-12-13 14:37:07 +00:00
val status = confirmPermission ( CONFIRM _ACTION _START , ! adapter . isEnabled )
2020-12-12 12:12:34 +00:00
if ( status . isSuccess ) {
2020-12-13 14:37:07 +00:00
val context = context
adapter . enableAsync ( context )
2020-12-12 12:12:34 +00:00
ExposurePreferences ( context ) . enabled = true
ExposureDatabase . with ( context ) { database ->
database . authorizeApp ( packageName )
database . noteAppAction ( packageName , " start " )
}
2020-12-12 11:40:48 +00:00
}
2020-12-12 12:12:34 +00:00
status
2020-10-12 19:25:34 +00:00
}
try {
params . callback . onResult ( status )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-08-03 16:07:06 +00:00
}
}
override fun stop ( params : StopParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-12-12 11:40:48 +00:00
val isAuthorized = ExposureDatabase . with ( context ) { database ->
database . isAppAuthorized ( packageName ) . also {
if ( it ) database . noteAppAction ( packageName , " stop " )
}
}
if ( isAuthorized ) {
ExposurePreferences ( context ) . enabled = false
2020-10-12 19:25:34 +00:00
}
try {
params . callback . onResult ( Status . SUCCESS )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-08-03 16:07:06 +00:00
}
}
2020-10-12 19:25:34 +00:00
2020-08-03 16:07:06 +00:00
override fun isEnabled ( params : IsEnabledParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-12-12 11:40:48 +00:00
val isAuthorized = ExposureDatabase . with ( context ) { database ->
2020-12-20 11:43:38 +00:00
database . isAppAuthorized ( packageName )
2020-12-12 11:40:48 +00:00
}
try {
params . callback . onResult ( Status . SUCCESS , isAuthorized && ExposurePreferences ( context ) . enabled )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-08-03 16:07:06 +00:00
}
}
2020-09-05 21:51:00 +00:00
override fun getTemporaryExposureKeyHistory ( params : GetTemporaryExposureKeyHistoryParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-10-12 19:25:34 +00:00
val status = confirmPermission ( CONFIRM _ACTION _KEYS )
val response = when {
status . isSuccess -> ExposureDatabase . with ( context ) { database ->
2020-12-12 11:40:48 +00:00
database . authorizeApp ( packageName )
2020-12-11 10:51:53 +00:00
database . exportKeys ( )
2020-10-12 19:25:34 +00:00
}
else -> emptyList ( )
2020-08-03 16:07:06 +00:00
}
2020-10-12 19:25:34 +00:00
ExposureDatabase . with ( context ) { database ->
database . noteAppAction ( packageName , " getTemporaryExposureKeyHistory " , JSONObject ( ) . apply {
put ( " result " , status . statusCode )
put ( " response_keys_size " , response . size )
} . toString ( ) )
}
try {
params . callback . onResult ( status , response )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-08-03 16:07:06 +00:00
}
}
private fun TemporaryExposureKeyProto . toKey ( ) : TemporaryExposureKey = TemporaryExposureKey . TemporaryExposureKeyBuilder ( )
. setKeyData ( key _data ?. toByteArray ( ) ?: throw IllegalArgumentException ( " key data missing " ) )
. setRollingStartIntervalNumber ( rolling _start _interval _number
?: throw IllegalArgumentException ( " rolling start interval number missing " ) )
. setRollingPeriod ( rolling _period ?: throw IllegalArgumentException ( " rolling period missing " ) )
. setTransmissionRiskLevel ( transmission _risk _level ?: 0 )
. build ( )
2020-09-27 09:33:02 +00:00
private fun InputStream . copyToFile ( outputFile : File ) {
outputFile . outputStream ( ) . use { output ->
copyTo ( output )
output . flush ( )
}
}
private fun MessageDigest . digest ( file : File ) : ByteArray = file . inputStream ( ) . use { input ->
val buf = ByteArray ( 4096 )
var bytes = input . read ( buf )
while ( bytes != - 1 ) {
update ( buf , 0 , bytes )
bytes = input . read ( buf )
}
digest ( )
2020-08-03 16:07:06 +00:00
}
2020-11-18 16:06:29 +00:00
private fun ExposureConfiguration ?. orDefault ( ) = this
?: ExposureConfiguration . ExposureConfigurationBuilder ( ) . build ( )
2020-10-12 19:25:34 +00:00
private suspend fun buildExposureSummary ( token : String ) : ExposureSummary = ExposureDatabase . with ( context ) { database ->
2020-12-12 11:40:48 +00:00
if ( ! database . isAppAuthorized ( packageName ) ) {
// Not providing summary if app not authorized
Log . d ( TAG , " $packageName not yet authorized " )
return @with ExposureSummary . ExposureSummaryBuilder ( ) . build ( )
}
2020-10-09 11:54:32 +00:00
val pair = database . loadConfiguration ( packageName , token )
val ( configuration , exposures ) = if ( pair != null ) {
2020-11-18 16:06:29 +00:00
pair . second . orDefault ( ) to database . findAllMeasuredExposures ( pair . first ) . merge ( )
2020-10-09 11:54:32 +00:00
} else {
ExposureConfiguration . ExposureConfigurationBuilder ( ) . build ( ) to emptyList ( )
}
ExposureSummary . ExposureSummaryBuilder ( )
2022-01-18 17:51:21 +00:00
. setDaysSinceLastExposure ( exposures . map { it . daysSinceExposure } . minOrNull ( ) ?. toInt ( ) ?: 0 )
2020-10-09 11:54:32 +00:00
. setMatchedKeyCount ( exposures . map { it . key } . distinct ( ) . size )
2022-01-18 17:51:21 +00:00
. setMaximumRiskScore ( exposures . map { it . getRiskScore ( configuration ) } . maxOrNull ( ) ?. toInt ( ) ?: 0 )
2020-10-09 11:54:32 +00:00
. setAttenuationDurations ( intArrayOf (
exposures . map { it . getAttenuationDurations ( configuration ) [ 0 ] } . sum ( ) ,
exposures . map { it . getAttenuationDurations ( configuration ) [ 1 ] } . sum ( ) ,
exposures . map { it . getAttenuationDurations ( configuration ) [ 2 ] } . sum ( )
) )
. setSummationRiskScore ( exposures . map { it . getRiskScore ( configuration ) } . sum ( ) )
. build ( )
}
2020-08-03 16:07:06 +00:00
override fun provideDiagnosisKeys ( params : ProvideDiagnosisKeysParams ) {
2020-12-03 09:15:18 +00:00
val token = params . token ?: TOKEN _A
Log . w ( TAG , " provideDiagnosisKeys() with $packageName / $token " )
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-10-12 19:25:34 +00:00
val tid = ExposureDatabase . with ( context ) { database ->
2020-12-03 09:15:18 +00:00
val configuration = params . configuration
if ( configuration != null ) {
database . storeConfiguration ( packageName , token , configuration )
2020-10-12 19:25:34 +00:00
} else {
2020-12-03 09:15:18 +00:00
database . getOrCreateTokenId ( packageName , token )
2020-10-12 19:25:34 +00:00
}
2020-09-27 09:33:02 +00:00
}
2020-10-12 19:25:34 +00:00
if ( tid == null ) {
2020-12-03 09:15:18 +00:00
Log . w ( TAG , " Unknown token without configuration: $packageName / $token " )
2020-10-12 19:25:34 +00:00
try {
params . callback . onResult ( Status . INTERNAL _ERROR )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-12-27 16:09:51 +00:00
return @launchSafely
2020-09-27 09:33:02 +00:00
}
2020-08-05 12:17:29 +00:00
ExposureDatabase . with ( context ) { database ->
2020-09-06 12:59:41 +00:00
val start = System . currentTimeMillis ( )
2020-08-04 09:42:15 +00:00
// keys
2020-09-27 09:33:02 +00:00
params . keys ?. let { database . batchStoreSingleDiagnosisKey ( tid , it ) }
2020-08-03 16:07:06 +00:00
2020-08-04 09:42:15 +00:00
var keys = params . keys ?. size ?: 0
2020-09-27 09:33:02 +00:00
// Key files
val todoKeyFiles = arrayListOf < Pair < File , ByteArray > > ( )
2020-08-04 09:42:15 +00:00
for ( file in params . keyFiles . orEmpty ( ) ) {
try {
2020-09-27 09:33:02 +00:00
val cacheFile = File ( context . cacheDir , " en-keyfile- ${System.currentTimeMillis()} - ${Random.nextInt()} .zip " )
ParcelFileDescriptor . AutoCloseInputStream ( file ) . use { it . copyToFile ( cacheFile ) }
val hash = MessageDigest . getInstance ( " SHA-256 " ) . digest ( cacheFile )
val storedKeys = database . storeDiagnosisFileUsed ( tid , hash )
if ( storedKeys != null ) {
keys += storedKeys . toInt ( )
cacheFile . delete ( )
} else {
todoKeyFiles . add ( cacheFile to hash )
2020-08-04 09:42:15 +00:00
}
} catch ( e : Exception ) {
Log . w ( TAG , " Failed parsing file " , e )
2020-08-03 16:07:06 +00:00
}
}
2020-11-18 16:06:29 +00:00
params . keyFileSupplier ?. let { keyFileSupplier ->
Log . d ( TAG , " Using key file supplier " )
2020-12-27 16:09:51 +00:00
try {
while ( keyFileSupplier . isAvailable && keyFileSupplier . hasNext ( ) ) {
2021-01-06 11:11:57 +00:00
withContext ( Dispatchers . IO ) {
try {
val cacheFile = File ( context . cacheDir , " en-keyfile- ${System.currentTimeMillis()} - ${Random.nextLong()} .zip " )
ParcelFileDescriptor . AutoCloseInputStream ( keyFileSupplier . next ( ) ) . use { it . copyToFile ( cacheFile ) }
val hash = MessageDigest . getInstance ( " SHA-256 " ) . digest ( cacheFile )
val storedKeys = database . storeDiagnosisFileUsed ( tid , hash )
if ( storedKeys != null ) {
keys += storedKeys . toInt ( )
cacheFile . delete ( )
} else {
todoKeyFiles . add ( cacheFile to hash )
}
} catch ( e : Exception ) {
Log . w ( TAG , " Failed parsing file " , e )
2020-12-27 16:09:51 +00:00
}
2020-11-18 16:06:29 +00:00
}
}
2020-12-27 16:09:51 +00:00
} catch ( e : Exception ) {
Log . w ( TAG , " Disconnected from key file supplier " , e )
2020-11-18 16:06:29 +00:00
}
}
2020-08-03 16:07:06 +00:00
2020-09-27 09:33:02 +00:00
if ( todoKeyFiles . size > 0 ) {
2020-12-01 15:42:18 +00:00
val time = ( System . currentTimeMillis ( ) - start ) . coerceAtLeast ( 1 ) . toDouble ( ) / 1000.0
2020-12-03 09:15:18 +00:00
Log . d ( TAG , " $packageName / $token processed $keys keys ( ${todoKeyFiles.size} files pending) in ${time} s -> ${(keys.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s " )
2020-09-27 09:33:02 +00:00
}
2020-08-24 08:12:49 +00:00
2020-08-04 09:42:15 +00:00
Handler ( Looper . getMainLooper ( ) ) . post {
try {
params . callback . onResult ( Status . SUCCESS )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
}
2020-09-27 09:33:02 +00:00
var newKeys = if ( params . keys != null ) database . finishSingleMatching ( tid ) else 0
for ( ( cacheFile , hash ) in todoKeyFiles ) {
2021-01-06 11:11:57 +00:00
withContext ( Dispatchers . IO ) {
2021-02-09 17:42:21 +00:00
if ( backendPublicKey != null && ! verifyKeyFile ( cacheFile ) ) {
Log . w ( TAG , " Skipping non-verified key file " )
return @withContext
2021-02-09 14:49:21 +00:00
}
2021-01-06 11:11:57 +00:00
try {
ZipFile ( cacheFile ) . use { zip ->
for ( entry in zip . entries ( ) ) {
if ( entry . name == " export.bin " ) {
val stream = zip . getInputStream ( entry )
val prefix = ByteArray ( 16 )
var totalBytesRead = 0
var bytesRead = 0
while ( bytesRead != - 1 && totalBytesRead < prefix . size ) {
bytesRead = stream . read ( prefix , totalBytesRead , prefix . size - totalBytesRead )
if ( bytesRead > 0 ) {
totalBytesRead += bytesRead
}
}
if ( totalBytesRead == prefix . size && String ( prefix ) . trim ( ) == " EK Export v1 " ) {
val export = TemporaryExposureKeyExport . ADAPTER . decode ( stream )
database . finishFileMatching ( tid , hash , export . end _timestamp ?. let { it * 1000 }
?: System . currentTimeMillis ( ) , export . keys . map { it . toKey ( ) } , export . revised _keys . map { it . toKey ( ) } )
keys += export . keys . size + export . revised _keys . size
newKeys += export . keys . size
} else {
Log . d ( TAG , " export.bin had invalid prefix " )
}
2020-09-27 09:33:02 +00:00
}
}
}
2021-01-06 11:11:57 +00:00
cacheFile . delete ( )
} catch ( e : Exception ) {
Log . w ( TAG , " Failed parsing file " , e )
2020-09-27 09:33:02 +00:00
}
}
}
2020-11-18 16:06:29 +00:00
val time = ( System . currentTimeMillis ( ) - start ) . coerceAtLeast ( 1 ) . toDouble ( ) / 1000.0
2020-12-03 09:15:18 +00:00
Log . d ( TAG , " $packageName / $token processed $keys keys ( $newKeys new) in ${time} s -> ${(keys.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s " )
2020-09-27 09:33:02 +00:00
database . noteAppAction ( packageName , " provideDiagnosisKeys " , JSONObject ( ) . apply {
2020-12-03 09:15:18 +00:00
put ( " request_token " , token )
2020-09-27 09:33:02 +00:00
put ( " request_keys_size " , params . keys ?. size )
put ( " request_keyFiles_size " , params . keyFiles ?. size )
put ( " request_keys_count " , keys )
} . toString ( ) )
2020-12-12 11:40:48 +00:00
if ( ! database . isAppAuthorized ( packageName ) ) {
// Not sending results via broadcast if app not authorized
Log . d ( TAG , " $packageName not yet authorized " )
return @with
}
2020-12-03 09:15:18 +00:00
val exposureSummary = buildExposureSummary ( token )
2020-08-04 09:42:15 +00:00
2020-08-03 16:07:06 +00:00
try {
2020-10-09 11:54:32 +00:00
val intent = if ( exposureSummary . matchedKeyCount > 0 ) {
2020-12-20 10:46:33 +00:00
Intent ( ACTION _EXPOSURE _STATE _UPDATED )
2020-10-09 11:54:32 +00:00
} else {
Intent ( ACTION _EXPOSURE _NOT _FOUND )
}
2020-12-20 10:46:33 +00:00
if ( token != TOKEN _A ) {
intent . putExtra ( EXTRA _EXPOSURE _SUMMARY , exposureSummary )
}
2020-12-03 09:15:18 +00:00
intent . putExtra ( EXTRA _TOKEN , token )
2020-08-04 09:42:15 +00:00
intent . `package` = packageName
Log . d ( TAG , " Sending $intent " )
2020-09-03 22:13:11 +00:00
context . sendOrderedBroadcast ( intent , null )
2020-08-03 16:07:06 +00:00
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
}
}
}
2021-02-09 14:49:21 +00:00
private fun verifyKeyFile ( file : File ) : Boolean {
try {
ZipFile ( file ) . use { zip ->
var dataEntry : ZipEntry ? = null
var sigEntry : ZipEntry ? = null
for ( entry in zip . entries ( ) ) {
when ( entry . name ) {
2021-02-09 17:39:34 +00:00
" export.bin " ->
if ( dataEntry != null ) {
throw Exception ( " Zip archive contains more than one 'export.bin' entry " )
} else {
dataEntry = entry
}
" export.sig " ->
if ( sigEntry != null ) {
throw Exception ( " Zip archive contains more than one 'export.sig' entry " )
} else {
sigEntry = entry
}
2021-02-09 14:49:21 +00:00
else -> throw Exception ( " Unexpected entry in zip archive: ${entry.name} " )
}
}
when {
dataEntry == null -> throw Exception ( " Zip archive does not contain 'export.bin' " )
sigEntry == null -> throw Exception ( " Zip archive does not contain 'export.sin' " )
}
val sigStream = zip . getInputStream ( sigEntry )
val sigList = TEKSignatureList . ADAPTER . decode ( sigStream )
for ( sig in sigList . signatures ) {
Log . d ( TAG , " Verifying signature ${sig.batch_num} / ${sig.batch_size} " )
val sigInfo = sig . signature _info ?: throw Exception ( " Signature information is missing " )
Log . d ( TAG , " Signature info: algo= ${sigInfo.signature_algorithm} key={id= ${sigInfo.verification_key_id} , version= ${sigInfo.verification_key_version} } " )
val signature = sig . signature ?. toByteArray ( ) ?: throw Exception ( " Signature contents is missing " )
2021-11-02 13:52:04 +00:00
val sigVerifier = ( sigAlgoForOid . get ( sigInfo . signature _algorithm ) ?: throw Exception ( " Signature algorithm not supported: ${sigInfo.signature_algorithm} " ) ) ( )
2021-02-09 17:42:21 +00:00
sigVerifier . initVerify ( backendPublicKey )
2021-02-09 14:49:21 +00:00
val stream = zip . getInputStream ( dataEntry )
val buf = ByteArray ( 1024 )
var nbRead = 0
while ( nbRead != - 1 ) {
nbRead = stream . read ( buf )
if ( nbRead > 0 ) {
sigVerifier . update ( buf , 0 , nbRead )
}
}
if ( ! sigVerifier . verify ( signature ) ) {
throw Exception ( " Signature does not verify " )
}
}
}
} catch ( e : Exception ) {
Log . w ( TAG , " Key file verification failed: " + e . message )
return false
}
Log . i ( TAG , " Key file verification successful " )
return true
}
2020-10-12 19:25:34 +00:00
override fun getExposureSummary ( params : GetExposureSummaryParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-10-12 19:25:34 +00:00
val response = buildExposureSummary ( params . token )
ExposureDatabase . with ( context ) { database ->
database . noteAppAction ( packageName , " getExposureSummary " , JSONObject ( ) . apply {
put ( " request_token " , params . token )
put ( " response_days_since " , response . daysSinceLastExposure )
put ( " response_matched_keys " , response . matchedKeyCount )
put ( " response_max_risk " , response . maximumRiskScore )
put ( " response_attenuation_durations " , JSONArray ( ) . apply {
response . attenuationDurationsInMinutes . forEach { put ( it ) }
} )
put ( " response_summation_risk " , response . summationRiskScore )
} . toString ( ) )
}
try {
params . callback . onResult ( Status . SUCCESS , response )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
2020-08-03 16:07:06 +00:00
}
}
2020-10-12 19:25:34 +00:00
}
2020-08-03 16:07:06 +00:00
2020-10-12 19:25:34 +00:00
override fun getExposureInformation ( params : GetExposureInformationParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-10-12 19:25:34 +00:00
ExposureDatabase . with ( context ) { database ->
val pair = database . loadConfiguration ( packageName , params . token )
2020-12-12 11:40:48 +00:00
val response = if ( pair != null && database . isAppAuthorized ( packageName ) ) {
2020-10-12 19:25:34 +00:00
database . findAllMeasuredExposures ( pair . first ) . merge ( ) . map {
2020-11-18 16:06:29 +00:00
it . toExposureInformation ( pair . second . orDefault ( ) )
2020-10-12 19:25:34 +00:00
}
} else {
2020-12-12 11:40:48 +00:00
// Not providing information if app not authorized
Log . d ( TAG , " $packageName not yet authorized " )
2020-10-12 19:25:34 +00:00
emptyList ( )
}
database . noteAppAction ( packageName , " getExposureInformation " , JSONObject ( ) . apply {
put ( " request_token " , params . token )
put ( " response_size " , response . size )
} . toString ( ) )
try {
params . callback . onResult ( Status . SUCCESS , response )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
}
2020-08-03 16:07:06 +00:00
}
}
2020-08-11 20:34:04 +00:00
2020-11-18 16:06:29 +00:00
private fun ScanInstance . Builder . apply ( subExposure : MergedSubExposure ) : ScanInstance . Builder {
return this
2021-01-06 11:00:59 +00:00
. setSecondsSinceLastScan ( subExposure . duration . coerceAtMost ( 5 * 60 ) . toInt ( ) )
2020-11-18 16:06:29 +00:00
. setMinAttenuationDb ( subExposure . attenuation ) // FIXME: We use the average for both, because we don't store the minimum attenuation yet
. setTypicalAttenuationDb ( subExposure . attenuation )
}
private fun List < MergedSubExposure > . toScanInstances ( ) : List < ScanInstance > {
val res = arrayListOf < ScanInstance > ( )
for ( subExposure in this ) {
res . add ( ScanInstance . Builder ( ) . apply ( subExposure ) . build ( ) )
if ( subExposure . duration > 5 * 60 * 1000L ) {
2021-01-06 11:00:59 +00:00
res . add ( ScanInstance . Builder ( ) . apply ( subExposure ) . setSecondsSinceLastScan ( ( subExposure . duration - 5 * 60 ) . coerceAtMost ( 5 * 60 ) . toInt ( ) ) . build ( ) )
2020-11-18 16:06:29 +00:00
}
}
return res
}
private fun DiagnosisKeysDataMapping ?. orDefault ( ) = this ?: DiagnosisKeysDataMapping ( )
private suspend fun getExposureWindowsInternal ( token : String = TOKEN _A ) : List < ExposureWindow > {
val ( exposures , mapping ) = ExposureDatabase . with ( context ) { database ->
val triple = database . loadConfiguration ( packageName , token )
2020-12-12 11:40:48 +00:00
if ( triple != null && database . isAppAuthorized ( packageName ) ) {
2020-11-18 16:06:29 +00:00
database . findAllMeasuredExposures ( triple . first ) . merge ( ) to triple . third . orDefault ( )
} else {
2020-12-12 11:40:48 +00:00
// Not providing windows if app not authorized
Log . d ( TAG , " $packageName not yet authorized " )
2020-11-18 16:06:29 +00:00
emptyList < MergedExposure > ( ) to DiagnosisKeysDataMapping ( )
}
}
return exposures . map {
val infectiousness =
if ( it . key . daysSinceOnsetOfSymptoms == DAYS _SINCE _ONSET _OF _SYMPTOMS _UNKNOWN )
mapping . infectiousnessWhenDaysSinceOnsetMissing
else
mapping . daysSinceOnsetToInfectiousness [ it . key . daysSinceOnsetOfSymptoms ]
?: Infectiousness . NONE
val reportType =
if ( it . key . reportType == ReportType . UNKNOWN )
mapping . reportTypeWhenMissing
else
it . key . reportType
ExposureWindow . Builder ( )
. setCalibrationConfidence ( it . confidence )
. setDateMillisSinceEpoch ( it . key . rollingStartIntervalNumber . toLong ( ) * ROLLING _WINDOW _LENGTH _MS )
. setInfectiousness ( infectiousness )
. setReportType ( reportType )
. setScanInstances ( it . subs . toScanInstances ( ) )
. build ( )
}
}
2020-09-27 09:33:02 +00:00
override fun getExposureWindows ( params : GetExposureWindowsParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-11-18 16:06:29 +00:00
val response = getExposureWindowsInternal ( params . token ?: TOKEN _A )
ExposureDatabase . with ( context ) { database ->
database . noteAppAction ( packageName , " getExposureWindows " , JSONObject ( ) . apply {
put ( " request_token " , params . token )
put ( " response_size " , response . size )
} . toString ( ) )
}
2020-12-10 15:53:42 +00:00
try {
params . callback . onResult ( Status . SUCCESS , response )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-11-18 16:06:29 +00:00
}
}
private fun DailySummariesConfig . bucketFor ( attenuation : Int ) : Int {
if ( attenuation < attenuationBucketThresholdDb [ 0 ] ) return 0
if ( attenuation < attenuationBucketThresholdDb [ 1 ] ) return 1
if ( attenuation < attenuationBucketThresholdDb [ 2 ] ) return 2
return 3
}
private fun DailySummariesConfig . weightedDurationFor ( attenuation : Int , seconds : Int ) : Double {
return attenuationBucketWeights [ bucketFor ( attenuation ) ] * seconds
}
private fun Collection < DailySummary . ExposureSummaryData > . sum ( ) : DailySummary . ExposureSummaryData {
return DailySummary . ExposureSummaryData ( map { it . maximumScore } . maxOrNull ( )
?: 0.0 , sumByDouble { it . scoreSum } , sumByDouble { it . weightedDurationSum } )
2020-09-27 09:33:02 +00:00
}
override fun getDailySummaries ( params : GetDailySummariesParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-11-18 16:06:29 +00:00
val response = getExposureWindowsInternal ( ) . groupBy { it . dateMillisSinceEpoch } . map {
val map = arrayListOf < DailySummary . ExposureSummaryData > ( )
for ( i in 0 until ReportType . VALUES ) {
map [ i ] = DailySummary . ExposureSummaryData ( 0.0 , 0.0 , 0.0 )
}
for ( entry in it . value . groupBy { it . reportType } ) {
for ( window in entry . value ) {
val weightedDuration = window . scanInstances . map { params . config . weightedDurationFor ( it . typicalAttenuationDb , it . secondsSinceLastScan ) } . sum ( )
val score = ( params . config . reportTypeWeights [ window . reportType ] ?: 1.0 ) *
( params . config . infectiousnessWeights [ window . infectiousness ] ?: 1.0 ) *
weightedDuration
if ( score >= params . config . minimumWindowScore ) {
map [ entry . key ] = DailySummary . ExposureSummaryData ( max ( map [ entry . key ] . maximumScore , score ) , map [ entry . key ] . scoreSum + score , map [ entry . key ] . weightedDurationSum + weightedDuration )
}
}
}
DailySummary ( ( it . key / ( 1000L * 60 * 60 * 24 ) ) . toInt ( ) , map , map . sum ( ) )
}
ExposureDatabase . with ( context ) { database ->
database . noteAppAction ( packageName , " getDailySummaries " , JSONObject ( ) . apply {
put ( " response_size " , response . size )
} . toString ( ) )
}
2020-12-10 15:53:42 +00:00
try {
params . callback . onResult ( Status . SUCCESS , response )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-11-18 16:06:29 +00:00
}
2020-09-27 09:33:02 +00:00
}
override fun setDiagnosisKeysDataMapping ( params : SetDiagnosisKeysDataMappingParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-11-18 16:06:29 +00:00
ExposureDatabase . with ( context ) { database ->
database . storeConfiguration ( packageName , TOKEN _A , params . mapping )
database . noteAppAction ( packageName , " setDiagnosisKeysDataMapping " )
}
2020-12-10 15:53:42 +00:00
try {
params . callback . onResult ( Status . SUCCESS )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-11-18 16:06:29 +00:00
}
2020-09-27 09:33:02 +00:00
}
override fun getDiagnosisKeysDataMapping ( params : GetDiagnosisKeysDataMappingParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-11-18 16:06:29 +00:00
val mapping = ExposureDatabase . with ( context ) { database ->
val triple = database . loadConfiguration ( packageName , TOKEN _A )
database . noteAppAction ( packageName , " getDiagnosisKeysDataMapping " )
triple ?. third
}
2020-12-10 15:53:42 +00:00
try {
params . callback . onResult ( Status . SUCCESS , mapping . orDefault ( ) )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-11-18 16:06:29 +00:00
}
}
override fun getPackageConfiguration ( params : GetPackageConfigurationParams ) {
Log . w ( TAG , " Not yet implemented: getPackageConfiguration " )
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2020-11-18 16:06:29 +00:00
ExposureDatabase . with ( context ) { database ->
database . noteAppAction ( packageName , " getPackageConfiguration " )
}
2020-12-10 15:53:42 +00:00
try {
params . callback . onResult ( Status . SUCCESS , PackageConfiguration . PackageConfigurationBuilder ( ) . setValues ( Bundle . EMPTY ) . build ( ) )
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-11-18 16:06:29 +00:00
}
}
override fun getStatus ( params : GetStatusParams ) {
2020-12-27 16:09:51 +00:00
lifecycleScope . launchSafely {
2021-04-28 18:08:19 +00:00
val isAuthorized = ExposureDatabase . with ( context ) { database ->
2020-11-18 16:06:29 +00:00
database . noteAppAction ( packageName , " getStatus " )
2021-04-28 18:08:19 +00:00
database . isAppAuthorized ( packageName )
}
val flags = hashSetOf < ExposureNotificationStatus > ( )
val adapter = BluetoothAdapter . getDefaultAdapter ( )
if ( adapter == null || ! adapter . isEnabled ) {
flags . add ( ExposureNotificationStatus . BLUETOOTH _DISABLED )
flags . add ( ExposureNotificationStatus . BLUETOOTH _SUPPORT _UNKNOWN )
} else if ( Build . VERSION . SDK _INT < 21 ) {
flags . add ( ExposureNotificationStatus . EN _NOT _SUPPORT )
} else if ( adapter . bluetoothLeScanner == null ) {
flags . add ( ExposureNotificationStatus . HW _NOT _SUPPORT )
}
if ( ! LocationManagerCompat . isLocationEnabled ( context . getSystemService ( Context . LOCATION _SERVICE ) as LocationManager ) ) {
flags . add ( ExposureNotificationStatus . LOCATION _DISABLED )
}
if ( !is Authorized ) {
flags . add ( ExposureNotificationStatus . NO _CONSENT )
}
if ( isAuthorized && ExposurePreferences ( context ) . enabled ) {
flags . add ( ExposureNotificationStatus . ACTIVATED )
} else {
flags . add ( ExposureNotificationStatus . INACTIVATED )
2020-11-18 16:06:29 +00:00
}
2020-12-10 15:53:42 +00:00
try {
2021-04-28 18:08:19 +00:00
params . callback . onResult ( Status . SUCCESS , ExposureNotificationStatus . setToFlags ( flags ) )
2020-12-10 15:53:42 +00:00
} catch ( e : Exception ) {
Log . w ( TAG , " Callback failed " , e )
}
2020-11-18 16:06:29 +00:00
}
2020-09-27 09:33:02 +00:00
}
2021-02-21 22:24:22 +00:00
override fun requestPreAuthorizedTemporaryExposureKeyHistory ( params : RequestPreAuthorizedTemporaryExposureKeyHistoryParams ) {
// TODO: Proper implementation
lifecycleScope . launchSafely {
params . callback . onResult ( Status . CANCELED )
}
}
override fun requestPreAuthorizedTemporaryExposureKeyRelease ( params : RequestPreAuthorizedTemporaryExposureKeyReleaseParams ) {
// TODO: Proper implementation
lifecycleScope . launchSafely {
2021-05-13 12:16:37 +00:00
params . callback . onResult ( Status ( FAILED _KEY _RELEASE _NOT _PREAUTHORIZED ) )
2021-02-21 22:24:22 +00:00
}
}
2022-01-18 17:51:21 +00:00
override fun onTransact ( code : Int , data : Parcel , reply : Parcel ? , flags : Int ) : Boolean = warnOnTransactionIssues ( code , reply , flags ) { super . onTransact ( code , data , reply , flags ) }
2020-10-12 19:25:34 +00:00
companion object {
private val tempGrantedPermissions : MutableSet < Pair < String , String > > = hashSetOf ( )
}
2020-08-03 16:07:06 +00:00
}