Skip to content

Commit

Permalink
Gaen 772 (#917)
Browse files Browse the repository at this point in the history
* Schedule chaff background task.

* Obfuscate data for chaff request (iOS).

* Set X-Chaff header.

* Implement JS layer chaff request.

* Implement chaff request for verification code endpoint.

* Address validation warnings/errors.

* Post correct notification for chaff request.

* Added 24 hour check to chaff with random flip.

* Work/gaen 790 chaff requests (#918)

* GAEN-790 Chaff Requests

Schedule the work on Application launch
Add the correct EventSender function to trigger the ts code
Remove reusable code for encoding to a separate object file
Add the new worker that schedules the periodic work

* GAEN-790 Chaff Requests

Add a ChaffManager class to handle specific timing for firing requests.

* GAEN-790 Chaff

Remove the random z variable

* GAEN-790 Chaff Requests

Attach a react context initializer to trigger Chaff
Add a Config class for the ChaffManager to handle some QA needs

* Added react debug menu for Android only chaff fast request call.

* Added Android check to debug menu.

Co-authored-by: lundjrl <jrlbidamin@gmail.com>

* GAEN-772 Chaff

Update the build.yml file with a new NDK version

* GAEN-772 Chaff

Apply Kotlin Format

* GAEN-772 Chaff

Adding another maven url

* GAEN-772 Chaff

Reformat Java Files

* GAEN-772 Chaff

Updated NDK version in the gradle file

* GAEN-772 Chaff

More import format updates

* GAEN-772 Chaff

The formatter is not working correctly so I am manually adjusting per each error I get

* GAEN-772 Chaff

Import formatting

* Removed unnecessary print statement.

* Should fix ios action failing.

* Added default case to fix Switch must be exhaustive error.

* Fixing debug test post push actions fail.

* Fixing debug test post push actions fail.

* Fixing debug test post push actions fail.

Co-authored-by: Matt Buckley <matt@smalldevshop.co>
Co-authored-by: Kyle Wolff <kyle.a.wolff87@gmail.com>
  • Loading branch information
3 people committed Aug 24, 2021
1 parent 6ef9652 commit 8be3404
Show file tree
Hide file tree
Showing 28 changed files with 804 additions and 141 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:

- name: Set up NDK
# This is not the usual sdkmanager path, but it's what Github Actions uses https://github.com/actions/virtual-environments/issues/60
run: sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;21.0.6113669"
run: sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;21.4.7075529"

- name: Setup env
run: ./bin/set_ha_env_github.sh
Expand Down
11 changes: 10 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,17 @@ def enableHermes = project.ext.react.get("enableHermes", false)

android {
compileSdkVersion rootProject.ext.compileSdkVersion
ndkVersion "21.0.6113669"
ndkVersion '21.4.7075529'

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}

defaultConfig {
applicationId project.env.get("ANDROID_APPLICATION_ID")
minSdkVersion rootProject.ext.minSdkVersion
Expand Down Expand Up @@ -297,6 +301,11 @@ dependencies {
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}

testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.1"
testImplementation "io.mockk:mockk:1.12.0"
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'org.amshove.kluent:kluent-android:1.61'
}

// Run this once to be able to run the application with BUCK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.ReactContext;
import com.facebook.soloader.SoLoader;
import com.jakewharton.threetenabp.AndroidThreeTen;
import io.realm.Realm;
Expand All @@ -17,6 +18,7 @@
import java.util.List;
import java.util.Set;
import org.pathcheck.covidsafepaths.bridge.ExposureNotificationsPackage;
import org.pathcheck.covidsafepaths.exposurenotifications.chaff.ChaffRequestWorker;
import org.pathcheck.covidsafepaths.exposurenotifications.storage.RealmSecureStorageBte;

public class MainApplication extends Application implements ReactApplication {
Expand All @@ -36,14 +38,15 @@ protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new ExposureNotificationsPackage());


return packages;
}

@Override
protected String getJSMainModuleName() {
return "index";
}


};

