Skip to content

Commit

Permalink
Lower minSdkVersion to 21
Browse files Browse the repository at this point in the history
This is a first look at what would be required to lower the
minSdkVersion to 21, thereby picking up devices with
Android 5.0 and 5.1 (<5% in Switzerland, approx 15% worldwide).

API level 21 has good support for Bluetooth LE. What it is missing
is some stuff related to power management, and support from the
AndroidX libraries. The solution was to back-port the
EncryptedSharedPreferences.java file from AndroidX, as it alone
did not depend on much API 23 stuff, and then adapt the key
creation stuff to do the same thing as MasterKey did, but
to fallback to another way of making the master key that works in
API 21.

This change has been tested from one Android 5.1 phone to another
Android 6 phone, and contact tracing worked.

Fixes DP-3T#16.
  • Loading branch information
Jeff R. Allen committed May 5, 2020
1 parent d0ced8f commit 5d07255
Show file tree
Hide file tree
Showing 11 changed files with 727 additions and 24 deletions.
2 changes: 1 addition & 1 deletion calibration-app/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ android {

defaultConfig {
applicationId "org.dpppt.android.calibration"
minSdkVersion 23
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "0.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import android.content.IntentFilter;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
Expand Down Expand Up @@ -393,7 +394,13 @@ private SpannableString formatStatusString(TracingStatus status) {
for (TracingStatus.ErrorState error : errors) {
builder.append("\n").append(error.toString());
}
builder.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.red, null)),
int color;
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
color = getResources().getColor(R.color.red, null);
} else {
color = getResources().getColor(R.color.red);
}
builder.setSpan(new ForegroundColorSpan(color),
start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
});
}

private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}

