aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@users.noreply.github.com>2019-04-08 17:13:56 -0700
committerGitHub <noreply@github.com>2019-04-08 17:13:56 -0700
commit166d395e3e5de223527fec96d8b17dd42ed9a991 (patch)
tree6d697758622be65007384e16d1a16bdaad59fc6d
parentc4bf564c548289d38bc80f1ff211c97647dbcb26 (diff)
parent0e5c95e5cb6f7db5cc1c3ae711f622378d8ef786 (diff)
downloaddsub-166d395e3e5de223527fec96d8b17dd42ed9a991.tar.gz
dsub-166d395e3e5de223527fec96d8b17dd42ed9a991.tar.bz2
dsub-166d395e3e5de223527fec96d8b17dd42ed9a991.zip
Merge pull request #945 from KBerstene/feature/encryptedPasswords
Added password encryption for SDK 23 and higher
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java28
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java5
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java2
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Constants.java1
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/KeyStoreUtil.java147
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/UserUtil.java4
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Util.java2
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/EditPasswordPreference.java77
8 files changed, 262 insertions, 4 deletions
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);
+ }
+}