@Override
Expand All @@ -58,8 +61,18 @@ public void onCreate() {
AndroidThreeTen.init(this);
SoLoader.init(this, /* native exopackage */ false);
initializeRealm();
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
initializeBugsnag();

Context application = this;

getReactNativeHost().getReactInstanceManager()
.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext context) {
ChaffRequestWorker.scheduleWork(application);
}
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ object EventSender {
private const val BLUETOOTH_STATUS_CHANGED_EVENT = "onBluetoothStatusUpdated"
private const val LOCATION_STATUS_CHANGED_EVENT = "onLocationStatusUpdated"
private const val EN_EXPOSURE_RECORD_UPDATED_CHANGED_EVENT = "onExposureRecordUpdated"
private const val CHAFF_REQUEST_TRIGGERED = "onChaffRequestTriggered"

fun sendExposureNotificationStatusChanged(
reactContext: ReactContext?,
Expand Down Expand Up @@ -62,4 +63,10 @@ object EventSender {
?.getJSModule(RCTDeviceEventEmitter::class.java)
?.emit(EN_EXPOSURE_RECORD_UPDATED_CHANGED_EVENT, exposureJson)
}

fun sendChaffRequest(reactContext: ReactContext?) {
reactContext
?.getJSModule(RCTDeviceEventEmitter::class.java)
?.emit(CHAFF_REQUEST_TRIGGERED, null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package org.pathcheck.covidsafepaths.exposurenotifications.chaff

import android.content.Context
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.facebook.react.bridge.WritableArray
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.security.SecureRandom
import java.util.concurrent.TimeUnit
import org.pathcheck.covidsafepaths.exposurenotifications.dto.RNExposureKey
import org.pathcheck.covidsafepaths.exposurenotifications.utils.TimeProvider
import org.pathcheck.covidsafepaths.exposurenotifications.utils.TimeProviderImpl
import org.pathcheck.covidsafepaths.exposurenotifications.utils.Util

class ChaffManager private constructor(
context: Context,
private val timeProvider: TimeProvider,
private val secureRandom: SecureRandom
) {

private val sharedPreferences = context.getSharedPreferences(CHAFF_SHARED_PREF, Context.MODE_PRIVATE)
private val currentTimeMillis get() = timeProvider.currentTimeInMillis
private val currentTimeInHours get() = TimeUnit.MILLISECONDS.toHours(currentTimeMillis)
private val lastFiredEventInHours: Long
get() {
val previousFiredTime = sharedPreferences.getLong(REQUEST_FIRED, 0L)
return TimeUnit.MILLISECONDS.toHours(previousFiredTime)
}

private var config: Config = Config()

fun setConfiguration(config: Config) {
this.config = config
}

fun shouldFire(): Boolean {
if (config.makeProbability100Percent) {
return true
}

return secureRandom.nextDouble() < EXECUTION_PROBABILITY && hasBeen24Hours()
}

fun save(chaffKeys: List<RNExposureKey>?) {
saveTime()
convertTemporaryKeysToJson(chaffKeys).also { json ->
sharedPreferences
.edit()
.putString(CHAFF_JSON, json)
.apply()
}
}

fun getChaffKeys(): WritableArray? = convertToWriteableArray(getSavedRNExposureKeys())

@VisibleForTesting
fun getSavedRNExposureKeys(): List<RNExposureKey>? {
return sharedPreferences.getString(CHAFF_JSON, "").let { json ->

if (json.isNullOrEmpty()) {
return null
}
val gson = Gson()
val listType = TypeToken.getParameterized(List::class.java, RNExposureKey::class.java).type
gson.fromJson(json, listType)
}
}

fun getRepeatWorkerIntervalInMinutes(): Long {
return config.repeatIntervalInMinutes
}

private fun convertTemporaryKeysToJson(rnExposureKeys: List<RNExposureKey>?): String {
val gson = Gson()
return gson.toJson(rnExposureKeys).also {
Log.d("Chaff Json Log", it)
}
}

private fun saveTime() {
sharedPreferences
.edit()
.putLong(REQUEST_FIRED, currentTimeMillis)
.apply()
}

private fun hasBeen24Hours(): Boolean {
return lastFiredEventInHours == 0L ||
currentTimeInHours - lastFiredEventInHours >= TWENTY_FOUR_HOURS
}

private fun convertToWriteableArray(exposureKeys: List<RNExposureKey>?): WritableArray? {
return Util.convertListToWritableArray(exposureKeys)
}

companion object {
private const val CHAFF_SHARED_PREF = "ChaffManagerSharedPref"
private const val REQUEST_FIRED = "requestFired"
private const val CHAFF_JSON = "chaffJson"
private const val EXECUTION_PROBABILITY = 1.0 / 12.0
private const val TWENTY_FOUR_HOURS = 24

private var chaffManager: ChaffManager? = null

@JvmStatic
@JvmOverloads
fun getInstance(
context: Context,
timeProvider: TimeProvider = TimeProviderImpl,
secureRandom: SecureRandom = SecureRandom()
): ChaffManager {
return chaffManager ?: ChaffManager(context, timeProvider, secureRandom).also {
chaffManager = it
}
}

@JvmStatic
@VisibleForTesting
fun createChaffManager(
context: Context,
timeProvider: TimeProvider,
secureRandom: SecureRandom
) = ChaffManager(context, timeProvider, secureRandom)
}

data class Config(
val repeatIntervalInMinutes: Long = FOUR_HOURS_IN_MINUTES,
val makeProbability100Percent: Boolean = false
) {

companion object {
const val FIFTEEN_MINUTES = 15L
const val FOUR_HOURS_IN_MINUTES = 240L
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.pathcheck.covidsafepaths.exposurenotifications.chaff

import android.content.Context
import android.util.Log
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ListenableWorker
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import com.google.common.util.concurrent.FluentFuture
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.TimeUnit
import org.pathcheck.covidsafepaths.MainApplication
import org.pathcheck.covidsafepaths.bridge.EventSender
import org.pathcheck.covidsafepaths.exposurenotifications.ExposureNotificationClientWrapper
import org.pathcheck.covidsafepaths.exposurenotifications.common.AppExecutors
import org.pathcheck.covidsafepaths.exposurenotifications.dto.RNExposureKey
import org.pathcheck.covidsafepaths.helpers.DiagnosisKeyEncoding

class ChaffRequestWorker(
application: Context,
workerParams: WorkerParameters
) : ListenableWorker(application, workerParams) {

companion object {
private const val TAG = "ChaffRequests"

@JvmStatic
fun scheduleWork(context: Context) {
val chaffManager = ChaffManager.getInstance(context)

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val request = PeriodicWorkRequest.Builder(
ChaffRequestWorker::class.java,
chaffManager.getRepeatWorkerIntervalInMinutes(),
TimeUnit.MINUTES
)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, PeriodicWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
.build()

WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
}
}

private val app = application as? MainApplication
private val reactContext = app?.reactNativeHost?.reactInstanceManager?.currentReactContext
private val chaffManager = ChaffManager.getInstance(application)

override fun startWork(): ListenableFuture<Result> {
val wrapper = ExposureNotificationClientWrapper.get(app)

if (!chaffManager.shouldFire()) {
return Futures.immediateFuture(Result.success())
}

return FluentFuture.from(wrapper.requestPermissionToGetExposureKeys(reactContext))
.transform(
{ exposureKeys ->
if (exposureKeys != null) {
chaffManager.save(encodeKeys(exposureKeys))
EventSender.sendChaffRequest(reactContext)
Result.success()
} else {
Result.failure()
}
},
AppExecutors.getBackgroundExecutor()
)
.catching(
Exception::class.java,
{ exception ->
Log.e(
"ChaffRequestWorker",
"Failure to update app state (tokens, etc) from exposure summary.", exception
)
Result.failure()
},
AppExecutors.getLightweightExecutor()
)
}

private fun encodeKeys(exposureKeys: List<TemporaryExposureKey>?): List<RNExposureKey>? {
return exposureKeys?.run {
DiagnosisKeyEncoding.encodeDiagnosisKeys(exposureKeys, true)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.pathcheck.covidsafepaths.exposurenotifications.dto;

import androidx.annotation.VisibleForTesting;

@SuppressWarnings({"FieldCanBeLocal", "unused"})
public class RNExposureKey {
private String key;
@VisibleForTesting
public String key;
private int rollingPeriod;
private int rollingStartNumber;
private int transmissionRisk;
Expand Down
Loading

0 comments on commit 8be3404

Please sign in to comment.