private void loadHandshakes(boolean raw) {
handshakeList.setText("Loading...");
new Database(getContext()).getHandshakes(response -> {
Expand All @@ -67,9 +78,8 @@ private void loadHandshakes(boolean raw) {
for (Handshake handShake : response) {
stringBuilder.append(sdf.format(new Date(handShake.getTimestamp())));
stringBuilder.append(" ");
stringBuilder
.append(new String(handShake.getEphId().getData()).substring(0, 10));
stringBuilder.append("...");
stringBuilder.append(bytesToHex(handShake.getEphId().getData()).substring(0,8));
stringBuilder.append(" ");
stringBuilder.append(" TxPowerLevel: ");
stringBuilder.append(handShake.getTxPowerLevel());
stringBuilder.append(" RSSI:");
Expand Down Expand Up @@ -102,7 +112,7 @@ private List<HandshakeInterval> mergeHandshakes(List<Handshake> handshakes) {
for (int i = 0; i < 4; i++) {
head[i] = handshake.getEphId().getData()[i];
}
String identifier = new String(head);
String identifier = bytesToHex(head);
if (!groupedHandshakes.containsKey(identifier)) {
groupedHandshakes.put(identifier, new ArrayList<>());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.PowerManager;
import androidx.core.content.ContextCompat;

Expand All @@ -22,7 +23,12 @@ public static boolean isLocationPermissionGranted(Context context) {

public static boolean isBatteryOptimizationDeactivated(Context context) {
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
} else {
// this phone is too old to turn them off, so we lie and say we did.
return true;
}
}

public static boolean isBluetoothEnabled() {
Expand Down
4 changes: 2 additions & 2 deletions dp3t-sdk/sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ android {
compileSdkVersion 29

defaultConfig {
minSdkVersion 23
minSdkVersion 21
targetSdkVersion 29
versionCode 23
versionName "0.2.3"
Expand Down Expand Up @@ -110,7 +110,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])

implementation 'androidx.core:core:1.2.0'
implementation 'androidx.security:security-crypto:1.0.0-rc01'
implementation 'com.google.crypto.tink:tink-android:1.3.0'
implementation 'androidx.work:work-runtime:2.3.4'

implementation 'com.squareup.retrofit2:retrofit:2.6.2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public enum BluetoothScanMode {
SCAN_MODE_LOW_POWER(ScanSettings.SCAN_MODE_LOW_POWER),
SCAN_MODE_BALANCED(ScanSettings.SCAN_MODE_BALANCED),
SCAN_MODE_LOW_LATENCY(ScanSettings.SCAN_MODE_LOW_LATENCY),
SCAN_MODE_OPPORTUNISTIC(ScanSettings.SCAN_MODE_OPPORTUNISTIC);
SCAN_MODE_OPPORTUNISTIC(-1); // ScanSettings.SCAN_MODE_OPPORTUNISTIC

private final int systemValue;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,14 @@ public static Collection<ErrorState> checkTracingErrorStatus(Context context) {
}

PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
boolean batteryOptimizationsDeactivated =
powerManager == null || powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
boolean batteryOptimizationsDeactivated;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
batteryOptimizationsDeactivated = powerManager == null ||
powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
} else {
// This phone is too old to do it, so we lie and say they did.
batteryOptimizationsDeactivated = true;
}
if (!batteryOptimizationsDeactivated) {
errors.add(ErrorState.BATTERY_OPTIMIZER_ENABLED);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,11 @@ public static void scheduleNextClientRestart(Context context, long scanInterval)
Intent intent = new Intent(context, TracingServiceBroadcastReceiver.class);
intent.setAction(ACTION_RESTART_CLIENT);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, now + delay, pendingIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, now + delay, pendingIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, now + delay, pendingIntent);
}
}

public static void scheduleNextServerRestart(Context context) {
Expand All @@ -288,7 +292,11 @@ public static void scheduleNextServerRestart(Context context) {
Intent intent = new Intent(context, TracingServiceBroadcastReceiver.class);
intent.setAction(ACTION_RESTART_SERVER);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 2, intent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextAdvertiseChange, pendingIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextAdvertiseChange, pendingIntent);
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, nextAdvertiseChange, pendingIntent);
}
}

private void stopForegroundService() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.security.KeyPairGeneratorSpec;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Pair;
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKeys;

import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
Expand All @@ -25,11 +29,14 @@
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.x500.X500Principal;

import org.dpppt.android.sdk.BuildConfig;
import org.dpppt.android.sdk.backend.models.ExposeeAuthMethod;
import org.dpppt.android.sdk.backend.models.ExposeeAuthMethodJson;
import org.dpppt.android.sdk.internal.backend.models.ExposeeRequest;
import org.dpppt.android.sdk.internal.database.models.Contact;
import org.dpppt.android.sdk.internal.logger.Logger;
import org.dpppt.android.sdk.internal.util.DayDate;
import org.dpppt.android.sdk.internal.util.Json;

Expand All @@ -55,20 +62,87 @@ public class CryptoModule {
public static CryptoModule getInstance(Context context) {
if (instance == null) {
instance = new CryptoModule();

// Make the key if it does not already exist.
String keyAlias = "_security_master_key_";
if (! keyExists(keyAlias)) {
try {
generateKey(context, keyAlias);
} catch (GeneralSecurityException ex) {
ex.printStackTrace();
instance = null;
}
}

try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
KeyStore.Entry e = keyStore.getEntry(keyAlias, null);
Logger.d("jra", e.toString());
} catch (Exception e) {
e.printStackTrace();
}

try {
String KEY_ALIAS = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
instance.esp = EncryptedSharedPreferences.create("dp3t_store",
KEY_ALIAS,
keyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM);
} catch (GeneralSecurityException | IOException ex) {
ex.printStackTrace();
instance = null;
}
}
return instance;
}

private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static boolean keyExists(String keyAlias) {
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
return keyStore.containsAlias(keyAlias);
} catch (GeneralSecurityException|IOException ex) {
return false;
}
}

private static void generateKey(Context context, String keyAlias)
throws GeneralSecurityException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256);
KeyGenParameterSpec AES256_GCM_SPEC = builder.build();

KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEYSTORE);
keyGenerator.init(AES256_GCM_SPEC);
keyGenerator.generateKey();
} else {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", ANDROID_KEYSTORE);
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 10);
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec
.Builder(context)
.setAlias(keyAlias)
.setSubject(new X500Principal("O=DP-3T C=CH"))
.setSerialNumber(BigInteger.ONE)
.setStartDate(start.getTime())
.setEndDate(end.getTime())
.build();
generator.initialize(spec);
generator.generateKeyPair();
}
}

public boolean init() {
try {
String stringKey = esp.getString(KEY_SK_LIST_JSON, null);
Expand Down Expand Up @@ -125,7 +199,9 @@ protected byte[] getCurrentSK(DayDate day) {
rotateSK();
SKList = getSKList();
}
assert SKList.get(0).first.equals(day);
if (BuildConfig.DEBUG && ! SKList.get(0).first.equals(day)) {
throw new RuntimeException("getCurrentSK: assert failed");
}
return SKList.get(0).second;
}

Expand Down
Loading

0 comments on commit 5d07255

Please sign in to comment.