From 0e5c95e5cb6f7db5cc1c3ae711f622378d8ef786 Mon Sep 17 00:00:00 2001 From: "Kevin T. Berstene" Date: Mon, 1 Apr 2019 14:30:43 -0400 Subject: Added password encryption for SDK 23 and higher --- .../dsub/activity/SubsonicFragmentActivity.java | 28 +++- .../dsub/fragments/SettingsFragment.java | 5 +- .../daneren2005/dsub/service/RESTMusicService.java | 2 + .../github/daneren2005/dsub/util/Constants.java | 1 + .../github/daneren2005/dsub/util/KeyStoreUtil.java | 147 +++++++++++++++++++++ .../github/daneren2005/dsub/util/UserUtil.java | 4 + .../java/github/daneren2005/dsub/util/Util.java | 2 + .../dsub/view/EditPasswordPreference.java | 77 +++++++++++ 8 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/github/daneren2005/dsub/util/KeyStoreUtil.java create mode 100644 app/src/main/java/github/daneren2005/dsub/view/EditPasswordPreference.java (limited to 'app/src/main/java/github/daneren2005') diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java index 803e6f72..a698f530 100644 --- a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java +++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java @@ -27,6 +27,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.TypedArray; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.provider.MediaStore; @@ -74,6 +75,7 @@ import github.daneren2005.dsub.updates.Updater; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.DrawableTint; import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.KeyStoreUtil; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.UserUtil; import github.daneren2005.dsub.util.Util; @@ -691,6 +693,15 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } private void loadSession() { + if (Build.VERSION.SDK_INT >= 23) { + try { + KeyStoreUtil.loadKeyStore(); + } catch (Exception e) { + Log.w(TAG, "Error loading keystore"); + Log.w(TAG, Log.getStackTraceString(e)); + } + } + loadSettings(); if(!Util.isOffline(this) && ServerInfo.canBookmark(this)) { loadBookmarks(); @@ -730,7 +741,22 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + 1, "Demo Server"); editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + 1, "http://demo.subsonic.org"); editor.putString(Constants.PREFERENCES_KEY_USERNAME + 1, "guest2"); - editor.putString(Constants.PREFERENCES_KEY_PASSWORD + 1, "guest"); + if (Build.VERSION.SDK_INT < 23) { + editor.putString(Constants.PREFERENCES_KEY_PASSWORD + 1, "guest"); + } else { + // Attempt to encrypt password + String encryptedDefaultPassword = KeyStoreUtil.encrypt("guest"); + + if (encryptedDefaultPassword != null) { + // If encryption succeeds, store encrypted password and flag password as encrypted + editor.putString(Constants.PREFERENCES_KEY_PASSWORD + 1, encryptedDefaultPassword); + editor.putBoolean(Constants.PREFERENCES_KEY_ENCRYPTED_PASSWORD + 1, true); + } else { + // Fall back to plaintext if Keystore is having issue + editor = editor.putString(Constants.PREFERENCES_KEY_PASSWORD + 1, "guest"); + editor.putBoolean(Constants.PREFERENCES_KEY_ENCRYPTED_PASSWORD + 1, false); + } + } editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); editor.commit(); } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java index f7031146..674ce98a 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java @@ -36,8 +36,6 @@ import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.text.InputType; import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; @@ -65,6 +63,7 @@ import github.daneren2005.dsub.util.SyncUtil; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.CacheLocationPreference; import github.daneren2005.dsub.view.ErrorDialog; +import github.daneren2005.dsub.view.EditPasswordPreference; public class SettingsFragment extends PreferenceCompatFragment implements SharedPreferences.OnSharedPreferenceChangeListener { private final static String TAG = SettingsFragment.class.getSimpleName(); @@ -557,7 +556,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared serverUsernamePreference.setTitle(R.string.settings_server_username); serverUsernamePreference.setDialogTitle(R.string.settings_server_username); - final EditTextPreference serverPasswordPreference = new EditTextPreference(context); + final EditTextPreference serverPasswordPreference = new EditPasswordPreference(context, instance); serverPasswordPreference.setKey(Constants.PREFERENCES_KEY_PASSWORD + instance); serverPasswordPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); serverPasswordPreference.setSummary("***"); diff --git a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java index a4987b09..100224e8 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java @@ -72,6 +72,7 @@ import github.daneren2005.dsub.service.parser.StarredListParser; import github.daneren2005.dsub.service.parser.TopSongsParser; import github.daneren2005.dsub.service.parser.UserParser; import github.daneren2005.dsub.service.parser.VideosParser; +import github.daneren2005.dsub.util.KeyStoreUtil; import github.daneren2005.dsub.util.Pair; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.Constants; @@ -1912,6 +1913,7 @@ public class RESTMusicService implements MusicService { int instance = getInstance(context); String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); + if (prefs.getBoolean(Constants.PREFERENCES_KEY_ENCRYPTED_PASSWORD + instance, false)) password = KeyStoreUtil.decrypt(password); String encoded = Base64.encodeToString((username + ":" + password).getBytes("UTF-8"), Base64.NO_WRAP);; connection.setRequestProperty("Authorization", "Basic " + encoded); diff --git a/app/src/main/java/github/daneren2005/dsub/util/Constants.java b/app/src/main/java/github/daneren2005/dsub/util/Constants.java index 7f5ff3f1..017ba2f3 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Constants.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Constants.java @@ -85,6 +85,7 @@ public final class Constants { public static final String PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId"; public static final String PREFERENCES_KEY_USERNAME = "username"; public static final String PREFERENCES_KEY_PASSWORD = "password"; + public static final String PREFERENCES_KEY_ENCRYPTED_PASSWORD = "encryptedPassword"; public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime"; public static final String PREFERENCES_KEY_THEME = "theme"; public static final String PREFERENCES_KEY_FULL_SCREEN = "fullScreen"; diff --git a/app/src/main/java/github/daneren2005/dsub/util/KeyStoreUtil.java b/app/src/main/java/github/daneren2005/dsub/util/KeyStoreUtil.java new file mode 100644 index 00000000..10ec9497 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/KeyStoreUtil.java @@ -0,0 +1,147 @@ +package github.daneren2005.dsub.util; + +import android.annotation.TargetApi; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.support.annotation.NonNull; +import android.util.Base64; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.spec.IvParameterSpec; + +@TargetApi(23) +public class KeyStoreUtil { + private static String TAG = KeyStoreUtil.class.getSimpleName(); + private static final String KEYSTORE_ALIAS = "DSubKeyStoreAlias"; + private static final String KEYSTORE_PROVIDER = "AndroidKeyStore"; + private static final String KEYSTORE_CIPHER_PROVIDER = "AndroidKeyStoreBCWorkaround"; + private static final String KEYSTORE_TRANSFORM = "AES/CBC/PKCS7Padding"; + private static final String KEYSTORE_BYTE_ENCODING = "UTF-8"; + + public static void loadKeyStore() throws KeyStoreException, IOException, + CertificateException, NoSuchAlgorithmException { + + // Load keystore + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER); + keyStore.load(null); + + // Check if keystore has been used before + if (!keyStore.containsAlias(KEYSTORE_ALIAS)) { + // If alias does not exist, keystore hasn't been used before + // Create a new secret key to store in the keystore + try { + Log.w(TAG, "Generating keys."); + generateKeys(); + } catch (Exception e) { + Log.w(TAG, "Key generation failed."); + Log.w(TAG, Log.getStackTraceString(e)); + } + } + } + + private static void generateKeys() throws InvalidAlgorithmParameterException, + NoSuchAlgorithmException, NoSuchProviderException { + KeyGenerator keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER); + keyGen.init(new KeyGenParameterSpec.Builder(KEYSTORE_ALIAS, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .build()); + keyGen.generateKey(); + } + + private static Key getKey() throws KeyStoreException, CertificateException, + NoSuchAlgorithmException, IOException, UnrecoverableEntryException { + + // Attempt to load keystore + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER); + keyStore.load(null); + + // Fetch and return secret key + return keyStore.getKey(KEYSTORE_ALIAS, null); + } + + public static String encrypt(@NonNull String plainTextString) { + Log.d(TAG, "Encrypting password..."); + try { + // Retrieve secret key + final Key key = getKey(); + + // Initialize cipher + Cipher cipher = Cipher.getInstance(KEYSTORE_TRANSFORM, KEYSTORE_CIPHER_PROVIDER); + cipher.init(Cipher.ENCRYPT_MODE, key); + + // Create stream for storing data + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + // Write the IV length first so the IV can be split from the encrypted password + outputStream.write(cipher.getIV().length); + + // Write the auto-generated IV + outputStream.write(cipher.getIV()); + + // Encrypt the plaintext and write the encrypted string + outputStream.write(cipher.doFinal(plainTextString.getBytes(KEYSTORE_BYTE_ENCODING))); + + // Encode the return full stream for storage + Log.d(TAG, "Password encryption successful"); + return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP); + + } catch (Exception e) { + Log.w(TAG, "Password encryption failed"); + Log.d(TAG, Log.getStackTraceString(e)); + return null; + } + } + + public static String decrypt(@NonNull String encryptedString) { + Log.d(TAG, "Decrypting password..."); + try { + // Retrieve secret key + final Key key = getKey(); + + // Decode the string from Base64 + byte[] decodedBytes = Base64.decode(encryptedString, Base64.NO_WRAP); + int ivLength = decodedBytes[0]; + int encryptedLength = decodedBytes.length - (ivLength + 1); + + // Get IV from decoded string + byte[] ivBytes = new byte[ivLength]; + System.arraycopy(decodedBytes, 1, ivBytes, 0, ivLength); + + // Get encrypted password from decoded string + byte[] encryptedBytes = new byte[encryptedLength]; + System.arraycopy(decodedBytes, ivLength + 1, encryptedBytes, 0, encryptedLength); + + // Initialize cipher using the IV from the dencoded string + Cipher cipher = Cipher.getInstance(KEYSTORE_TRANSFORM, KEYSTORE_CIPHER_PROVIDER); + IvParameterSpec ivParamSpec = new IvParameterSpec(ivBytes); + cipher.init(Cipher.DECRYPT_MODE, key, ivParamSpec); + + // Decrypt the password + String decryptedString = new String(cipher.doFinal(encryptedBytes)); + + // Return the decrypted password string + Log.d(TAG, "Password successfully decrypted"); + return decryptedString; + + } catch (Exception e) { + Log.w(TAG, "Password decryption failed"); + Log.w(TAG, Log.getStackTraceString(e)); + return null; + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java index db1c628f..0775c956 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java +++ b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java @@ -272,6 +272,10 @@ public final class UserUtil { SharedPreferences prefs = Util.getPreferences(context); String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null); + if (prefs.getBoolean(Constants.PREFERENCES_KEY_ENCRYPTED_PASSWORD + instance, false)) { + correctPassword = KeyStoreUtil.decrypt(correctPassword); + } + return password != null && password.equals(correctPassword); } diff --git a/app/src/main/java/github/daneren2005/dsub/util/Util.java b/app/src/main/java/github/daneren2005/dsub/util/Util.java index 78f3e2d6..791fea91 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Util.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Util.java @@ -199,6 +199,7 @@ public final class Util { String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null); String userName = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null); String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null); + if ((password != null) && (prefs.getBoolean(Constants.PREFERENCES_KEY_ENCRYPTED_PASSWORD + instance, false))) password = KeyStoreUtil.decrypt(password); String musicFolderId = prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + newInstance, null); // Store the +1 server details in the to be deleted instance @@ -364,6 +365,7 @@ public final class Util { String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); + if ((password != null) && (prefs.getBoolean(Constants.PREFERENCES_KEY_ENCRYPTED_PASSWORD + instance, false))) password = KeyStoreUtil.decrypt(password); builder.append(serverUrl); if (builder.charAt(builder.length() - 1) != '/') { diff --git a/app/src/main/java/github/daneren2005/dsub/view/EditPasswordPreference.java b/app/src/main/java/github/daneren2005/dsub/view/EditPasswordPreference.java new file mode 100644 index 00000000..718fc22d --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/EditPasswordPreference.java @@ -0,0 +1,77 @@ +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.os.Build; +import android.preference.EditTextPreference; +import android.preference.Preference; + +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.KeyStoreUtil; +import github.daneren2005.dsub.util.Util; + +public class EditPasswordPreference extends EditTextPreference { + final private String TAG = EditPasswordPreference.class.getSimpleName(); + + private int instance; + private boolean passwordDecrypted; + + public EditPasswordPreference(Context context, int instance) { + super(context); + + final EditPasswordPreference editPassPref = this; + this.instance = instance; + + if (Build.VERSION.SDK_INT >= 23) { + this.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + return editPassPref.onPreferenceClick(); + } + }); + } + } + + private boolean onPreferenceClick() { + Context context = this.getContext(); + + // If password is encrypted, attempt to decrypt it to list actual password in masked box + // It could be that we should fill in nonsense in here instead, but if we do and the user clicks OK, + // the nonsense will be encrypted and the server connection will fail + // Checks first to see if the password has already been decrypted - if the user clicks on the preference a second time + // before the box has loaded, but after the password has already been encrypted + if (!(passwordDecrypted) && (Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_ENCRYPTED_PASSWORD + this.instance, false))) { + String decryptedPassword = KeyStoreUtil.decrypt(this.getEditText().getText().toString()); + if (decryptedPassword != null) { + this.getEditText().setText(decryptedPassword); + this.passwordDecrypted = true; + } else { + Util.toast(context, "Password Decryption Failed"); + } + } + + // Let the click action continue as normal + return false; + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + if ((positiveResult) && (Build.VERSION.SDK_INT >= 23)) { + Context context = this.getContext(); + + String encryptedString = KeyStoreUtil.encrypt(this.getEditText().getText().toString()); + if (encryptedString != null) { + this.getEditText().setText(encryptedString); + Util.getPreferences(context).edit().putBoolean(Constants.PREFERENCES_KEY_ENCRYPTED_PASSWORD + instance, true).commit(); + } else { + Util.toast(context, "Password encryption failed"); + Util.getPreferences(context).edit().putBoolean(Constants.PREFERENCES_KEY_ENCRYPTED_PASSWORD + instance, false).commit(); + } + } + + // Reset this flag so it decrypts if applicable next time the dialog is opened + this.passwordDecrypted = false; + + // Continue the dialog closing process + super.onDialogClosed(positiveResult); + } +} -- cgit v1.2.3