From 3565c899b8e739efe04c2bc660f04e19a25e343a Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Tue, 2 Jan 2018 22:00:53 +0100 Subject: [PATCH] Custom CredentialsPreference and better handling of encryption changes --- .../Activities/AuthenticateActivity.java | 42 ++- .../andotp/Activities/MainActivity.java | 127 ++++---- .../andotp/Activities/SettingsActivity.java | 115 +++----- .../Preferences/CredentialsPreference.java | 271 ++++++++++++++++++ .../Preferences/PBKDF2PasswordPreference.java | 189 ------------ .../flocke/andotp/Utilities/Constants.java | 4 + .../flocke/andotp/Utilities/Settings.java | 40 ++- .../res/layout/component_authentication.xml | 73 +++++ .../main/res/values-cs-rCZ/strings_main.xml | 2 +- .../main/res/values-de-rDE/strings_main.xml | 2 +- .../main/res/values-es-rES/strings_main.xml | 2 +- .../main/res/values-fr-rFR/strings_main.xml | 2 +- .../main/res/values-gl-rES/strings_main.xml | 2 +- .../main/res/values-nl-rNL/strings_main.xml | 2 +- .../main/res/values-pl-rPL/strings_main.xml | 2 +- .../main/res/values-ru-rRU/strings_main.xml | 2 +- .../main/res/values-zh-rCN/strings_main.xml | 2 +- app/src/main/res/values/settings.xml | 8 - app/src/main/res/values/strings_auth.xml | 6 +- app/src/main/res/values/strings_main.xml | 3 +- app/src/main/res/values/strings_settings.xml | 5 +- app/src/main/res/xml/preferences.xml | 13 +- 22 files changed, 534 insertions(+), 380 deletions(-) create mode 100644 app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java delete mode 100644 app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java create mode 100644 app/src/main/res/layout/component_authentication.xml diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/AuthenticateActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/AuthenticateActivity.java index 5fac7d72..e4890f04 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/AuthenticateActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/AuthenticateActivity.java @@ -48,21 +48,25 @@ import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; -import static org.shadowice.flocke.andotp.Utilities.Settings.AuthMethod; +import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; public class AuthenticateActivity extends ThemedActivity implements EditText.OnEditorActionListener { - public static final String EXTRA_NAME_PASSWORD_KEY = "password_key"; - public static final String EXTRA_NAME_SAVE_DATABASE = "save_database"; - public static final String EXTRA_NAME_MESSAGE = "message"; + public static final String AUTH_EXTRA_NAME_PASSWORD_KEY = "password_key"; + public static final String AUTH_EXTRA_NAME_FATAL = "fatal"; + public static final String AUTH_EXTRA_NAME_SAVE_DATABASE = "save_database"; + public static final String AUTH_EXTRA_NAME_MESSAGE = "message"; boolean saveDatabase = false; + boolean fatal = true; private String password; AuthMethod authMethod; boolean oldPassword = false; + TextInputEditText passwordInput; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -79,12 +83,13 @@ public class AuthenticateActivity extends ThemedActivity View v = stub.inflate(); Intent callingIntent = getIntent(); - saveDatabase = callingIntent.getBooleanExtra(EXTRA_NAME_SAVE_DATABASE, false); - int labelMsg = callingIntent.getIntExtra(EXTRA_NAME_MESSAGE, R.string.auth_msg_authenticate); + int labelMsg = callingIntent.getIntExtra(AUTH_EXTRA_NAME_MESSAGE, R.string.auth_msg_authenticate); + saveDatabase = callingIntent.getBooleanExtra(AUTH_EXTRA_NAME_SAVE_DATABASE, false); + fatal = callingIntent.getBooleanExtra(AUTH_EXTRA_NAME_FATAL, true); TextView passwordLabel = v.findViewById(R.id.passwordLabel); TextInputLayout passwordLayout = v.findViewById(R.id.passwordLayout); - TextInputEditText passwordInput = v.findViewById(R.id.passwordEdit); + passwordInput = v.findViewById(R.id.passwordEdit); passwordLabel.setText(labelMsg); @@ -191,17 +196,26 @@ public class AuthenticateActivity extends ThemedActivity // End with a result public void finishWithResult(boolean success, byte[] key) { - Intent data = new Intent(); + if (success || fatal) { + Intent data = new Intent(); - data.putExtra(EXTRA_NAME_SAVE_DATABASE, saveDatabase); + data.putExtra(AUTH_EXTRA_NAME_SAVE_DATABASE, saveDatabase); - if (key != null) - data.putExtra(EXTRA_NAME_PASSWORD_KEY, key); + if (key != null) + data.putExtra(AUTH_EXTRA_NAME_PASSWORD_KEY, key); - if (success) - setResult(RESULT_OK, data); + if (success) + setResult(RESULT_OK, data); - finish(); + finish(); + } else { + passwordInput.setText(""); + + if (authMethod == AuthMethod.PASSWORD) + Toast.makeText(this, R.string.auth_toast_password_again, Toast.LENGTH_LONG).show(); + else if (authMethod == AuthMethod.PIN) + Toast.makeText(this, R.string.auth_toast_pin_again, Toast.LENGTH_LONG).show(); + } } // Go back to the main activity diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java index 48622121..e7e46847 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java @@ -58,10 +58,8 @@ import com.google.zxing.integration.android.IntentResult; import org.shadowice.flocke.andotp.Database.Entry; import org.shadowice.flocke.andotp.R; -import org.shadowice.flocke.andotp.Utilities.Constants; import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; -import org.shadowice.flocke.andotp.Utilities.Settings; import org.shadowice.flocke.andotp.Utilities.TokenCalculator; import org.shadowice.flocke.andotp.View.EntriesCardAdapter; import org.shadowice.flocke.andotp.View.FloatingActionMenu; @@ -74,10 +72,15 @@ import java.util.HashMap; import javax.crypto.SecretKey; -import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.EXTRA_NAME_MESSAGE; -import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.EXTRA_NAME_PASSWORD_KEY; -import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.EXTRA_NAME_SAVE_DATABASE; +import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.AUTH_EXTRA_NAME_FATAL; +import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.AUTH_EXTRA_NAME_MESSAGE; +import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.AUTH_EXTRA_NAME_PASSWORD_KEY; +import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.AUTH_EXTRA_NAME_SAVE_DATABASE; import static org.shadowice.flocke.andotp.Activities.BackupActivity.EXTRA_NAME_ENCRYPTION_KEY; +import static org.shadowice.flocke.andotp.Activities.SettingsActivity.SETTINGS_EXTRA_NAME_ENCRYPTION_CHANGED; +import static org.shadowice.flocke.andotp.Activities.SettingsActivity.SETTINGS_EXTRA_NAME_ENCRYPTION_KEY; +import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; +import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType; import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; public class MainActivity extends BaseActivity @@ -92,7 +95,7 @@ public class MainActivity extends BaseActivity private MenuItem sortMenu; private SimpleItemTouchHelperCallback touchHelperCallback; - private Constants.EncryptionType encryptionType = Constants.EncryptionType.KEYSTORE; + private EncryptionType encryptionType = EncryptionType.KEYSTORE; private boolean requireAuthentication = false; private Handler handler; @@ -127,19 +130,20 @@ public class MainActivity extends BaseActivity .show(); } - public void authenticate(int messageId, boolean saveDatabase) { - Settings.AuthMethod authMethod = settings.getAuthMethod(); + public void authenticate(int messageId, boolean saveDatabase, boolean fatal) { + AuthMethod authMethod = settings.getAuthMethod(); - if (authMethod == Settings.AuthMethod.DEVICE) { + if (authMethod == AuthMethod.DEVICE) { KeyguardManager km = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP && km.isKeyguardSecure()) { Intent authIntent = km.createConfirmDeviceCredentialIntent(getString(R.string.dialog_title_auth), getString(R.string.dialog_msg_auth)); startActivityForResult(authIntent, INTENT_INTERNAL_AUTHENTICATE); } - } else if (authMethod == Settings.AuthMethod.PASSWORD || authMethod == Settings.AuthMethod.PIN) { + } else if (authMethod == AuthMethod.PASSWORD || authMethod == AuthMethod.PIN) { Intent authIntent = new Intent(this, AuthenticateActivity.class); - authIntent.putExtra(EXTRA_NAME_SAVE_DATABASE, saveDatabase); - authIntent.putExtra(EXTRA_NAME_MESSAGE, messageId); + authIntent.putExtra(AUTH_EXTRA_NAME_SAVE_DATABASE, saveDatabase); + authIntent.putExtra(AUTH_EXTRA_NAME_FATAL, fatal); + authIntent.putExtra(AUTH_EXTRA_NAME_MESSAGE, messageId); startActivityForResult(authIntent, INTENT_INTERNAL_AUTHENTICATE); } } @@ -198,13 +202,13 @@ public class MainActivity extends BaseActivity encryptionType = settings.getEncryption(); - if (settings.getAuthMethod() != Settings.AuthMethod.NONE && savedInstanceState == null) + if (settings.getAuthMethod() != AuthMethod.NONE && savedInstanceState == null) requireAuthentication = true; setBroadcastCallback(new BroadcastReceivedCallback() { @Override public void onReceivedScreenOff() { - if (settings.getAuthMethod() != Settings.AuthMethod.NONE) + if (settings.getAuthMethod() != AuthMethod.NONE) requireAuthentication = true; } }); @@ -318,20 +322,20 @@ public class MainActivity extends BaseActivity super.onResume(); if (requireAuthentication) { - if (settings.getAuthMethod() != Settings.AuthMethod.NONE) { + if (settings.getAuthMethod() != AuthMethod.NONE) { requireAuthentication = false; - authenticate(R.string.auth_msg_authenticate, false); + authenticate(R.string.auth_msg_authenticate,false, true); } } else { - if (encryptionType == Constants.EncryptionType.KEYSTORE) { + if (encryptionType == EncryptionType.KEYSTORE) { if (adapter.getEncryptionKey() == null) { adapter.setEncryptionKey(KeyStoreHelper.loadEncryptionKeyFromKeyStore(this)); } populateAdapter(); - } else if (encryptionType == Constants.EncryptionType.PASSWORD) { + } else if (encryptionType == EncryptionType.PASSWORD) { if (adapter.getEncryptionKey() == null) { - authenticate(R.string.auth_msg_authenticate,false); + authenticate(R.string.auth_msg_authenticate,false, true); } else { populateAdapter(); } @@ -358,20 +362,21 @@ public class MainActivity extends BaseActivity key.equals(getString(R.string.settings_key_lang)) || key.equals(getString(R.string.settings_key_enable_screenshot))) { recreate(); - } else if (key.equals(getString(R.string.settings_key_encryption))) { - if (settings.getEncryption() == Constants.EncryptionType.KEYSTORE) { - encryptionType = Constants.EncryptionType.KEYSTORE; - adapter.setEncryptionKey(KeyStoreHelper.loadEncryptionKeyFromKeyStore(this)); - adapter.saveEntries(); - } else if (settings.getEncryption() == Constants.EncryptionType.PASSWORD) { - encryptionType = Constants.EncryptionType.PASSWORD; - authenticate(R.string.auth_msg_confirm_encryption,true); - } - } else if (key.equals(getString(R.string.settings_key_auth)) || - key.equals(getString(R.string.settings_key_auth_password_pbkdf2)) || - key.equals(getString(R.string.settings_key_auth_pin_pbkdf2))) { - if (encryptionType == Constants.EncryptionType.PASSWORD) - authenticate(R.string.auth_msg_confirm_encryption, true); +// } else if (key.equals(getString(R.string.settings_key_encryption))) { +// if (settings.getEncryption() == EncryptionType.KEYSTORE) { +// encryptionType = EncryptionType.KEYSTORE; +// adapter.setEncryptionKey(KeyStoreHelper.loadEncryptionKeyFromKeyStore(this)); +// adapter.saveEntries(); +// } else if (settings.getEncryption() == EncryptionType.PASSWORD) { +// encryptionType = EncryptionType.PASSWORD; +// authenticate(R.string.auth_msg_confirm_encryption,true); +// } +// } else if (key.equals(getString(R.string.settings_key_auth)) || +// key.equals(getString(R.string.settings_key_auth_password_pbkdf2)) || +// key.equals(getString(R.string.settings_key_auth_pin_pbkdf2))) { +// if (encryptionType == EncryptionType.PASSWORD) { +// authenticate(R.string.auth_msg_confirm_encryption,true); +// } } } @@ -404,9 +409,16 @@ public class MainActivity extends BaseActivity adapter.loadEntries(); refreshTags(); } + } else if (requestCode == INTENT_INTERNAL_SETTINGS && resultCode == RESULT_OK) { + boolean encryptionChanged = intent.getBooleanExtra(SETTINGS_EXTRA_NAME_ENCRYPTION_CHANGED, false); + + if (encryptionChanged) { + byte[] newKey = intent.getByteArrayExtra(SETTINGS_EXTRA_NAME_ENCRYPTION_KEY); + updateEncryption(newKey, true); + } } else if (requestCode == INTENT_INTERNAL_AUTHENTICATE) { if (resultCode != RESULT_OK) { - Toast.makeText(getBaseContext(), R.string.toast_auth_failed, Toast.LENGTH_LONG).show(); + Toast.makeText(getBaseContext(), R.string.toast_auth_failed_fatal, Toast.LENGTH_LONG).show(); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { finishAndRemoveTask(); @@ -416,30 +428,39 @@ public class MainActivity extends BaseActivity } else { requireAuthentication = false; - SecretKey encryptionKey = null; + boolean saveDatabase = intent.getBooleanExtra(AUTH_EXTRA_NAME_SAVE_DATABASE, false); + byte[] authKey = intent.getByteArrayExtra(AUTH_EXTRA_NAME_PASSWORD_KEY); - if (encryptionType == Constants.EncryptionType.KEYSTORE) { - encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(this); - } else if (encryptionType == Constants.EncryptionType.PASSWORD) { - byte[] credentialSeed = intent.getByteArrayExtra(EXTRA_NAME_PASSWORD_KEY); - if (credentialSeed != null && credentialSeed.length > 0) - encryptionKey = EncryptionHelper.generateSymmetricKey(credentialSeed); - } - - boolean saveDatabase = intent.getBooleanExtra(EXTRA_NAME_SAVE_DATABASE, false); - - if (encryptionKey != null) { - adapter.setEncryptionKey(encryptionKey); - - if (saveDatabase) - adapter.saveEntries(); - } - - populateAdapter(); + updateEncryption(authKey, saveDatabase); } } } + private void updateEncryption(byte[] newKey, boolean saveDatabase) { + SecretKey encryptionKey = null; + + encryptionType = settings.getEncryption(); + + if (encryptionType == EncryptionType.KEYSTORE) { + encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(this); + } else if (encryptionType == EncryptionType.PASSWORD) { + if (newKey != null && newKey.length > 0) { + encryptionKey = EncryptionHelper.generateSymmetricKey(newKey); + } else { + authenticate(R.string.auth_msg_confirm_encryption, true, false); + } + } + + if (encryptionKey != null) { + adapter.setEncryptionKey(encryptionKey); + + if (saveDatabase) + adapter.saveEntries(); + } + + populateAdapter(); + } + // Options menu @Override public boolean onCreateOptionsMenu(Menu menu) { diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java index 57c18003..5766c0c5 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java @@ -22,7 +22,6 @@ package org.shadowice.flocke.andotp.Activities; -import android.app.KeyguardManager; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -37,14 +36,18 @@ import android.widget.Toast; import org.openintents.openpgp.util.OpenPgpAppPreference; import org.openintents.openpgp.util.OpenPgpKeyPreference; -import org.shadowice.flocke.andotp.Preferences.PBKDF2PasswordPreference; +import org.shadowice.flocke.andotp.Preferences.CredentialsPreference; import org.shadowice.flocke.andotp.R; -import org.shadowice.flocke.andotp.Utilities.Constants; import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; -import org.shadowice.flocke.andotp.Utilities.Settings; + +import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; +import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType; public class SettingsActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener{ + public static final String SETTINGS_EXTRA_NAME_ENCRYPTION_CHANGED = "encryption_changed"; + public static final String SETTINGS_EXTRA_NAME_ENCRYPTION_KEY = "encryption_key"; + SettingsFragment fragment; @Override @@ -70,20 +73,27 @@ public class SettingsActivity extends BaseActivity sharedPref.registerOnSharedPreferenceChangeListener(this); } - public void finishWithResult() { - setResult(RESULT_OK); + public void finishWithResult(boolean encryptionChanged, byte[] newKey) { + Intent data = new Intent(); + + data.putExtra(SETTINGS_EXTRA_NAME_ENCRYPTION_CHANGED, encryptionChanged); + + if (newKey != null) + data.putExtra(SETTINGS_EXTRA_NAME_ENCRYPTION_KEY, newKey); + + setResult(RESULT_OK, data); finish(); } @Override public boolean onSupportNavigateUp() { - finishWithResult(); + finishWithResult(false,null); return true; } @Override public void onBackPressed() { - finishWithResult(); + finishWithResult(false, null); super.onBackPressed(); } @@ -112,100 +122,42 @@ public class SettingsActivity extends BaseActivity OpenPgpAppPreference pgpProvider; OpenPgpKeyPreference pgpKey; - public void updateAuthPassword(Settings.AuthMethod newAuth) { - PBKDF2PasswordPreference pwPref = (PBKDF2PasswordPreference) catSecurity.findPreference(getString(R.string.settings_key_auth_password_pbkdf2)); - PBKDF2PasswordPreference pinPref = (PBKDF2PasswordPreference) catSecurity.findPreference(getString(R.string.settings_key_auth_pin_pbkdf2)); - - if (pwPref != null) - catSecurity.removePreference(pwPref); - if (pinPref != null) - catSecurity.removePreference(pinPref); - - if (newAuth == Settings.AuthMethod.PASSWORD) { - PBKDF2PasswordPreference authPassword = new PBKDF2PasswordPreference(getActivity(), null); - authPassword.setTitle(R.string.settings_title_auth_password); - authPassword.setOrder(4); - authPassword.setKey(getString(R.string.settings_key_auth_password_pbkdf2)); - authPassword.setMode(PBKDF2PasswordPreference.Mode.PASSWORD); - - catSecurity.addPreference(authPassword); - } else if (newAuth == Settings.AuthMethod.PIN) { - PBKDF2PasswordPreference authPIN = new PBKDF2PasswordPreference(getActivity(), null); - authPIN.setTitle(R.string.settings_title_auth_pin); - authPIN.setOrder(4); - authPIN.setKey(getString(R.string.settings_key_auth_pin_pbkdf2)); - authPIN.setMode(PBKDF2PasswordPreference.Mode.PIN); - - catSecurity.addPreference(authPIN); - } - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getActivity().getBaseContext()); - addPreferencesFromResource(R.xml.preferences); + CredentialsPreference credentialsPreference = (CredentialsPreference) findPreference(getString(R.string.settings_key_auth)); + credentialsPreference.setEncryptionChangeHandler(new CredentialsPreference.EncryptionChangeHandler() { + @Override + public void onEncryptionChanged(byte[] newKey) { + ((SettingsActivity) getActivity()).finishWithResult(true, newKey); + } + }); + // Authentication catSecurity = (PreferenceCategory) findPreference(getString(R.string.settings_key_cat_security)); - ListPreference authPref = (ListPreference) findPreference(getString(R.string.settings_key_auth)); encryption = (ListPreference) findPreference(getString(R.string.settings_key_encryption)); - updateAuthPassword(Settings.AuthMethod.valueOf(authPref.getValue().toUpperCase())); - - authPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object o) { - String newAuth = (String) o; - String encryption = sharedPref.getString(getString(R.string.settings_key_encryption), getString(R.string.settings_default_encryption)); - - Constants.EncryptionType encryptionType = Constants.EncryptionType.valueOf(encryption.toUpperCase()); - Settings.AuthMethod authMethod = Settings.AuthMethod.valueOf(newAuth.toUpperCase()); - - if (encryptionType == Constants.EncryptionType.PASSWORD) { - if (authMethod == Settings.AuthMethod.NONE || authMethod == Settings.AuthMethod.DEVICE) { - Toast.makeText(getActivity(), R.string.settings_toast_auth_invalid_with_encryption, Toast.LENGTH_LONG).show(); - return false; - } - } - - if (authMethod == Settings.AuthMethod.DEVICE) { - KeyguardManager km = (KeyguardManager) getActivity().getSystemService(KEYGUARD_SERVICE); - - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { - Toast.makeText(getActivity(), R.string.settings_toast_auth_device_pre_lollipop, Toast.LENGTH_LONG).show(); - return false; - } else if (! km.isKeyguardSecure()) { - Toast.makeText(getActivity(), R.string.settings_toast_auth_device_not_secure, Toast.LENGTH_LONG).show(); - return false; - } - } - - updateAuthPassword(authMethod); - - return true; - } - }); - encryption.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(final Preference preference, Object o) { String newEncryption = (String) o; - String auth = sharedPref.getString(getString(R.string.settings_key_auth), getString(R.string.settings_default_auth)); - Constants.EncryptionType encryptionType = Constants.EncryptionType.valueOf(newEncryption.toUpperCase()); - Settings.AuthMethod authMethod = Settings.AuthMethod.valueOf(auth.toUpperCase()); + String auth = sharedPref.getString(getString(R.string.settings_key_auth), CredentialsPreference.DEFAULT_VALUE.name().toLowerCase()); + EncryptionType encryptionType = EncryptionType.valueOf(newEncryption.toUpperCase()); + AuthMethod authMethod = AuthMethod.valueOf(auth.toUpperCase()); - if (encryptionType == Constants.EncryptionType.PASSWORD) { - if (authMethod != Settings.AuthMethod.PASSWORD && authMethod != Settings.AuthMethod.PIN) { + if (encryptionType == EncryptionType.PASSWORD) { + if (authMethod != AuthMethod.PASSWORD && authMethod != AuthMethod.PIN) { Toast.makeText(getActivity(), R.string.settings_toast_encryption_invalid_with_auth, Toast.LENGTH_LONG).show(); return false; } else { String credentials = ""; - if (authMethod == Settings.AuthMethod.PASSWORD) + if (authMethod == AuthMethod.PASSWORD) credentials = sharedPref.getString(getString(R.string.settings_key_auth_password_pbkdf2), ""); - else if (authMethod == Settings.AuthMethod.PIN) + else if (authMethod == AuthMethod.PIN) credentials = sharedPref.getString(getString(R.string.settings_key_auth_pin_pbkdf2), ""); if (credentials.isEmpty()) { @@ -218,6 +170,7 @@ public class SettingsActivity extends BaseActivity } encryption.setValue(newEncryption); + ((SettingsActivity) getActivity()).finishWithResult(true, null); return true; } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java new file mode 100644 index 00000000..4d475eee --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2017 Jakob Nixdorf + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.shadowice.flocke.andotp.Preferences; + +import android.app.AlertDialog; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.res.TypedArray; +import android.preference.DialogPreference; +import android.support.design.widget.TextInputEditText; +import android.support.design.widget.TextInputLayout; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.text.method.PasswordTransformationMethod; +import android.util.AttributeSet; +import android.util.Base64; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.Toast; + +import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; +import org.shadowice.flocke.andotp.Utilities.Settings; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.List; + +import static android.content.Context.AUDIO_SERVICE; +import static android.content.Context.KEYGUARD_SERVICE; +import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; +import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType; + +public class CredentialsPreference extends DialogPreference + implements AdapterView.OnItemClickListener, View.OnClickListener, TextWatcher { + public static final AuthMethod DEFAULT_VALUE = AuthMethod.NONE; + + public interface EncryptionChangeHandler { + void onEncryptionChanged(byte[] newKey); + } + + private List entries; + private static final List entryValues = Arrays.asList( + AuthMethod.NONE, + AuthMethod.PASSWORD, + AuthMethod.PIN, + AuthMethod.DEVICE + ); + + private Settings settings; + private AuthMethod value = AuthMethod.NONE; + private EncryptionChangeHandler handler = null; + + private LinearLayout credentialsLayout; + private TextInputLayout passwordLayout; + private TextInputEditText passwordInput; + private EditText passwordConfirm; + + private Button btnSave; + + public CredentialsPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + settings = new Settings(context); + entries = Arrays.asList(context.getResources().getStringArray(R.array.settings_entries_auth)); + + setDialogLayoutResource(R.layout.component_authentication); + } + + public void setEncryptionChangeHandler(EncryptionChangeHandler handler) { + this.handler = handler; + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + + builder.setPositiveButton(null, null); + builder.setNegativeButton(null, null); + } + + @Override + protected void onBindDialogView(View view) { + value = settings.getAuthMethod(); + + ListView listView = view.findViewById(R.id.credentialSelection); + + ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_single_choice, entries); + listView.setAdapter(adapter); + + int index = entryValues.indexOf(value); + listView.setSelection(index); + listView.setItemChecked(index,true); + listView.setOnItemClickListener(this); + + credentialsLayout = view.findViewById(R.id.credentialsLayout); + + passwordLayout = view.findViewById(R.id.passwordLayout); + passwordInput = view.findViewById(R.id.passwordEdit); + passwordConfirm = view.findViewById(R.id.passwordConfirm); + + passwordInput.addTextChangedListener(this); + passwordConfirm.addTextChangedListener(this); + + Button btnCancel = view.findViewById(R.id.btnCancel); + btnSave = view.findViewById(R.id.btnSave); + + btnCancel.setOnClickListener(this); + btnSave.setOnClickListener(this); + + updateLayout(); + + super.onBindDialogView(view); + } + + @Override + protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { + if (restorePersistedValue) { + String stringValue = getPersistedString(DEFAULT_VALUE.name().toLowerCase()); + value = AuthMethod.valueOf(stringValue.toUpperCase()); + } else { + value = DEFAULT_VALUE; + persistString(value.name().toLowerCase()); + } + + setSummary(entries.get(entryValues.indexOf(value))); + } + + private void saveValues() { + byte[] newKey = null; + + if (settings.getEncryption() == EncryptionType.PASSWORD) { + if (value == AuthMethod.NONE || value == AuthMethod.DEVICE) { + Toast.makeText(getContext(), R.string.settings_toast_auth_invalid_with_encryption, Toast.LENGTH_LONG).show(); + return; + } + } + + if (value == AuthMethod.DEVICE) { + KeyguardManager km = (KeyguardManager) getContext().getSystemService(KEYGUARD_SERVICE); + + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { + Toast.makeText(getContext(), R.string.settings_toast_auth_device_pre_lollipop, Toast.LENGTH_LONG).show(); + return; + } else if (! km.isKeyguardSecure()) { + Toast.makeText(getContext(), R.string.settings_toast_auth_device_not_secure, Toast.LENGTH_LONG).show(); + return; + } + } + + if (value == AuthMethod.PASSWORD || value == AuthMethod.PIN) { + String password = passwordInput.getText().toString(); + if (!password.isEmpty()) { + newKey = settings.setAuthCredentials(value, password); + } else { + return; + } + } + + persistString(value.toString().toLowerCase()); + setSummary(entries.get(entryValues.indexOf(value))); + + if (newKey != null && handler != null) + handler.onEncryptionChanged(newKey); + } + + @Override + public void onClick(View view) { + switch (view.getId()) { + case (R.id.btnCancel): + getDialog().dismiss(); + break; + case (R.id.btnSave): + saveValues(); + getDialog().dismiss(); + break; + default: + break; + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + String password = passwordInput.getEditableText().toString(); + String confirm = passwordConfirm.getEditableText().toString(); + + if (password.equals(confirm) && ! password.isEmpty() && ! confirm.isEmpty()) { + btnSave.setEnabled(true); + } else { + btnSave.setEnabled(false); + } + } + + private void updateLayout() { + if (value == AuthMethod.NONE) { + credentialsLayout.setVisibility(View.GONE); + btnSave.setEnabled(true); + } else if (value == AuthMethod.PASSWORD) { + credentialsLayout.setVisibility(View.VISIBLE); + + passwordLayout.setHint(getContext().getString(R.string.settings_hint_password)); + passwordConfirm.setHint(R.string.settings_hint_password_confirm); + + passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + passwordConfirm.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + passwordInput.setTransformationMethod(new PasswordTransformationMethod()); + passwordConfirm.setTransformationMethod(new PasswordTransformationMethod()); + + btnSave.setEnabled(false); + } else if (value == AuthMethod.PIN) { + credentialsLayout.setVisibility(View.VISIBLE); + + passwordLayout.setHint(getContext().getString(R.string.settings_hint_pin)); + passwordConfirm.setHint(R.string.settings_hint_pin_confirm); + + passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + passwordConfirm.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + + passwordInput.setTransformationMethod(new PasswordTransformationMethod()); + passwordConfirm.setTransformationMethod(new PasswordTransformationMethod()); + + btnSave.setEnabled(false); + } else if (value == AuthMethod.DEVICE) { + credentialsLayout.setVisibility(View.GONE); + btnSave.setEnabled(true); + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + value = entryValues.get(position); + updateLayout(); + } + + // Needed stub functions + @Override + public void afterTextChanged(Editable s) {} + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java deleted file mode 100644 index b1c7f99a..00000000 --- a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (C) 2017 Jakob Nixdorf - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.shadowice.flocke.andotp.Preferences; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.res.TypedArray; -import android.preference.DialogPreference; -import android.support.design.widget.TextInputEditText; -import android.support.design.widget.TextInputLayout; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.text.method.PasswordTransformationMethod; -import android.util.AttributeSet; -import android.util.Base64; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.Toast; - -import org.shadowice.flocke.andotp.R; -import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; -import org.shadowice.flocke.andotp.Utilities.Settings; - -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; - -public class PBKDF2PasswordPreference extends DialogPreference - implements View.OnClickListener, TextWatcher { - - public enum Mode { - PASSWORD, PIN - } - - private static final String DEFAULT_VALUE = ""; - - private Settings settings; - private Mode mode = Mode.PASSWORD; - - private TextInputEditText passwordInput; - private EditText passwordConfirm; - - private Button btnSave; - - private String value = DEFAULT_VALUE; - - public PBKDF2PasswordPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - settings = new Settings(context); - - setDialogLayoutResource(R.layout.component_password); - } - - public void setMode(Mode mode) { - this.mode = mode; - } - - private void persistEncryptedString(String value) { - if (! value.isEmpty()) { - try { - int iter = EncryptionHelper.generateRandomIterations(); - EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(value, settings.getSalt(), iter); - persistString(Base64.encodeToString(credentials.password, Base64.URL_SAFE)); - settings.setIterations(Settings.AuthMethod.valueOf(mode.name()), iter); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - e.printStackTrace(); - } - } else { - Toast.makeText(getContext(), R.string.settings_toast_password_empty, Toast.LENGTH_LONG).show(); - } - } - - @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { - super.onPrepareDialogBuilder(builder); - - builder.setPositiveButton(null, null); - builder.setNegativeButton(null, null); - } - - @Override - protected void onBindDialogView(View view) { - TextInputLayout passwordLayout = view.findViewById(R.id.passwordLayout); - passwordInput = view.findViewById(R.id.passwordEdit); - passwordConfirm = view.findViewById(R.id.passwordConfirm); - - Button btnCancel = view.findViewById(R.id.btnCancel); - btnSave = view.findViewById(R.id.btnSave); - btnSave.setEnabled(false); - - btnCancel.setOnClickListener(this); - btnSave.setOnClickListener(this); - - if (! value.isEmpty()) { - passwordInput.setHint(R.string.settings_hint_unchanged); - } - - if (mode == Mode.PASSWORD) { - passwordLayout.setHint(getContext().getString(R.string.settings_hint_password)); - passwordConfirm.setHint(R.string.settings_hint_password_confirm); - - passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - passwordConfirm.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } else if (mode == Mode.PIN) { - passwordLayout.setHint(getContext().getString(R.string.settings_hint_pin)); - passwordConfirm.setHint(R.string.settings_hint_pin_confirm); - - passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - passwordConfirm.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - } - - passwordInput.setTransformationMethod(new PasswordTransformationMethod()); - passwordConfirm.setTransformationMethod(new PasswordTransformationMethod()); - - passwordConfirm.addTextChangedListener(this); - passwordInput.addTextChangedListener(this); - - super.onBindDialogView(view); - } - - @Override - protected Object onGetDefaultValue(TypedArray a, int index) { - return a.getString(index); - } - - @Override - protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { - if (restorePersistedValue) { - value = getPersistedString(DEFAULT_VALUE); - } else { - value = (String) defaultValue; - persistEncryptedString(value); - } - } - - @Override - public void onClick(View view) { - switch (view.getId()) { - case (R.id.btnCancel): - getDialog().dismiss(); - break; - case (R.id.btnSave): - value = passwordInput.getText().toString(); - persistEncryptedString(value); - - getDialog().dismiss(); - break; - default: - break; - } - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (passwordConfirm.getEditableText().toString().equals(passwordInput.getEditableText().toString())) { - btnSave.setEnabled(true); - } else { - btnSave.setEnabled(false); - } - } - - @Override - public void afterTextChanged(Editable s) {} - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} -} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java index feb42cbb..549fa3b5 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java @@ -23,6 +23,10 @@ package org.shadowice.flocke.andotp.Utilities; public class Constants { + public enum AuthMethod { + NONE, PASSWORD, PIN, DEVICE + } + public enum EncryptionType { KEYSTORE, PASSWORD } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java index d535be55..24d19e4d 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java @@ -29,6 +29,7 @@ import android.preference.PreferenceManager; import android.util.Base64; import android.widget.Toast; +import org.shadowice.flocke.andotp.Preferences.CredentialsPreference; import org.shadowice.flocke.andotp.R; import java.io.File; @@ -43,6 +44,8 @@ import java.util.Set; import static org.shadowice.flocke.andotp.Preferences.PasswordEncryptedPreference.KEY_ALIAS; import static org.shadowice.flocke.andotp.Utilities.EncryptionHelper.PBKDF2_OLD_DEFAULT_ITERATIONS; +import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; +import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType; public class Settings { private static final String DEFAULT_BACKUP_FOLDER = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "andOTP"; @@ -50,10 +53,6 @@ public class Settings { private Context context; private SharedPreferences settings; - public enum AuthMethod { - NONE, PASSWORD, PIN, DEVICE - } - public enum SortMode { UNSORTED, LABEL, LAST_USED } @@ -232,11 +231,11 @@ public class Settings { } public AuthMethod getAuthMethod() { - String authString = getString(R.string.settings_key_auth, R.string.settings_default_auth); + String authString = getString(R.string.settings_key_auth, CredentialsPreference.DEFAULT_VALUE.name().toLowerCase()); return AuthMethod.valueOf(authString.toUpperCase()); } - public String getAuthPassword() { + private String getAuthPassword() { return getString(R.string.settings_key_auth_password, ""); } @@ -256,7 +255,7 @@ public class Settings { setString(R.string.settings_key_auth_password_pbkdf2, password); } - public String getAuthPIN() { + private String getAuthPIN() { return getString(R.string.settings_key_auth_pin, ""); } @@ -276,6 +275,29 @@ public class Settings { setString(R.string.settings_key_auth_pin_pbkdf2, pin); } + public byte[] setAuthCredentials(AuthMethod method, String plainPassword) { + byte[] key = null; + + try { + int iterations = EncryptionHelper.generateRandomIterations(); + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, getSalt(), iterations); + String password = Base64.encodeToString(credentials.password, Base64.URL_SAFE); + + setIterations(method, iterations); + + if (method == AuthMethod.PASSWORD) + setAuthPasswordPBKDF2(password); + else if (method == AuthMethod.PIN) + setAuthPINPBKDF2(password); + + key = credentials.key; + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + e.printStackTrace(); + } + + return key; + } + public void setSalt(byte[] bytes) { String encodedSalt = Base64.encodeToString(bytes, Base64.URL_SAFE); setString(R.string.settings_key_auth_salt, encodedSalt); @@ -310,9 +332,9 @@ public class Settings { setInt(R.string.settings_key_auth_pin_iter, value); } - public Constants.EncryptionType getEncryption() { + public EncryptionType getEncryption() { String encType = getString(R.string.settings_key_encryption, R.string.settings_default_encryption); - return Constants.EncryptionType.valueOf(encType.toUpperCase()); + return EncryptionType.valueOf(encType.toUpperCase()); } public void setEncryption(String encryption) { diff --git a/app/src/main/res/layout/component_authentication.xml b/app/src/main/res/layout/component_authentication.xml new file mode 100644 index 00000000..d3339ade --- /dev/null +++ b/app/src/main/res/layout/component_authentication.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + +