diff --git a/OpenScienceJournal/app/build.gradle b/OpenScienceJournal/app/build.gradle index 86a1bd94..bb83ebfb 100644 --- a/OpenScienceJournal/app/build.gradle +++ b/OpenScienceJournal/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "cc.arduino.sciencejournal" minSdkVersion 19 targetSdkVersion 29 - versionCode 9 - versionName "1.3.3" + versionCode 10 + versionName "1.3.4" multiDexEnabled true vectorDrawables.useSupportLibrary = true } diff --git a/OpenScienceJournal/whistlepunk_library/build.gradle b/OpenScienceJournal/whistlepunk_library/build.gradle index 6dbb6204..ee70cdee 100644 --- a/OpenScienceJournal/whistlepunk_library/build.gradle +++ b/OpenScienceJournal/whistlepunk_library/build.gradle @@ -21,8 +21,8 @@ android { defaultConfig { minSdkVersion 19 targetSdkVersion 29 - versionCode 9 - versionName "1.3.3" + versionCode 10 + versionName "1.3.4" vectorDrawables.useSupportLibrary = true } diff --git a/OpenScienceJournal/whistlepunk_library/src/main/java/com/google/android/apps/forscience/whistlepunk/accounts/arduino/ArduinoAccountProvider.java b/OpenScienceJournal/whistlepunk_library/src/main/java/com/google/android/apps/forscience/whistlepunk/accounts/arduino/ArduinoAccountProvider.java index aaf044cf..bd2a9c21 100644 --- a/OpenScienceJournal/whistlepunk_library/src/main/java/com/google/android/apps/forscience/whistlepunk/accounts/arduino/ArduinoAccountProvider.java +++ b/OpenScienceJournal/whistlepunk_library/src/main/java/com/google/android/apps/forscience/whistlepunk/accounts/arduino/ArduinoAccountProvider.java @@ -12,21 +12,31 @@ import com.google.android.apps.forscience.auth0.Auth0Token; import com.google.android.apps.forscience.whistlepunk.ActivityWithNavigationView; +import com.google.android.apps.forscience.whistlepunk.MainActivity; import com.google.android.apps.forscience.whistlepunk.accounts.AbstractAccountsProvider; import com.google.android.apps.forscience.whistlepunk.accounts.AppAccount; import com.google.android.apps.forscience.whistlepunk.remote.StringUtils; +import java.io.File; import java.io.IOException; import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; public class ArduinoAccountProvider extends AbstractAccountsProvider { private static final String LOG_TAG = "ArduinoAccountProvider"; + private static final String SHARED_PREFS_FILENAME = "ArduinoSharedPreferences"; + private static final String UNENCRYPTED_SHARED_PREFS_FILENAME = "ArduinoSharedPreferencesSafe"; + private static final String KEY_KEYSTORE_RESET = "keystore_reset"; private ArduinoAccount arduinoAccount; public ArduinoAccountProvider(final Context context) { super(context); + final SharedPreferences prefs = getSharedPreferences(); final String jsonToken = prefs.getString("token", null); if (!StringUtils.isEmpty(jsonToken)) { @@ -100,27 +110,86 @@ public void showAddAccountDialog(Activity activity) { public void showAccountSwitcherDialog(Fragment fragment, int requestCode) { } + private Boolean getKeystoreResetState() { + SharedPreferences prefs = applicationContext.getSharedPreferences(UNENCRYPTED_SHARED_PREFS_FILENAME, Context.MODE_PRIVATE); + return prefs.getBoolean(KEY_KEYSTORE_RESET, false); + } + private void setKeystoreResetState(Boolean state) { + SharedPreferences prefs = applicationContext.getSharedPreferences(UNENCRYPTED_SHARED_PREFS_FILENAME, Context.MODE_PRIVATE); + prefs.edit().putBoolean(KEY_KEYSTORE_RESET, state).apply(); + } + + private void resetKeystoreAndRestart() { + // delete shared preferences file + File sharedPrefsFile = new File(applicationContext.getFilesDir().getParent() + "/shared_prefs/" + SHARED_PREFS_FILENAME + ".xml"); + if (sharedPrefsFile.exists()) { + Boolean deleted = sharedPrefsFile.delete(); + Log.i(LOG_TAG, String.format("Shared prefs file \"%s\" deleted: %s", sharedPrefsFile.getAbsolutePath(), deleted)); + } else { + Log.i(LOG_TAG, String.format("Shared prefs file \"%s\" non-existent", sharedPrefsFile.getAbsolutePath())); + } + + // delete master key + try { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + keyStore.deleteEntry(MasterKey.DEFAULT_MASTER_KEY_ALIAS); + + Log.i(LOG_TAG, "Master key deleted"); + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) { + Log.i(LOG_TAG, "Unable to delete master key"); + throw new RuntimeException("Unable to delete master key", e); + } + + // save on (non-encrypted) shared preferences the reset state to avoid loops + setKeystoreResetState(true); + Log.i(LOG_TAG, "Set keystore reset state to TRUE."); + + // restart app + Intent intent = new Intent(applicationContext, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Log.i(LOG_TAG, "Restarting application..."); + applicationContext.startActivity(intent); + if (applicationContext instanceof Activity) { + ((Activity) applicationContext).finish(); + } + + Runtime.getRuntime().exit(0); + } + private SharedPreferences getSharedPreferences() { MasterKey masterKey = null; try { + // build MasterKey masterKey = new MasterKey.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); - return EncryptedSharedPreferences.create( + // get or create shared preferences + SharedPreferences prefs = EncryptedSharedPreferences.create( applicationContext, - "ArduinoSharedPreferences", + SHARED_PREFS_FILENAME, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); - } catch (GeneralSecurityException e) { - Log.e(LOG_TAG, "Unable to retrieve encrypted shared preferences", e); - throw new RuntimeException("Unable to retrieve encrypted shared preferences", e); - } catch (IOException e) { - Log.e(LOG_TAG, "Unable to retrieve encrypted shared preferences", e); - throw new RuntimeException("Unable to retrieve encrypted shared preferences", e); + + // set keystore reset to false to enable future resets + setKeystoreResetState(false); + Log.i(LOG_TAG, "Set keystore reset state to FALSE."); + + return prefs; + } catch (GeneralSecurityException | IOException e) { + Boolean keystoreReset = getKeystoreResetState(); + if (keystoreReset) { + // a keystore reset just occurred, interrupt execution. + throw new RuntimeException("Unable to retrieve encrypted shared preferences", e); + } else { + Log.i(LOG_TAG, "Unable to retrieve encrypted shared preferences, regenerating master key.", e); + resetKeystoreAndRestart(); + } + + return null; } } - } diff --git a/OpenScienceJournal/whistlepunk_library/src/main/java/com/google/android/apps/forscience/whistlepunk/gdrivesync/GDriveShared.java b/OpenScienceJournal/whistlepunk_library/src/main/java/com/google/android/apps/forscience/whistlepunk/gdrivesync/GDriveShared.java index 769840c8..c9d56641 100644 --- a/OpenScienceJournal/whistlepunk_library/src/main/java/com/google/android/apps/forscience/whistlepunk/gdrivesync/GDriveShared.java +++ b/OpenScienceJournal/whistlepunk_library/src/main/java/com/google/android/apps/forscience/whistlepunk/gdrivesync/GDriveShared.java @@ -1,12 +1,26 @@ package com.google.android.apps.forscience.whistlepunk.gdrivesync; +import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.util.Log; import androidx.security.crypto.EncryptedSharedPreferences; -import androidx.security.crypto.MasterKeys; +import androidx.security.crypto.MasterKey; + +import com.google.android.apps.forscience.whistlepunk.MainActivity; + +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; public class GDriveShared { + private static final String LOG_TAG = "GDriveShared"; private static boolean mAccountLoaded = false; @@ -54,23 +68,93 @@ public static GDriveAccount getCredentials(final Context context) { return mAccount; } + private static Boolean getKeystoreResetState(final Context context) { + SharedPreferences prefs = context.getSharedPreferences(UNENCRYPTED_SHARED_PREFS_FILENAME, Context.MODE_PRIVATE); + return prefs.getBoolean(KEY_KEYSTORE_RESET, false); + } + private static void setKeystoreResetState(final Context context, Boolean state) { + SharedPreferences prefs = context.getSharedPreferences(UNENCRYPTED_SHARED_PREFS_FILENAME, Context.MODE_PRIVATE); + prefs.edit().putBoolean(KEY_KEYSTORE_RESET, state).apply(); + } + + private static void resetKeystoreAndRestart(final Context context) { + // delete shared preferences file + File sharedPrefsFile = new File(context.getFilesDir().getParent() + "/shared_prefs/" + SHARED_PREFS_FILENAME + ".xml"); + if (sharedPrefsFile.exists()) { + Boolean deleted = sharedPrefsFile.delete(); + Log.i(LOG_TAG, String.format("Shared prefs file \"%s\" deleted: %s", sharedPrefsFile.getAbsolutePath(), deleted)); + } else { + Log.i(LOG_TAG, String.format("Shared prefs file \"%s\" non-existent", sharedPrefsFile.getAbsolutePath())); + } + + // delete master key + try { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + keyStore.deleteEntry(MasterKey.DEFAULT_MASTER_KEY_ALIAS); + + Log.i(LOG_TAG, "Master key deleted"); + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) { + Log.i(LOG_TAG, "Unable to delete master key"); + throw new RuntimeException("Unable to delete master key", e); + } + + // save on (non-encrypted) shared preferences the reset state to avoid loops + setKeystoreResetState(context, true); + Log.i(LOG_TAG, "Set keystore reset state to TRUE."); + + // restart app + Intent intent = new Intent(context, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Log.i(LOG_TAG, "Restarting application..."); + context.startActivity(intent); + if (context instanceof Activity) { + ((Activity) context).finish(); + } + + Runtime.getRuntime().exit(0); + } + private static SharedPreferences getSharedPreferences(final Context context) { + MasterKey masterKey = null; try { - final String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC); - return EncryptedSharedPreferences.create( - PREFS_NAME, - masterKeyAlias, + // build MasterKey + masterKey = new MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build(); + + // get or create shared preferences + SharedPreferences prefs = EncryptedSharedPreferences.create( context, + SHARED_PREFS_FILENAME, + masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); - } catch (Exception e) { - throw new RuntimeException(e); + + // set keystore reset to false to enable future resets + setKeystoreResetState(context, false); + Log.i(LOG_TAG, "Set keystore reset state to FALSE."); + + return prefs; + } catch (GeneralSecurityException | IOException e) { + Boolean keystoreReset = getKeystoreResetState(context); + if (keystoreReset) { + // a keystore reset just occurred, interrupt execution to avoid loops + throw new RuntimeException("Unable to retrieve encrypted shared preferences", e); + } else { + Log.i(LOG_TAG, "Unable to retrieve encrypted shared preferences, regenerating master key.", e); + resetKeystoreAndRestart(context); + } + + return null; } } - private static final String PREFS_NAME = "GoogleDriveSync"; + private static final String SHARED_PREFS_FILENAME = "GoogleDriveSync"; + private static final String UNENCRYPTED_SHARED_PREFS_FILENAME = "ArduinoSharedPreferencesSafe"; + private static final String KEY_KEYSTORE_RESET = "keystore_reset"; private static final String KEY_ACCOUNT_ID = "account_id"; private static final String KEY_EMAIL = "email"; private static final String KEY_TOKEN = "token";