From 08ad01b3dbbcfbebd8f2c9be18bc6a7203171ae4 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Wed, 27 Dec 2017 15:08:29 +0100 Subject: [PATCH 01/41] Only load the database after successful authentication --- .../andotp/Activities/MainActivity.java | 37 +++++++++++++------ .../andotp/View/EntriesCardAdapter.java | 7 +++- 2 files changed, 31 insertions(+), 13 deletions(-) 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 6cffff8a..aec82e9e 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 @@ -150,6 +150,23 @@ public class MainActivity extends BaseActivity settings.setSortMode(mode); } + private HashMap createTagsMap(ArrayList entries) { + HashMap tagsHashMap = new HashMap<>(); + + for(Entry entry : entries) { + for(String tag : entry.getTags()) + tagsHashMap.put(tag, settings.getTagToggle(tag)); + } + + return tagsHashMap; + } + + private void populateAdapter() { + adapter.loadEntries(); + tagsDrawerAdapter.setTags(createTagsMap(adapter.getEntries())); + adapter.filterByTags(tagsDrawerAdapter.getActiveTags()); + } + // Initialize the main application @Override protected void onCreate(Bundle savedInstanceState) { @@ -168,13 +185,14 @@ public class MainActivity extends BaseActivity PreferenceManager.setDefaultValues(this, R.xml.preferences, false); settings.registerPreferenceChangeListener(this); - if (savedInstanceState == null) + if (settings.getAuthMethod() != Settings.AuthMethod.NONE && savedInstanceState == null) requireAuthentication = true; setBroadcastCallback(new BroadcastReceivedCallback() { @Override public void onReceivedScreenOff() { - requireAuthentication = true; + if (settings.getAuthMethod() != Settings.AuthMethod.NONE) + requireAuthentication = true; } }); @@ -203,16 +221,10 @@ public class MainActivity extends BaseActivity llm.setOrientation(LinearLayoutManager.VERTICAL); recList.setLayoutManager(llm); - HashMap tagsHashMap = new HashMap<>(); - for(Entry entry : DatabaseHelper.loadDatabase(this)) { - for(String tag : entry.getTags()) - tagsHashMap.put(tag, settings.getTagToggle(tag)); - } - tagsDrawerAdapter = new TagsAdapter(this, tagsHashMap); - + tagsDrawerAdapter = new TagsAdapter(this, new HashMap()); adapter = new EntriesCardAdapter(this, tagsDrawerAdapter); - recList.setAdapter(adapter); + recList.setAdapter(adapter); recList.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { @@ -292,9 +304,11 @@ public class MainActivity extends BaseActivity public void onResume() { super.onResume(); - if (requireAuthentication) { + if (settings.getAuthMethod() != Settings.AuthMethod.NONE && requireAuthentication) { requireAuthentication = false; authenticate(); + } else { + populateAdapter(); } startUpdater(); @@ -360,6 +374,7 @@ public class MainActivity extends BaseActivity } } else { requireAuthentication = false; + populateAdapter(); } } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java index e7ccda64..3e4f48d9 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java @@ -83,8 +83,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter this.tagsFilterAdapter = tagsFilterAdapter; this.settings = new Settings(context); this.taskHandler = new Handler(); - - loadEntries(); + this.entries = new ArrayList<>(); } @Override @@ -92,6 +91,10 @@ public class EntriesCardAdapter extends RecyclerView.Adapter return displayedEntries.size(); } + public ArrayList getEntries() { + return entries; + } + public void addEntry(Entry e) { if (! entries.contains(e)) { entries.add(e); From cb155823f715e1558251e81dd7098be25315be5d Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Wed, 27 Dec 2017 15:44:45 +0100 Subject: [PATCH 02/41] Remove exportAsJSON function from DatabaseHelper --- .../flocke/andotp/Activities/BackupActivity.java | 4 ++-- .../flocke/andotp/Utilities/DatabaseHelper.java | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java index 41414e69..03a71cc6 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java @@ -399,9 +399,9 @@ public class BackupActivity extends BaseActivity { private void doBackupPlain(Uri uri) { if (Tools.isExternalStorageWritable()) { - boolean success = DatabaseHelper.exportAsJSON(this, uri); + ArrayList entries = DatabaseHelper.loadDatabase(this); - if (success) + if (FileHelper.writeStringToFile(this, uri, DatabaseHelper.entriesToString(entries))) Toast.makeText(this, R.string.backup_toast_export_success, Toast.LENGTH_LONG).show(); else Toast.makeText(this, R.string.backup_toast_export_failed, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java index c870130a..baaa6718 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java @@ -24,7 +24,6 @@ package org.shadowice.flocke.andotp.Utilities; import android.content.Context; -import android.net.Uri; import org.json.JSONArray; import org.shadowice.flocke.andotp.Database.Entry; @@ -108,12 +107,4 @@ public class DatabaseHelper { return entries; } - - /* Export functions */ - - public static boolean exportAsJSON(Context context, Uri file) { - ArrayList entries = loadDatabase(context); - - return FileHelper.writeStringToFile(context, file, entriesToString(entries)); - } } \ No newline at end of file From bb55a5391e4dd0abf1bf6123dcbf86628c978247 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Wed, 27 Dec 2017 15:46:30 +0100 Subject: [PATCH 03/41] Fix Travis builds --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dd99d4d4..d293a1d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ android: - platform-tools - tools - - build-tools-27.0.2 + - build-tools-27.0.3 - android-27 - extra-google-m2repository From c96c7d94bc5db38031e40853f6e35e7fd28e986a Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Wed, 27 Dec 2017 18:18:06 +0100 Subject: [PATCH 04/41] Load the encryption key just once and cache it inside the adapter --- .../andotp/Activities/BackupActivity.java | 22 +++++++++++---- .../andotp/Activities/MainActivity.java | 22 ++++++++++++++- .../Activities/PanicResponderActivity.java | 5 +--- .../andotp/Utilities/DatabaseHelper.java | 14 +++------- .../andotp/View/EntriesCardAdapter.java | 28 +++++++++++++------ 5 files changed, 63 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java index 03a71cc6..634d8692 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java @@ -62,6 +62,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; public class BackupActivity extends BaseActivity { private final static int INTENT_OPEN_DOCUMENT_PLAIN = 200; @@ -87,6 +88,10 @@ public class BackupActivity extends BaseActivity { private static final String DEFAULT_BACKUP_MIMETYPE_CRYPT = "binary/aes"; private static final String DEFAULT_BACKUP_MIMETYPE_PGP = "application/pgp-encrypted"; + public static final String ENCRYPTION_KEY_PARAM = "encryption_key"; + + private SecretKey encryptionKey = null; + private OpenPgpServiceConnection pgpServiceConnection; private long pgpKeyId; @@ -111,6 +116,13 @@ public class BackupActivity extends BaseActivity { stub.setLayoutResource(R.layout.content_backup); View v = stub.inflate(); + Bundle extras = getIntent().getExtras(); + if (extras != null) { + byte[] keyMaterial = extras.getByteArray(ENCRYPTION_KEY_PARAM); + if (keyMaterial != null) + encryptionKey = new SecretKeySpec(keyMaterial, 0, keyMaterial.length, "AES"); + } + // Plain-text LinearLayout backupPlain = v.findViewById(R.id.button_backup_plain); @@ -367,13 +379,13 @@ public class BackupActivity extends BaseActivity { if (entries.size() > 0) { if (! replace.isChecked()) { - ArrayList currentEntries = DatabaseHelper.loadDatabase(this); + ArrayList currentEntries = DatabaseHelper.loadDatabase(this, encryptionKey); entries.removeAll(currentEntries); entries.addAll(currentEntries); } - if (DatabaseHelper.saveDatabase(this, entries)) { + if (DatabaseHelper.saveDatabase(this, entries, encryptionKey)) { reload = true; Toast.makeText(this, R.string.backup_toast_import_success, Toast.LENGTH_LONG).show(); finishWithResult(); @@ -399,7 +411,7 @@ public class BackupActivity extends BaseActivity { private void doBackupPlain(Uri uri) { if (Tools.isExternalStorageWritable()) { - ArrayList entries = DatabaseHelper.loadDatabase(this); + ArrayList entries = DatabaseHelper.loadDatabase(this, encryptionKey); if (FileHelper.writeStringToFile(this, uri, DatabaseHelper.entriesToString(entries))) Toast.makeText(this, R.string.backup_toast_export_success, Toast.LENGTH_LONG).show(); @@ -472,7 +484,7 @@ public class BackupActivity extends BaseActivity { if (! password.isEmpty()) { if (Tools.isExternalStorageWritable()) { - ArrayList entries = DatabaseHelper.loadDatabase(this); + ArrayList entries = DatabaseHelper.loadDatabase(this, encryptionKey); String plain = DatabaseHelper.entriesToString(entries); boolean success = true; @@ -534,7 +546,7 @@ public class BackupActivity extends BaseActivity { } private void backupEncryptedWithPGP(Uri uri, Intent encryptIntent) { - ArrayList entries = DatabaseHelper.loadDatabase(this); + ArrayList entries = DatabaseHelper.loadDatabase(this, encryptionKey); String plainJSON = DatabaseHelper.entriesToString(entries); if (encryptIntent == null) { 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 aec82e9e..374ad296 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,7 +58,7 @@ 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.DatabaseHelper; +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; @@ -67,13 +67,20 @@ import org.shadowice.flocke.andotp.View.ItemTouchHelper.SimpleItemTouchHelperCal import org.shadowice.flocke.andotp.View.ManualEntryDialog; import org.shadowice.flocke.andotp.View.TagsAdapter; +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.HashMap; +import javax.crypto.SecretKey; + import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; public class MainActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener { + public static final String KEY_FILE = "otp.key"; + private static final int INTENT_INTERNAL_AUTHENTICATE = 100; private static final int INTENT_INTERNAL_SETTINGS = 101; private static final int INTENT_INTERNAL_BACKUP = 102; @@ -161,6 +168,15 @@ public class MainActivity extends BaseActivity return tagsHashMap; } + private SecretKey loadEncryptionKeyFromKeyStore() { + try { + return KeyStoreHelper.loadOrGenerateWrappedKey(this, new File(getFilesDir() + "/" + KEY_FILE)); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + return null; + } + } + private void populateAdapter() { adapter.loadEntries(); tagsDrawerAdapter.setTags(createTagsMap(adapter.getEntries())); @@ -308,6 +324,7 @@ public class MainActivity extends BaseActivity requireAuthentication = false; authenticate(); } else { + adapter.setEncryptionKey(loadEncryptionKeyFromKeyStore()); populateAdapter(); } @@ -374,6 +391,8 @@ public class MainActivity extends BaseActivity } } else { requireAuthentication = false; + + adapter.setEncryptionKey(loadEncryptionKeyFromKeyStore()); populateAdapter(); } } @@ -453,6 +472,7 @@ public class MainActivity extends BaseActivity if (id == R.id.action_backup) { Intent backupIntent = new Intent(this, BackupActivity.class); + backupIntent.putExtra(BackupActivity.ENCRYPTION_KEY_PARAM, adapter.getEncryptionKey().getEncoded()); startActivityForResult(backupIntent, INTENT_INTERNAL_BACKUP); } else if (id == R.id.action_settings) { Intent settingsIntent = new Intent(this, SettingsActivity.class); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java index c927e17e..ef6a2c8b 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java @@ -27,11 +27,8 @@ import android.content.Intent; import android.os.Build; import android.os.Bundle; -import org.shadowice.flocke.andotp.Database.Entry; -import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; import org.shadowice.flocke.andotp.Utilities.Settings; -import java.util.ArrayList; import java.util.Set; public class PanicResponderActivity extends Activity { @@ -48,7 +45,7 @@ public class PanicResponderActivity extends Activity { Set response = settings.getPanicResponse(); if (response.contains("accounts")) - DatabaseHelper.saveDatabase(this, new ArrayList()); + // TODO: wipe database by deleting the files if (response.contains("settings")) settings.clear(true); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java index baaa6718..484673ab 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java @@ -34,19 +34,15 @@ import java.util.ArrayList; import javax.crypto.SecretKey; public class DatabaseHelper { - public static final String KEY_FILE = "otp.key"; public static final String SETTINGS_FILE = "secrets.dat"; /* Database functions */ - public static boolean saveDatabase(Context context, ArrayList entries) { + public static boolean saveDatabase(Context context, ArrayList entries, SecretKey encryptionKey) { String jsonString = entriesToString(entries); try { - byte[] data = jsonString.getBytes(); - - SecretKey key = KeyStoreHelper.loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE)); - data = EncryptionHelper.encrypt(key, data); + byte[] data = EncryptionHelper.encrypt(encryptionKey, jsonString.getBytes()); FileHelper.writeBytesToFile(new File(context.getFilesDir() + "/" + SETTINGS_FILE), data); @@ -58,14 +54,12 @@ public class DatabaseHelper { return true; } - public static ArrayList loadDatabase(Context context){ + public static ArrayList loadDatabase(Context context, SecretKey encryptionKey){ ArrayList entries = new ArrayList<>(); try { byte[] data = FileHelper.readFileToBytes(new File(context.getFilesDir() + "/" + SETTINGS_FILE)); - - SecretKey key = KeyStoreHelper.loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE)); - data = EncryptionHelper.decrypt(key, data); + data = EncryptionHelper.decrypt(encryptionKey, data); entries = stringToEntries(new String(data)); } catch (Exception error) { diff --git a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java index 3e4f48d9..c113c375 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java @@ -62,6 +62,8 @@ import java.util.HashSet; import java.util.List; import java.util.concurrent.Callable; +import javax.crypto.SecretKey; + import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; public class EntriesCardAdapter extends RecyclerView.Adapter @@ -74,6 +76,8 @@ public class EntriesCardAdapter extends RecyclerView.Adapter private Callback callback; private List tagsFilter = new ArrayList<>(); + private SecretKey encryptionKey = null; + private SortMode sortMode = SortMode.UNSORTED; private TagsAdapter tagsFilterAdapter; private Settings settings; @@ -86,6 +90,14 @@ public class EntriesCardAdapter extends RecyclerView.Adapter this.entries = new ArrayList<>(); } + public void setEncryptionKey(SecretKey key) { + encryptionKey = key; + } + + public SecretKey getEncryptionKey() { + return encryptionKey; + } + @Override public int getItemCount() { return displayedEntries.size(); @@ -115,11 +127,11 @@ public class EntriesCardAdapter extends RecyclerView.Adapter } public void saveEntries() { - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); } public void loadEntries() { - entries = DatabaseHelper.loadDatabase(context); + entries = DatabaseHelper.loadDatabase(context, encryptionKey); entriesChanged(); } @@ -258,7 +270,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter displayedEntries.get(position).setLastUsed(timeStamp); entries.get(realIndex).setLastUsed(timeStamp); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); if (sortMode == SortMode.LAST_USED) { displayedEntries = sortEntries(displayedEntries); @@ -277,7 +289,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter displayedEntries = new ArrayList<>(entries); notifyItemMoved(fromPosition, toPosition); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); } return true; @@ -317,7 +329,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter Entry e = entries.get(realIndex); e.setLabel(newLabel); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); } }) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @@ -398,7 +410,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter Entry e = entries.get(realIndex); e.setThumbnail(thumbnail); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); notifyItemChanged(pos); alert.cancel(); } @@ -426,7 +438,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter @Override public Object call() throws Exception { entries.get(realPos).setTags(tagsAdapter.getActiveTags()); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); List inUseTags = getTags(); @@ -471,7 +483,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter notifyItemRemoved(pos); entries.remove(realIndex); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); } }) .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { From b301a4fa8047a0cea19ef308a62b58c09044a6dc Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Thu, 28 Dec 2017 09:55:16 +0100 Subject: [PATCH 05/41] Show an error message if loading the key fails --- .../andotp/Activities/MainActivity.java | 4 ++ .../flocke/andotp/Utilities/UIHelper.java | 42 +++++++++++++++++++ app/src/main/res/values/strings_main.xml | 3 ++ 3 files changed, 49 insertions(+) create mode 100644 app/src/main/java/org/shadowice/flocke/andotp/Utilities/UIHelper.java 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 374ad296..c6cfeb4b 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 @@ -61,6 +61,7 @@ import org.shadowice.flocke.andotp.R; 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.Utilities.UIHelper; import org.shadowice.flocke.andotp.View.EntriesCardAdapter; import org.shadowice.flocke.andotp.View.FloatingActionMenu; import org.shadowice.flocke.andotp.View.ItemTouchHelper.SimpleItemTouchHelperCallback; @@ -173,6 +174,9 @@ public class MainActivity extends BaseActivity return KeyStoreHelper.loadOrGenerateWrappedKey(this, new File(getFilesDir() + "/" + KEY_FILE)); } catch (GeneralSecurityException | IOException e) { e.printStackTrace(); + + UIHelper.showGenericDialog(this, R.string.dialog_title_keystore_error, R.string.dialog_msg_keystore_error); + return null; } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/UIHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/UIHelper.java new file mode 100644 index 00000000..519624ab --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/UIHelper.java @@ -0,0 +1,42 @@ +/* + * 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.Utilities; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; + +public class UIHelper { + public static void showGenericDialog(Context context, int titleId, int messageId) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(titleId) + .setMessage(messageId) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + } + }) + .create() + .show(); + } +} diff --git a/app/src/main/res/values/strings_main.xml b/app/src/main/res/values/strings_main.xml index 6a6a2a29..5453bdd2 100644 --- a/app/src/main/res/values/strings_main.xml +++ b/app/src/main/res/values/strings_main.xml @@ -63,6 +63,7 @@ Rename Security and Backups Last used + KeyStore error Please enter your device credentials to start andOTP. Are you sure you want do remove the account \"%1$s\"? @@ -83,4 +84,6 @@ In order for andOTP to recognize which token was used last you have to have \"tap to reveal\" enabled or use the copy button.\n\nThis message will not be shown again. + Failed to load the encryption key from the KeyStore. + You won\'t be able to use this app. Please contact the developer and provide a logcat! From d284ce18cf5558bf0ee7c45982fc73ffe6ac05f9 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Thu, 28 Dec 2017 10:35:59 +0100 Subject: [PATCH 06/41] Load the encryption key in the BackupActivity directly --- .../andotp/Activities/BackupActivity.java | 9 ++----- .../andotp/Activities/MainActivity.java | 25 ++----------------- .../andotp/Utilities/KeyStoreHelper.java | 16 ++++++++++++ 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java index 634d8692..fc833ab8 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java @@ -53,6 +53,7 @@ import org.shadowice.flocke.andotp.R; import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; import org.shadowice.flocke.andotp.Utilities.FileHelper; +import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; import org.shadowice.flocke.andotp.Utilities.Tools; import java.io.ByteArrayInputStream; @@ -62,7 +63,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; public class BackupActivity extends BaseActivity { private final static int INTENT_OPEN_DOCUMENT_PLAIN = 200; @@ -116,12 +116,7 @@ public class BackupActivity extends BaseActivity { stub.setLayoutResource(R.layout.content_backup); View v = stub.inflate(); - Bundle extras = getIntent().getExtras(); - if (extras != null) { - byte[] keyMaterial = extras.getByteArray(ENCRYPTION_KEY_PARAM); - if (keyMaterial != null) - encryptionKey = new SecretKeySpec(keyMaterial, 0, keyMaterial.length, "AES"); - } + encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(this); // Plain-text 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 c6cfeb4b..223dc416 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 @@ -61,27 +61,19 @@ import org.shadowice.flocke.andotp.R; 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.Utilities.UIHelper; import org.shadowice.flocke.andotp.View.EntriesCardAdapter; import org.shadowice.flocke.andotp.View.FloatingActionMenu; import org.shadowice.flocke.andotp.View.ItemTouchHelper.SimpleItemTouchHelperCallback; import org.shadowice.flocke.andotp.View.ManualEntryDialog; import org.shadowice.flocke.andotp.View.TagsAdapter; -import java.io.File; -import java.io.IOException; -import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.HashMap; -import javax.crypto.SecretKey; - import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; public class MainActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener { - public static final String KEY_FILE = "otp.key"; - private static final int INTENT_INTERNAL_AUTHENTICATE = 100; private static final int INTENT_INTERNAL_SETTINGS = 101; private static final int INTENT_INTERNAL_BACKUP = 102; @@ -169,18 +161,6 @@ public class MainActivity extends BaseActivity return tagsHashMap; } - private SecretKey loadEncryptionKeyFromKeyStore() { - try { - return KeyStoreHelper.loadOrGenerateWrappedKey(this, new File(getFilesDir() + "/" + KEY_FILE)); - } catch (GeneralSecurityException | IOException e) { - e.printStackTrace(); - - UIHelper.showGenericDialog(this, R.string.dialog_title_keystore_error, R.string.dialog_msg_keystore_error); - - return null; - } - } - private void populateAdapter() { adapter.loadEntries(); tagsDrawerAdapter.setTags(createTagsMap(adapter.getEntries())); @@ -328,7 +308,7 @@ public class MainActivity extends BaseActivity requireAuthentication = false; authenticate(); } else { - adapter.setEncryptionKey(loadEncryptionKeyFromKeyStore()); + adapter.setEncryptionKey(KeyStoreHelper.loadEncryptionKeyFromKeyStore(this)); populateAdapter(); } @@ -396,7 +376,7 @@ public class MainActivity extends BaseActivity } else { requireAuthentication = false; - adapter.setEncryptionKey(loadEncryptionKeyFromKeyStore()); + adapter.setEncryptionKey(KeyStoreHelper.loadEncryptionKeyFromKeyStore(this)); populateAdapter(); } } @@ -476,7 +456,6 @@ public class MainActivity extends BaseActivity if (id == R.id.action_backup) { Intent backupIntent = new Intent(this, BackupActivity.class); - backupIntent.putExtra(BackupActivity.ENCRYPTION_KEY_PARAM, adapter.getEncryptionKey().getEncoded()); startActivityForResult(backupIntent, INTENT_INTERNAL_BACKUP); } else if (id == R.id.action_settings) { Intent settingsIntent = new Intent(this, SettingsActivity.class); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java index 6ea4854c..1008036c 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java @@ -28,6 +28,8 @@ import android.security.KeyPairGeneratorSpec; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; +import org.shadowice.flocke.andotp.R; + import java.io.File; import java.io.IOException; import java.math.BigInteger; @@ -45,6 +47,8 @@ import javax.crypto.spec.SecretKeySpec; import javax.security.auth.x500.X500Principal; public class KeyStoreHelper { + public static final String KEY_FILE = "otp.key"; + private final static int KEY_LENGTH = 16; public static KeyPair loadOrGenerateAsymmetricKeyPair(Context context, String alias) @@ -113,4 +117,16 @@ public class KeyStoreHelper { return wrapper.unwrap(wrapped); } + + public static SecretKey loadEncryptionKeyFromKeyStore(Context context) { + try { + return loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE)); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + + UIHelper.showGenericDialog(context, R.string.dialog_title_keystore_error, R.string.dialog_msg_keystore_error); + + return null; + } + } } From 84a96e393242d35c375c7f3472fcaa7bf22d42d4 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Thu, 28 Dec 2017 10:47:49 +0100 Subject: [PATCH 07/41] Fix deleting the database when receiving a Panic Trigger --- .../andotp/Activities/PanicResponderActivity.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java index ef6a2c8b..59a8890f 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java @@ -29,8 +29,12 @@ import android.os.Bundle; import org.shadowice.flocke.andotp.Utilities.Settings; +import java.io.File; import java.util.Set; +import static org.shadowice.flocke.andotp.Utilities.DatabaseHelper.SETTINGS_FILE; +import static org.shadowice.flocke.andotp.Utilities.KeyStoreHelper.KEY_FILE; + public class PanicResponderActivity extends Activity { public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER"; @@ -44,8 +48,13 @@ public class PanicResponderActivity extends Activity { Set response = settings.getPanicResponse(); - if (response.contains("accounts")) - // TODO: wipe database by deleting the files + if (response.contains("accounts")) { + File database = new File(getFilesDir() + "/" + SETTINGS_FILE); + File key = new File(getFilesDir() + "/" + KEY_FILE); + + database.delete(); + key.delete(); + } if (response.contains("settings")) settings.clear(true); From d40cce6c1b7929be535aa2ca08b284f6fa3df70f Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Thu, 28 Dec 2017 11:39:31 +0100 Subject: [PATCH 08/41] Also delete the keystore entry when receiving a Panic Trigger --- .../andotp/Activities/PanicResponderActivity.java | 15 +++++++++++++++ .../flocke/andotp/Utilities/KeyStoreHelper.java | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java index 59a8890f..568a873b 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java @@ -30,9 +30,13 @@ import android.os.Bundle; import org.shadowice.flocke.andotp.Utilities.Settings; import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; import java.util.Set; import static org.shadowice.flocke.andotp.Utilities.DatabaseHelper.SETTINGS_FILE; +import static org.shadowice.flocke.andotp.Utilities.KeyStoreHelper.KEYSTORE_ALIAS_WRAPPING; import static org.shadowice.flocke.andotp.Utilities.KeyStoreHelper.KEY_FILE; public class PanicResponderActivity extends Activity { @@ -54,6 +58,17 @@ public class PanicResponderActivity extends Activity { database.delete(); key.delete(); + + try { + final KeyStore keyStore = java.security.KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + if (keyStore.containsAlias(KEYSTORE_ALIAS_WRAPPING)) { + keyStore.deleteEntry(KEYSTORE_ALIAS_WRAPPING); + } + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + } } if (response.contains("settings")) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java index 1008036c..7ea3c19a 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java @@ -49,6 +49,8 @@ import javax.security.auth.x500.X500Principal; public class KeyStoreHelper { public static final String KEY_FILE = "otp.key"; + public static final String KEYSTORE_ALIAS_WRAPPING = "settings"; + private final static int KEY_LENGTH = 16; public static KeyPair loadOrGenerateAsymmetricKeyPair(Context context, String alias) @@ -97,7 +99,7 @@ public class KeyStoreHelper { */ public static SecretKey loadOrGenerateWrappedKey(Context context, File keyFile) throws GeneralSecurityException, IOException { - final SecretKeyWrapper wrapper = new SecretKeyWrapper(context, "settings"); + final SecretKeyWrapper wrapper = new SecretKeyWrapper(context, KEYSTORE_ALIAS_WRAPPING); // Generate secret key if none exists if (!keyFile.exists()) { From ded32a709098756d3c5025aa3e9d726713b6fe65 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Fri, 29 Dec 2017 11:30:13 +0100 Subject: [PATCH 09/41] Use PBKDF2 to store and verify the credentials --- .../Activities/AuthenticateActivity.java | 68 +++++++++++++++++-- .../andotp/Activities/SettingsActivity.java | 18 ++--- ...nce.java => PBKDF2PasswordPreference.java} | 34 +++++++--- .../flocke/andotp/Utilities/Constants.java | 30 ++++++++ .../andotp/Utilities/EncryptionHelper.java | 42 +++++++++++- .../andotp/Utilities/KeyStoreHelper.java | 4 +- .../andotp/Utilities/SecretKeyWrapper.java | 4 +- .../flocke/andotp/Utilities/Settings.java | 65 ++++++++++++++++-- app/src/main/res/values/settings.xml | 3 + app/src/main/res/values/strings_settings.xml | 3 + 10 files changed, 234 insertions(+), 37 deletions(-) rename app/src/main/java/org/shadowice/flocke/andotp/Preferences/{PasswordHashPreference.java => PBKDF2PasswordPreference.java} (84%) create mode 100644 app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java 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 23b44379..8034c8fb 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 @@ -29,6 +29,7 @@ import android.support.design.widget.TextInputLayout; import android.support.v7.widget.Toolbar; import android.text.InputType; import android.text.method.PasswordTransformationMethod; +import android.util.Base64; import android.view.KeyEvent; import android.view.View; import android.view.ViewStub; @@ -41,6 +42,11 @@ import android.widget.Toast; import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; import static org.shadowice.flocke.andotp.Utilities.Settings.AuthMethod; @@ -48,6 +54,9 @@ public class AuthenticateActivity extends ThemedActivity implements EditText.OnEditorActionListener { private String password; + AuthMethod authMethod; + boolean oldPassword = false; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -67,10 +76,15 @@ public class AuthenticateActivity extends ThemedActivity TextInputLayout passwordLayout = v.findViewById(R.id.passwordLayout); TextInputEditText passwordInput = v.findViewById(R.id.passwordEdit); - AuthMethod authMethod = settings.getAuthMethod(); + authMethod = settings.getAuthMethod(); if (authMethod == AuthMethod.PASSWORD) { - password = settings.getAuthPasswordHash(); + password = settings.getAuthPasswordPBKDF2(); + + if (password.isEmpty()) { + password = settings.getAuthPasswordHash(); + oldPassword = true; + } if (password.isEmpty()) { Toast.makeText(this, R.string.auth_toast_password_missing, Toast.LENGTH_LONG).show(); @@ -81,7 +95,12 @@ public class AuthenticateActivity extends ThemedActivity passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } } else if (authMethod == AuthMethod.PIN) { - password = settings.getAuthPINHash(); + password = settings.getAuthPINPBKDF2(); + + if (password.isEmpty()) { + password = settings.getAuthPINHash(); + oldPassword = true; + } if (password.isEmpty()) { Toast.makeText(this, R.string.auth_toast_pin_missing, Toast.LENGTH_LONG).show(); @@ -104,12 +123,47 @@ public class AuthenticateActivity extends ThemedActivity @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_DONE) { - String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(v.getText().toString()))); + if (! oldPassword) { + try { + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(v.getText().toString(), settings.getSalt()); + byte[] passwordArray = Base64.decode(password, Base64.URL_SAFE); - if (hashedPassword.equals(password)) { - finishWithResult(true); + if (Arrays.equals(passwordArray, credentials.password)) { + finishWithResult(true); + } else { + finishWithResult(false); + } + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + e.printStackTrace(); + finishWithResult(false); + } } else { - finishWithResult(false); + String plainPassword = v.getText().toString(); + String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword))); + + if (hashedPassword.equals(password)) { + try { + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, settings.getSalt()); + String base64 = Base64.encodeToString(credentials.password, Base64.URL_SAFE); + + if (authMethod == AuthMethod.PASSWORD) + settings.setAuthPasswordPBKDF2(base64); + else if (authMethod == AuthMethod.PIN) + settings.setAuthPINPBKDF2(base64); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + Toast.makeText(this, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); + e.printStackTrace(); + } + + if (authMethod == AuthMethod.PASSWORD) + settings.removeAuthPasswordHash(); + else if (authMethod == AuthMethod.PIN) + settings.removeAuthPINHash(); + + finishWithResult(true); + } else { + finishWithResult(false); + } } return true; 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 89d38066..67835482 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 @@ -37,7 +37,7 @@ import android.widget.Toast; import org.openintents.openpgp.util.OpenPgpAppPreference; import org.openintents.openpgp.util.OpenPgpKeyPreference; -import org.shadowice.flocke.andotp.Preferences.PasswordHashPreference; +import org.shadowice.flocke.andotp.Preferences.PBKDF2PasswordPreference; import org.shadowice.flocke.andotp.R; public class SettingsActivity extends BaseActivity @@ -108,8 +108,8 @@ public class SettingsActivity extends BaseActivity OpenPgpKeyPreference pgpKey; public void updateAuthPassword(String newAuth) { - PasswordHashPreference pwPref = (PasswordHashPreference) catSecurity.findPreference(getString(R.string.settings_key_auth_password_hash)); - PasswordHashPreference pinPref = (PasswordHashPreference) catSecurity.findPreference(getString(R.string.settings_key_auth_pin_hash)); + 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); @@ -118,22 +118,22 @@ public class SettingsActivity extends BaseActivity switch (newAuth) { case "password": - PasswordHashPreference authPassword = new PasswordHashPreference(getActivity(), null); + 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_hash)); - authPassword.setMode(PasswordHashPreference.Mode.PASSWORD); + authPassword.setKey(getString(R.string.settings_key_auth_password_pbkdf2)); + authPassword.setMode(PBKDF2PasswordPreference.Mode.PASSWORD); catSecurity.addPreference(authPassword); break; case "pin": - PasswordHashPreference authPIN = new PasswordHashPreference(getActivity(), null); + 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_hash)); - authPIN.setMode(PasswordHashPreference.Mode.PIN); + authPIN.setKey(getString(R.string.settings_key_auth_pin_pbkdf2)); + authPIN.setMode(PBKDF2PasswordPreference.Mode.PIN); catSecurity.addPreference(authPIN); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordHashPreference.java b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java similarity index 84% rename from app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordHashPreference.java rename to app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java index bfe8e24b..d0841dbf 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordHashPreference.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java @@ -33,15 +33,19 @@ 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 org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; +import org.shadowice.flocke.andotp.Utilities.Settings; -public class PasswordHashPreference extends DialogPreference +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +public class PBKDF2PasswordPreference extends DialogPreference implements View.OnClickListener, TextWatcher { public enum Mode { @@ -50,6 +54,7 @@ public class PasswordHashPreference extends DialogPreference private static final String DEFAULT_VALUE = ""; + private Settings settings; private Mode mode = Mode.PASSWORD; private TextInputEditText passwordInput; @@ -59,9 +64,11 @@ public class PasswordHashPreference extends DialogPreference private String value = DEFAULT_VALUE; - public PasswordHashPreference(Context context, AttributeSet attrs) { + public PBKDF2PasswordPreference(Context context, AttributeSet attrs) { super(context, attrs); + settings = new Settings(context); + setDialogLayoutResource(R.layout.component_password); } @@ -69,6 +76,19 @@ public class PasswordHashPreference extends DialogPreference this.mode = mode; } + private void persistEncryptedString(String value) { + if (value.isEmpty()) { + persistString(value); + } else { + try { + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(value, settings.getSalt()); + persistString(Base64.encodeToString(credentials.password, Base64.URL_SAFE)); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + e.printStackTrace(); + } + } + } + @Override protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { super.onPrepareDialogBuilder(builder); @@ -128,7 +148,7 @@ public class PasswordHashPreference extends DialogPreference value = getPersistedString(DEFAULT_VALUE); } else { value = (String) defaultValue; - persistString(value); + persistEncryptedString(value); } } @@ -140,9 +160,7 @@ public class PasswordHashPreference extends DialogPreference break; case (R.id.btnSave): value = passwordInput.getText().toString(); - String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(value))); - - persistString(hashedPassword); + persistEncryptedString(value); getDialog().dismiss(); break; 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 new file mode 100644 index 00000000..a1a644b1 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java @@ -0,0 +1,30 @@ +/* + * 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.Utilities; + +public class Constants { + final static String ALGORITHM_SYMMETRIC = "AES/GCM/NoPadding"; + final static String ALGORITHM_ASYMMETRIC = "RSA/ECB/PKCS1Padding"; + + final static int AUTH_SALT_LENGTH = 16; +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java index aabff962..9fd6891a 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java @@ -32,6 +32,9 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.ArrayList; import java.util.Arrays; import javax.crypto.BadPaddingException; @@ -39,15 +42,48 @@ import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; -public class EncryptionHelper { - private final static String ALGORITHM_SYMMETRIC = "AES/GCM/NoPadding"; - private final static String ALGORITHM_ASYMMETRIC = "RSA/ECB/PKCS1Padding"; +import static org.shadowice.flocke.andotp.Utilities.Constants.ALGORITHM_ASYMMETRIC; +import static org.shadowice.flocke.andotp.Utilities.Constants.ALGORITHM_SYMMETRIC; +public class EncryptionHelper { private final static int IV_LENGTH = 12; + private final static int PBKDF2_ITERATIONS = 1000; + private final static int PBKDF2_LENGTH = 512; + + public static class PBKDF2Credentials { + public byte[] password; + public byte[] seed; + } + + public static byte[] generateRandom(int length) { + final byte[] raw = new byte[length]; + new SecureRandom().nextBytes(raw); + + return raw; + } + + public static PBKDF2Credentials generatePBKDF2Credentials(String password, byte[] salt) + throws NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, PBKDF2_ITERATIONS, PBKDF2_LENGTH); + + byte[] array = secretKeyFactory.generateSecret(keySpec).getEncoded(); + + int halfPoint = array.length / 2; + + PBKDF2Credentials credentials = new PBKDF2Credentials(); + credentials.password = Arrays.copyOfRange(array, halfPoint, array.length); + credentials.seed = Arrays.copyOfRange(array, 0, halfPoint - 1); + + return credentials; + } + public static SecretKey generateSymmetricKeyFromPassword(String password) throws NoSuchAlgorithmException { MessageDigest sha = MessageDigest.getInstance("SHA-256"); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java index 7ea3c19a..d8490d87 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java @@ -37,7 +37,6 @@ import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; -import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; import java.util.Calendar; import java.util.GregorianCalendar; @@ -103,8 +102,7 @@ public class KeyStoreHelper { // Generate secret key if none exists if (!keyFile.exists()) { - final byte[] raw = new byte[KEY_LENGTH]; - new SecureRandom().nextBytes(raw); + final byte[] raw = EncryptionHelper.generateRandom(KEY_LENGTH); final SecretKey key = new SecretKeySpec(raw, "AES"); final byte[] wrapped = wrapper.wrap(key); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java index 0c5ca8f2..0e6be63f 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java @@ -27,6 +27,8 @@ import java.security.KeyStore; import javax.crypto.Cipher; import javax.crypto.SecretKey; +import static org.shadowice.flocke.andotp.Utilities.Constants.ALGORITHM_ASYMMETRIC; + /** * Wraps {@link SecretKey} instances using a public/private key pair stored in * the platform {@link KeyStore}. This allows us to protect symmetric keys with @@ -48,7 +50,7 @@ public class SecretKeyWrapper { @SuppressLint("GetInstance") public SecretKeyWrapper(Context context, String alias) throws GeneralSecurityException, IOException { - mCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + mCipher = Cipher.getInstance(ALGORITHM_ASYMMETRIC); mPair = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, alias); } 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 d391e4e6..2dafc180 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 @@ -27,14 +27,15 @@ import android.content.SharedPreferences; import android.os.Environment; import android.preference.PreferenceManager; import android.util.Base64; +import android.widget.Toast; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; import org.shadowice.flocke.andotp.R; import java.io.File; import java.nio.charset.StandardCharsets; import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; import java.util.Collections; import java.util.HashSet; import java.util.Locale; @@ -74,18 +75,28 @@ public class Settings { private void migrateDeprecatedSettings() { if (settings.contains(getResString(R.string.settings_key_auth_password))) { String plainPassword = getAuthPassword(); - String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword))); - setString(R.string.settings_key_auth_password_hash, hashedPassword); + try { + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, getSalt()); + setString(R.string.settings_key_auth_password_pbkdf2, Base64.encodeToString(credentials.password, Base64.URL_SAFE)); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + Toast.makeText(context, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); + e.printStackTrace(); + } remove(R.string.settings_key_auth_password); } if (settings.contains(getResString(R.string.settings_key_auth_pin))) { String plainPIN = getAuthPIN(); - String hashedPIN = new String(Hex.encodeHex(DigestUtils.sha256(plainPIN))); - setString(R.string.settings_key_auth_pin_hash, hashedPIN); + try { + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPIN, getSalt()); + setString(R.string.settings_key_auth_pin_pbkdf2, Base64.encodeToString(credentials.password, Base64.URL_SAFE)); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + Toast.makeText(context, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); + e.printStackTrace(); + } remove(R.string.settings_key_auth_pin); } @@ -218,6 +229,18 @@ public class Settings { return getString(R.string.settings_key_auth_password_hash, ""); } + public void removeAuthPasswordHash() { + remove(R.string.settings_key_auth_password_hash); + } + + public String getAuthPasswordPBKDF2() { + return getString(R.string.settings_key_auth_password_pbkdf2, ""); + } + + public void setAuthPasswordPBKDF2(String password) { + setString(R.string.settings_key_auth_password_pbkdf2, password); + } + public String getAuthPIN() { return getString(R.string.settings_key_auth_pin, ""); } @@ -226,6 +249,36 @@ public class Settings { return getString(R.string.settings_key_auth_pin_hash, ""); } + public void removeAuthPINHash() { + remove(R.string.settings_key_auth_pin_hash); + } + + public String getAuthPINPBKDF2() { + return getString(R.string.settings_key_auth_pin_pbkdf2, ""); + } + + public void setAuthPINPBKDF2(String pin) { + setString(R.string.settings_key_auth_pin_pbkdf2, pin); + } + + public void setSalt(byte[] bytes) { + String encodedSalt = Base64.encodeToString(bytes, Base64.URL_SAFE); + setString(R.string.settings_key_auth_salt, encodedSalt); + } + + public byte[] getSalt() { + String storedSalt = getString(R.string.settings_key_auth_salt, ""); + + if (storedSalt.isEmpty()) { + byte[] newSalt = EncryptionHelper.generateRandom(Constants.AUTH_SALT_LENGTH); + setSalt(newSalt); + + return newSalt; + } else { + return Base64.decode(storedSalt, Base64.URL_SAFE); + } + } + public Set getPanicResponse() { return settings.getStringSet(getResString(R.string.settings_key_panic), Collections.emptySet()); } diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index 85855b71..4d40e157 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -8,8 +8,11 @@ pref_auth pref_auth_password pref_auth_password_hash + pref_auth_password_pbkdf2 pref_auth_pin pref_auth_pin_hash + pref_auth_pin_pbkdf2 + pref_auth_salt pref_panic pref_lang diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 0e7f0e0a..5183a39b 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -64,6 +64,9 @@ This feature requires a secure lock screen to be set up (Settings -> Security -> Screenlock) + Failed to silently upgrade your password / PIN + to the new encryption, please manually reset it in the settings! + None From 1f4b46e89ac3d5e06fac6cedeb6447c93bb938f2 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Fri, 29 Dec 2017 12:08:00 +0100 Subject: [PATCH 10/41] Use random iterations to make PBKDF2 a little more secure --- .../Activities/AuthenticateActivity.java | 7 ++-- .../Preferences/PBKDF2PasswordPreference.java | 4 ++- .../andotp/Utilities/EncryptionHelper.java | 15 +++++--- .../flocke/andotp/Utilities/Settings.java | 35 +++++++++++++++++-- app/src/main/res/values/settings.xml | 2 ++ 5 files changed, 54 insertions(+), 9 deletions(-) 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 8034c8fb..108ba9cd 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 @@ -125,7 +125,7 @@ public class AuthenticateActivity extends ThemedActivity if (actionId == EditorInfo.IME_ACTION_DONE) { if (! oldPassword) { try { - EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(v.getText().toString(), settings.getSalt()); + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(v.getText().toString(), settings.getSalt(), settings.getIterations(authMethod)); byte[] passwordArray = Base64.decode(password, Base64.URL_SAFE); if (Arrays.equals(passwordArray, credentials.password)) { @@ -143,13 +143,16 @@ public class AuthenticateActivity extends ThemedActivity if (hashedPassword.equals(password)) { try { - EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, settings.getSalt()); + int iter = EncryptionHelper.generateRandomIterations(); + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, settings.getSalt(), iter); String base64 = Base64.encodeToString(credentials.password, Base64.URL_SAFE); if (authMethod == AuthMethod.PASSWORD) settings.setAuthPasswordPBKDF2(base64); else if (authMethod == AuthMethod.PIN) settings.setAuthPINPBKDF2(base64); + + settings.setIterations(authMethod, iter); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { Toast.makeText(this, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); e.printStackTrace(); 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 index d0841dbf..4db32f12 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java @@ -81,8 +81,10 @@ public class PBKDF2PasswordPreference extends DialogPreference persistString(value); } else { try { - EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(value, settings.getSalt()); + 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(); } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java index 9fd6891a..57b37edb 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java @@ -34,8 +34,8 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; -import java.util.ArrayList; import java.util.Arrays; +import java.util.Random; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -53,7 +53,9 @@ import static org.shadowice.flocke.andotp.Utilities.Constants.ALGORITHM_SYMMETRI public class EncryptionHelper { private final static int IV_LENGTH = 12; - private final static int PBKDF2_ITERATIONS = 1000; + public final static int PBKDF2_MIN_ITERATIONS = 1000; + public final static int PBKDF2_MAX_ITERATIONS = 5000; + public final static int PBKDF2_OLD_DEFAULT_ITERATIONS = 1000; private final static int PBKDF2_LENGTH = 512; public static class PBKDF2Credentials { @@ -61,6 +63,11 @@ public class EncryptionHelper { public byte[] seed; } + public static int generateRandomIterations() { + Random rand = new Random(); + return rand.nextInt((PBKDF2_MAX_ITERATIONS - PBKDF2_MIN_ITERATIONS) + 1) + PBKDF2_MIN_ITERATIONS; + } + public static byte[] generateRandom(int length) { final byte[] raw = new byte[length]; new SecureRandom().nextBytes(raw); @@ -68,10 +75,10 @@ public class EncryptionHelper { return raw; } - public static PBKDF2Credentials generatePBKDF2Credentials(String password, byte[] salt) + public static PBKDF2Credentials generatePBKDF2Credentials(String password, byte[] salt, int iter) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, PBKDF2_ITERATIONS, PBKDF2_LENGTH); + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iter, PBKDF2_LENGTH); byte[] array = secretKeyFactory.generateSecret(keySpec).getEncoded(); 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 2dafc180..d3abbcf8 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 @@ -42,6 +42,7 @@ import java.util.Locale; 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; public class Settings { private static final String DEFAULT_BACKUP_FOLDER = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "andOTP"; @@ -77,8 +78,10 @@ public class Settings { String plainPassword = getAuthPassword(); try { - EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, getSalt()); + int iter = EncryptionHelper.generateRandomIterations(); + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, getSalt(), iter); setString(R.string.settings_key_auth_password_pbkdf2, Base64.encodeToString(credentials.password, Base64.URL_SAFE)); + setInt(R.string.settings_key_auth_password_iter, iter); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { Toast.makeText(context, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); e.printStackTrace(); @@ -91,8 +94,10 @@ public class Settings { String plainPIN = getAuthPIN(); try { - EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPIN, getSalt()); + int iter = EncryptionHelper.generateRandomIterations(); + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPIN, getSalt(), iter); setString(R.string.settings_key_auth_pin_pbkdf2, Base64.encodeToString(credentials.password, Base64.URL_SAFE)); + setInt(R.string.settings_key_auth_pin_iter, iter); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { Toast.makeText(context, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); e.printStackTrace(); @@ -141,6 +146,10 @@ public class Settings { return settings.getInt(getResString(keyId), getResInt(defaultId)); } + private int getIntValue(int keyId, int defaultValue) { + return settings.getInt(getResString(keyId), defaultValue); + } + private long getLong(int keyId, long defaultValue) { return settings.getLong(getResString(keyId), defaultValue); } @@ -155,6 +164,12 @@ public class Settings { .apply(); } + private void setInt(int keyId, int value) { + settings.edit() + .putInt(getResString(keyId), value) + .apply(); + } + private void setString(int keyId, String value) { settings.edit() .putString(getResString(keyId), value) @@ -279,6 +294,22 @@ public class Settings { } } + public int getIterations(AuthMethod method) { + if (method == AuthMethod.PASSWORD) + return getIntValue(R.string.settings_key_auth_password_iter, PBKDF2_OLD_DEFAULT_ITERATIONS); + else if (method == AuthMethod.PIN) + return getIntValue(R.string.settings_key_auth_pin_iter, PBKDF2_OLD_DEFAULT_ITERATIONS); + else + return 0; + } + + public void setIterations(AuthMethod method, int value) { + if (method == AuthMethod.PASSWORD) + setInt(R.string.settings_key_auth_password_iter, value); + else if (method == AuthMethod.PIN) + setInt(R.string.settings_key_auth_pin_iter, value); + } + public Set getPanicResponse() { return settings.getStringSet(getResString(R.string.settings_key_panic), Collections.emptySet()); } diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index 4d40e157..e69816a2 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -9,9 +9,11 @@ pref_auth_password pref_auth_password_hash pref_auth_password_pbkdf2 + pref_auth_password_iter pref_auth_pin pref_auth_pin_hash pref_auth_pin_pbkdf2 + pref_auth_pin_iter pref_auth_salt pref_panic From 6612095e8f258c1d2c54724f0404a3eafd9943a1 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Fri, 29 Dec 2017 12:30:00 +0100 Subject: [PATCH 11/41] Pass the seed from the AuthenticatActivity to the caller --- .../Activities/AuthenticateActivity.java | 29 ++++++++++++------- .../andotp/Activities/MainActivity.java | 3 ++ 2 files changed, 22 insertions(+), 10 deletions(-) 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 108ba9cd..96438b22 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 @@ -52,6 +52,8 @@ import static org.shadowice.flocke.andotp.Utilities.Settings.AuthMethod; public class AuthenticateActivity extends ThemedActivity implements EditText.OnEditorActionListener { + public static final String EXTRA_NAME_SEED = "credential_seed"; + private String password; AuthMethod authMethod; @@ -88,7 +90,7 @@ public class AuthenticateActivity extends ThemedActivity if (password.isEmpty()) { Toast.makeText(this, R.string.auth_toast_password_missing, Toast.LENGTH_LONG).show(); - finishWithResult(true); + finishWithResult(true, null); } else { passwordLabel.setText(R.string.auth_msg_password); passwordLayout.setHint(getString(R.string.auth_hint_password)); @@ -104,14 +106,14 @@ public class AuthenticateActivity extends ThemedActivity if (password.isEmpty()) { Toast.makeText(this, R.string.auth_toast_pin_missing, Toast.LENGTH_LONG).show(); - finishWithResult(true); + finishWithResult(true, null); } else { passwordLabel.setText(R.string.auth_msg_pin); passwordLayout.setHint(getString(R.string.auth_hint_pin)); passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); } } else { - finishWithResult(true); + finishWithResult(true, null); } passwordInput.setTransformationMethod(new PasswordTransformationMethod()); @@ -129,19 +131,21 @@ public class AuthenticateActivity extends ThemedActivity byte[] passwordArray = Base64.decode(password, Base64.URL_SAFE); if (Arrays.equals(passwordArray, credentials.password)) { - finishWithResult(true); + finishWithResult(true, credentials.seed); } else { - finishWithResult(false); + finishWithResult(false, null); } } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { e.printStackTrace(); - finishWithResult(false); + finishWithResult(false, null); } } else { String plainPassword = v.getText().toString(); String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword))); if (hashedPassword.equals(password)) { + byte[] seed = null; + try { int iter = EncryptionHelper.generateRandomIterations(); EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, settings.getSalt(), iter); @@ -153,6 +157,8 @@ public class AuthenticateActivity extends ThemedActivity settings.setAuthPINPBKDF2(base64); settings.setIterations(authMethod, iter); + + seed = credentials.seed; } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { Toast.makeText(this, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); e.printStackTrace(); @@ -163,9 +169,9 @@ public class AuthenticateActivity extends ThemedActivity else if (authMethod == AuthMethod.PIN) settings.removeAuthPINHash(); - finishWithResult(true); + finishWithResult(true, seed); } else { - finishWithResult(false); + finishWithResult(false, null); } } @@ -176,9 +182,12 @@ public class AuthenticateActivity extends ThemedActivity } // End with a result - public void finishWithResult(boolean success) { + public void finishWithResult(boolean success, byte[] seed) { Intent data = new Intent(); + if (seed != null) + data.putExtra(EXTRA_NAME_SEED, seed); + if (success) setResult(RESULT_OK, data); @@ -188,7 +197,7 @@ public class AuthenticateActivity extends ThemedActivity // Go back to the main activity @Override public void onBackPressed() { - finishWithResult(false); + finishWithResult(false, null); super.onBackPressed(); } } 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 223dc416..df0a5bce 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 @@ -70,6 +70,7 @@ import org.shadowice.flocke.andotp.View.TagsAdapter; import java.util.ArrayList; import java.util.HashMap; +import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.EXTRA_NAME_SEED; import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; public class MainActivity extends BaseActivity @@ -376,6 +377,8 @@ public class MainActivity extends BaseActivity } else { requireAuthentication = false; + byte[] credentialSeed = intent.getByteArrayExtra(EXTRA_NAME_SEED); + adapter.setEncryptionKey(KeyStoreHelper.loadEncryptionKeyFromKeyStore(this)); populateAdapter(); } From d68798e69a48e3ee89ab198f27b099327f7cd735 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Fri, 29 Dec 2017 14:11:40 +0100 Subject: [PATCH 12/41] Refactor some functions in more appropriate classes --- .../andotp/Utilities/EncryptionHelper.java | 34 +++++++++++++ .../andotp/Utilities/KeyStoreHelper.java | 50 ++++++------------- .../andotp/Utilities/SecretKeyWrapper.java | 5 +- app/src/main/res/values/strings_main.xml | 10 ++-- 4 files changed, 58 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java index 57b37edb..437967ec 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java @@ -23,10 +23,16 @@ package org.shadowice.flocke.andotp.Utilities; +import android.content.Context; + +import java.io.File; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; +import java.security.KeyPair; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; @@ -51,6 +57,7 @@ import static org.shadowice.flocke.andotp.Utilities.Constants.ALGORITHM_ASYMMETR import static org.shadowice.flocke.andotp.Utilities.Constants.ALGORITHM_SYMMETRIC; public class EncryptionHelper { + private final static int KEY_LENGTH = 16; private final static int IV_LENGTH = 12; public final static int PBKDF2_MIN_ITERATIONS = 1000; @@ -151,4 +158,31 @@ public class EncryptionHelper { return cipher.doFinal(cipherText); } + + /** + * Load our symmetric secret key. + * The symmetric secret key is stored securely on disk by wrapping + * it with a public/private key pair, possibly backed by hardware. + */ + public static SecretKey loadOrGenerateWrappedKey(Context context, File keyFile, KeyPair keyPair) + throws GeneralSecurityException, IOException { + final SecretKeyWrapper wrapper = new SecretKeyWrapper(keyPair); + + // Generate secret key if none exists + if (!keyFile.exists()) { + final byte[] raw = EncryptionHelper.generateRandom(KEY_LENGTH); + + final SecretKey key = new SecretKeySpec(raw, "AES"); + final byte[] wrapped = wrapper.wrap(key); + + + FileHelper.writeBytesToFile(keyFile, wrapped); + } + + // Even if we just generated the key, always read it back to ensure we + // can read it successfully. + final byte[] wrapped = FileHelper.readFileToBytes(keyFile); + + return wrapper.unwrap(wrapped); + } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java index d8490d87..06a14b98 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java @@ -42,7 +42,6 @@ import java.util.Calendar; import java.util.GregorianCalendar; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import javax.security.auth.x500.X500Principal; public class KeyStoreHelper { @@ -50,8 +49,6 @@ public class KeyStoreHelper { public static final String KEYSTORE_ALIAS_WRAPPING = "settings"; - private final static int KEY_LENGTH = 16; - public static KeyPair loadOrGenerateAsymmetricKeyPair(Context context, String alias) throws GeneralSecurityException, IOException { final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); @@ -91,42 +88,25 @@ public class KeyStoreHelper { return new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey()); } - /** - * Load our symmetric secret key. - * The symmetric secret key is stored securely on disk by wrapping - * it with a public/private key pair, possibly backed by hardware. - */ - public static SecretKey loadOrGenerateWrappedKey(Context context, File keyFile) - throws GeneralSecurityException, IOException { - final SecretKeyWrapper wrapper = new SecretKeyWrapper(context, KEYSTORE_ALIAS_WRAPPING); - - // Generate secret key if none exists - if (!keyFile.exists()) { - final byte[] raw = EncryptionHelper.generateRandom(KEY_LENGTH); - - final SecretKey key = new SecretKeySpec(raw, "AES"); - final byte[] wrapped = wrapper.wrap(key); - - - FileHelper.writeBytesToFile(keyFile, wrapped); - } - - // Even if we just generated the key, always read it back to ensure we - // can read it successfully. - final byte[] wrapped = FileHelper.readFileToBytes(keyFile); - - return wrapper.unwrap(wrapped); - } - public static SecretKey loadEncryptionKeyFromKeyStore(Context context) { + KeyPair pair = null; + try { - return loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE)); + pair = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEYSTORE_ALIAS_WRAPPING); } catch (GeneralSecurityException | IOException e) { e.printStackTrace(); - - UIHelper.showGenericDialog(context, R.string.dialog_title_keystore_error, R.string.dialog_msg_keystore_error); - - return null; + UIHelper.showGenericDialog(context, R.string.dialog_title_encryption_error, R.string.dialog_msg_keystore_error); } + + if (pair != null) { + try { + return EncryptionHelper.loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE), pair); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + UIHelper.showGenericDialog(context, R.string.dialog_title_encryption_error, R.string.dialog_msg_unwrap_error); + } + } + + return null; } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java index 0e6be63f..3b9a4bbf 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java @@ -17,7 +17,6 @@ package org.shadowice.flocke.andotp.Utilities; import android.annotation.SuppressLint; -import android.content.Context; import java.io.IOException; import java.security.GeneralSecurityException; @@ -48,10 +47,10 @@ public class SecretKeyWrapper { * If no pair with that alias exists, it will be generated. */ @SuppressLint("GetInstance") - public SecretKeyWrapper(Context context, String alias) + public SecretKeyWrapper(KeyPair keyPair) throws GeneralSecurityException, IOException { mCipher = Cipher.getInstance(ALGORITHM_ASYMMETRIC); - mPair = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, alias); + mPair = keyPair; } /** diff --git a/app/src/main/res/values/strings_main.xml b/app/src/main/res/values/strings_main.xml index 5453bdd2..935db31a 100644 --- a/app/src/main/res/values/strings_main.xml +++ b/app/src/main/res/values/strings_main.xml @@ -63,7 +63,7 @@ Rename Security and Backups Last used - KeyStore error + Encryption error Please enter your device credentials to start andOTP. Are you sure you want do remove the account \"%1$s\"? @@ -84,6 +84,10 @@ In order for andOTP to recognize which token was used last you have to have \"tap to reveal\" enabled or use the copy button.\n\nThis message will not be shown again. - Failed to load the encryption key from the KeyStore. - You won\'t be able to use this app. Please contact the developer and provide a logcat! + + Failed to generate or load the wrapping key from the + KeyStore. You won\'t be able to use this app. Please contact the developer and provide a + logcat! + Failed to generate or unwrap the encryption key. You + won\'t be able to use this app. Please contact the developer and provide a logcat! From 04a511d3f4659d41820dbccd0ad5fd4b5410cc1f Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Fri, 29 Dec 2017 16:58:47 +0100 Subject: [PATCH 13/41] Add password-based encryption of the database --- .../Activities/AuthenticateActivity.java | 30 +++++--- .../andotp/Activities/BackupActivity.java | 8 ++- .../andotp/Activities/MainActivity.java | 68 ++++++++++++++++--- .../Activities/PanicResponderActivity.java | 28 ++------ .../andotp/Activities/SettingsActivity.java | 23 ++++++- .../flocke/andotp/Utilities/Constants.java | 4 ++ .../andotp/Utilities/DatabaseHelper.java | 6 +- .../andotp/Utilities/EncryptionHelper.java | 12 ++-- .../andotp/Utilities/KeyStoreHelper.java | 16 ++++- .../flocke/andotp/Utilities/Settings.java | 9 +++ app/src/main/res/values/settings.xml | 7 ++ app/src/main/res/values/strings_auth.xml | 4 +- app/src/main/res/values/strings_settings.xml | 6 ++ app/src/main/res/xml/preferences.xml | 11 ++- 14 files changed, 174 insertions(+), 58 deletions(-) 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 96438b22..402e9d45 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 @@ -52,7 +52,11 @@ import static org.shadowice.flocke.andotp.Utilities.Settings.AuthMethod; public class AuthenticateActivity extends ThemedActivity implements EditText.OnEditorActionListener { - public static final String EXTRA_NAME_SEED = "credential_seed"; + public static final String EXTRA_NAME_PASSWORD_KEY = "password_key"; + public static final String EXTRA_NAME_RELOAD_ADAPTER = "reload_adapter"; + public static final String EXTRA_NAME_MESSAGE_ID = "message_id"; + + boolean reloadAdapter = false; private String password; @@ -74,10 +78,16 @@ public class AuthenticateActivity extends ThemedActivity stub.setLayoutResource(R.layout.content_authenticate); View v = stub.inflate(); + Intent callingIntent = getIntent(); + reloadAdapter = callingIntent.getBooleanExtra(EXTRA_NAME_RELOAD_ADAPTER, false); + int labelMsg = callingIntent.getIntExtra(EXTRA_NAME_MESSAGE_ID, R.string.auth_msg_authenticate); + TextView passwordLabel = v.findViewById(R.id.passwordLabel); TextInputLayout passwordLayout = v.findViewById(R.id.passwordLayout); TextInputEditText passwordInput = v.findViewById(R.id.passwordEdit); + passwordLabel.setText(labelMsg); + authMethod = settings.getAuthMethod(); if (authMethod == AuthMethod.PASSWORD) { @@ -92,7 +102,6 @@ public class AuthenticateActivity extends ThemedActivity Toast.makeText(this, R.string.auth_toast_password_missing, Toast.LENGTH_LONG).show(); finishWithResult(true, null); } else { - passwordLabel.setText(R.string.auth_msg_password); passwordLayout.setHint(getString(R.string.auth_hint_password)); passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } @@ -108,7 +117,6 @@ public class AuthenticateActivity extends ThemedActivity Toast.makeText(this, R.string.auth_toast_pin_missing, Toast.LENGTH_LONG).show(); finishWithResult(true, null); } else { - passwordLabel.setText(R.string.auth_msg_pin); passwordLayout.setHint(getString(R.string.auth_hint_pin)); passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); } @@ -131,7 +139,7 @@ public class AuthenticateActivity extends ThemedActivity byte[] passwordArray = Base64.decode(password, Base64.URL_SAFE); if (Arrays.equals(passwordArray, credentials.password)) { - finishWithResult(true, credentials.seed); + finishWithResult(true, credentials.key); } else { finishWithResult(false, null); } @@ -144,7 +152,7 @@ public class AuthenticateActivity extends ThemedActivity String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword))); if (hashedPassword.equals(password)) { - byte[] seed = null; + byte[] key = null; try { int iter = EncryptionHelper.generateRandomIterations(); @@ -158,7 +166,7 @@ public class AuthenticateActivity extends ThemedActivity settings.setIterations(authMethod, iter); - seed = credentials.seed; + key = credentials.key; } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { Toast.makeText(this, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); e.printStackTrace(); @@ -169,7 +177,7 @@ public class AuthenticateActivity extends ThemedActivity else if (authMethod == AuthMethod.PIN) settings.removeAuthPINHash(); - finishWithResult(true, seed); + finishWithResult(true, key); } else { finishWithResult(false, null); } @@ -182,11 +190,13 @@ public class AuthenticateActivity extends ThemedActivity } // End with a result - public void finishWithResult(boolean success, byte[] seed) { + public void finishWithResult(boolean success, byte[] key) { Intent data = new Intent(); - if (seed != null) - data.putExtra(EXTRA_NAME_SEED, seed); + data.putExtra(EXTRA_NAME_RELOAD_ADAPTER, reloadAdapter); + + if (key != null) + data.putExtra(EXTRA_NAME_PASSWORD_KEY, key); if (success) setResult(RESULT_OK, data); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java index fc833ab8..cf14b600 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java @@ -53,7 +53,6 @@ import org.shadowice.flocke.andotp.R; import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; import org.shadowice.flocke.andotp.Utilities.FileHelper; -import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; import org.shadowice.flocke.andotp.Utilities.Tools; import java.io.ByteArrayInputStream; @@ -63,6 +62,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; public class BackupActivity extends BaseActivity { private final static int INTENT_OPEN_DOCUMENT_PLAIN = 200; @@ -88,7 +88,7 @@ public class BackupActivity extends BaseActivity { private static final String DEFAULT_BACKUP_MIMETYPE_CRYPT = "binary/aes"; private static final String DEFAULT_BACKUP_MIMETYPE_PGP = "application/pgp-encrypted"; - public static final String ENCRYPTION_KEY_PARAM = "encryption_key"; + public static final String EXTRA_NAME_ENCRYPTION_KEY = "encryption_key"; private SecretKey encryptionKey = null; @@ -116,7 +116,9 @@ public class BackupActivity extends BaseActivity { stub.setLayoutResource(R.layout.content_backup); View v = stub.inflate(); - encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(this); + Intent callingIntent = getIntent(); + byte[] keyMaterial = callingIntent.getByteArrayExtra(EXTRA_NAME_ENCRYPTION_KEY); + encryptionKey = new SecretKeySpec(keyMaterial, 0, keyMaterial.length, "AES"); // Plain-text 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 df0a5bce..5696fc5e 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,6 +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; @@ -70,7 +72,12 @@ import org.shadowice.flocke.andotp.View.TagsAdapter; import java.util.ArrayList; import java.util.HashMap; -import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.EXTRA_NAME_SEED; +import javax.crypto.SecretKey; + +import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.EXTRA_NAME_MESSAGE_ID; +import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.EXTRA_NAME_PASSWORD_KEY; +import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.EXTRA_NAME_RELOAD_ADAPTER; +import static org.shadowice.flocke.andotp.Activities.BackupActivity.EXTRA_NAME_ENCRYPTION_KEY; import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; public class MainActivity extends BaseActivity @@ -85,6 +92,7 @@ public class MainActivity extends BaseActivity private MenuItem sortMenu; private SimpleItemTouchHelperCallback touchHelperCallback; + private Constants.EncryptionType encryptionType = Constants.EncryptionType.KEYSTORE; private boolean requireAuthentication = false; private Handler handler; @@ -119,7 +127,7 @@ public class MainActivity extends BaseActivity .show(); } - public void authenticate() { + public void authenticate(int messageId, boolean reloadAdapter) { Settings.AuthMethod authMethod = settings.getAuthMethod(); if (authMethod == Settings.AuthMethod.DEVICE) { @@ -130,6 +138,8 @@ public class MainActivity extends BaseActivity } } else if (authMethod == Settings.AuthMethod.PASSWORD || authMethod == Settings.AuthMethod.PIN) { Intent authIntent = new Intent(this, AuthenticateActivity.class); + authIntent.putExtra(EXTRA_NAME_RELOAD_ADAPTER, reloadAdapter); + authIntent.putExtra(EXTRA_NAME_MESSAGE_ID, messageId); startActivityForResult(authIntent, INTENT_INTERNAL_AUTHENTICATE); } } @@ -186,6 +196,8 @@ public class MainActivity extends BaseActivity PreferenceManager.setDefaultValues(this, R.xml.preferences, false); settings.registerPreferenceChangeListener(this); + encryptionType = settings.getEncryption(); + if (settings.getAuthMethod() != Settings.AuthMethod.NONE && savedInstanceState == null) requireAuthentication = true; @@ -305,12 +317,25 @@ public class MainActivity extends BaseActivity public void onResume() { super.onResume(); - if (settings.getAuthMethod() != Settings.AuthMethod.NONE && requireAuthentication) { - requireAuthentication = false; - authenticate(); + if (requireAuthentication) { + if (settings.getAuthMethod() != Settings.AuthMethod.NONE) { + requireAuthentication = false; + authenticate(R.string.auth_msg_authenticate, true); + } } else { - adapter.setEncryptionKey(KeyStoreHelper.loadEncryptionKeyFromKeyStore(this)); - populateAdapter(); + if (encryptionType == Constants.EncryptionType.KEYSTORE) { + if (adapter.getEncryptionKey() == null) { + adapter.setEncryptionKey(KeyStoreHelper.loadEncryptionKeyFromKeyStore(this)); + } + + populateAdapter(); + } else if (encryptionType == Constants.EncryptionType.PASSWORD) { + if (adapter.getEncryptionKey() == null) { + authenticate(R.string.auth_msg_authenticate,true); + } else { + populateAdapter(); + } + } } startUpdater(); @@ -333,6 +358,15 @@ 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,false); + } } } @@ -377,10 +411,23 @@ public class MainActivity extends BaseActivity } else { requireAuthentication = false; - byte[] credentialSeed = intent.getByteArrayExtra(EXTRA_NAME_SEED); + SecretKey encryptionKey = null; - adapter.setEncryptionKey(KeyStoreHelper.loadEncryptionKeyFromKeyStore(this)); - populateAdapter(); + if (encryptionType == Constants.EncryptionType.KEYSTORE) { + encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(this); + } else if (encryptionType == Constants.EncryptionType.PASSWORD) { + byte[] credentialSeed = intent.getByteArrayExtra(EXTRA_NAME_PASSWORD_KEY); + encryptionKey = EncryptionHelper.generateSymmetricKey(credentialSeed); + } + + boolean reloadAdapter = intent.getBooleanExtra(EXTRA_NAME_RELOAD_ADAPTER, false); + + adapter.setEncryptionKey(encryptionKey); + + if (reloadAdapter) + populateAdapter(); + else + adapter.saveEntries(); } } } @@ -459,6 +506,7 @@ public class MainActivity extends BaseActivity if (id == R.id.action_backup) { Intent backupIntent = new Intent(this, BackupActivity.class); + backupIntent.putExtra(EXTRA_NAME_ENCRYPTION_KEY, adapter.getEncryptionKey().getEncoded()); startActivityForResult(backupIntent, INTENT_INTERNAL_BACKUP); } else if (id == R.id.action_settings) { Intent settingsIntent = new Intent(this, SettingsActivity.class); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java index 568a873b..9b999c11 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java @@ -27,18 +27,12 @@ import android.content.Intent; import android.os.Build; import android.os.Bundle; +import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; +import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; import org.shadowice.flocke.andotp.Utilities.Settings; -import java.io.File; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; import java.util.Set; -import static org.shadowice.flocke.andotp.Utilities.DatabaseHelper.SETTINGS_FILE; -import static org.shadowice.flocke.andotp.Utilities.KeyStoreHelper.KEYSTORE_ALIAS_WRAPPING; -import static org.shadowice.flocke.andotp.Utilities.KeyStoreHelper.KEY_FILE; - public class PanicResponderActivity extends Activity { public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER"; @@ -53,22 +47,8 @@ public class PanicResponderActivity extends Activity { Set response = settings.getPanicResponse(); if (response.contains("accounts")) { - File database = new File(getFilesDir() + "/" + SETTINGS_FILE); - File key = new File(getFilesDir() + "/" + KEY_FILE); - - database.delete(); - key.delete(); - - try { - final KeyStore keyStore = java.security.KeyStore.getInstance("AndroidKeyStore"); - keyStore.load(null); - - if (keyStore.containsAlias(KEYSTORE_ALIAS_WRAPPING)) { - keyStore.deleteEntry(KEYSTORE_ALIAS_WRAPPING); - } - } catch (GeneralSecurityException | IOException e) { - e.printStackTrace(); - } + DatabaseHelper.wipeDatabase(this); + KeyStoreHelper.wipeKeys(this); } if (response.contains("settings")) 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 67835482..9d9b3f5c 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 @@ -39,6 +39,7 @@ import org.openintents.openpgp.util.OpenPgpAppPreference; import org.openintents.openpgp.util.OpenPgpKeyPreference; import org.shadowice.flocke.andotp.Preferences.PBKDF2PasswordPreference; import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; public class SettingsActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener{ @@ -104,6 +105,8 @@ public class SettingsActivity extends BaseActivity public static class SettingsFragment extends PreferenceFragment { PreferenceCategory catSecurity; + ListPreference encryption; + OpenPgpAppPreference pgpProvider; OpenPgpKeyPreference pgpKey; @@ -125,6 +128,7 @@ public class SettingsActivity extends BaseActivity authPassword.setMode(PBKDF2PasswordPreference.Mode.PASSWORD); catSecurity.addPreference(authPassword); + encryption.setEnabled(true); break; @@ -136,10 +140,12 @@ public class SettingsActivity extends BaseActivity authPIN.setMode(PBKDF2PasswordPreference.Mode.PIN); catSecurity.addPreference(authPIN); + encryption.setEnabled(true); break; default: + encryption.setEnabled(false); break; } } @@ -148,13 +154,14 @@ public class SettingsActivity extends BaseActivity public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getActivity().getBaseContext()); + final SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getActivity().getBaseContext()); addPreferencesFromResource(R.xml.preferences); // 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(authPref.getValue()); @@ -181,6 +188,20 @@ public class SettingsActivity extends BaseActivity } }); + encryption.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(final Preference preference, Object o) { + final String newEncryption = (String) o; + + if (newEncryption.equals("password")) + KeyStoreHelper.wipeKeys(preference.getContext()); + + encryption.setValue(newEncryption); + + return true; + } + }); + // OpenPGP pgpProvider = (OpenPgpAppPreference) findPreference(getString(R.string.settings_key_openpgp_provider)); pgpKey = (OpenPgpKeyPreference) findPreference(getString(R.string.settings_key_openpgp_keyid)); 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 a1a644b1..feb42cbb 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 EncryptionType { + KEYSTORE, PASSWORD + } + final static String ALGORITHM_SYMMETRIC = "AES/GCM/NoPadding"; final static String ALGORITHM_ASYMMETRIC = "RSA/ECB/PKCS1Padding"; diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java index 484673ab..41706944 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java @@ -36,8 +36,12 @@ import javax.crypto.SecretKey; public class DatabaseHelper { public static final String SETTINGS_FILE = "secrets.dat"; - /* Database functions */ + public static void wipeDatabase(Context context) { + File db = new File(context.getFilesDir() + "/" + SETTINGS_FILE); + db.delete(); + } + /* Database functions */ public static boolean saveDatabase(Context context, ArrayList entries, SecretKey encryptionKey) { String jsonString = entriesToString(entries); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java index 437967ec..ff490304 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java @@ -23,8 +23,6 @@ package org.shadowice.flocke.andotp.Utilities; -import android.content.Context; - import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -67,7 +65,7 @@ public class EncryptionHelper { public static class PBKDF2Credentials { public byte[] password; - public byte[] seed; + public byte[] key; } public static int generateRandomIterations() { @@ -93,11 +91,15 @@ public class EncryptionHelper { PBKDF2Credentials credentials = new PBKDF2Credentials(); credentials.password = Arrays.copyOfRange(array, halfPoint, array.length); - credentials.seed = Arrays.copyOfRange(array, 0, halfPoint - 1); + credentials.key = Arrays.copyOfRange(array, 0, halfPoint); return credentials; } + public static SecretKey generateSymmetricKey(byte[] data) { + return new SecretKeySpec(data, 0, data.length, "AES"); + } + public static SecretKey generateSymmetricKeyFromPassword(String password) throws NoSuchAlgorithmException { MessageDigest sha = MessageDigest.getInstance("SHA-256"); @@ -164,7 +166,7 @@ public class EncryptionHelper { * The symmetric secret key is stored securely on disk by wrapping * it with a public/private key pair, possibly backed by hardware. */ - public static SecretKey loadOrGenerateWrappedKey(Context context, File keyFile, KeyPair keyPair) + public static SecretKey loadOrGenerateWrappedKey(File keyFile, KeyPair keyPair) throws GeneralSecurityException, IOException { final SecretKeyWrapper wrapper = new SecretKeyWrapper(keyPair); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java index 06a14b98..779582f2 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java @@ -49,6 +49,20 @@ public class KeyStoreHelper { public static final String KEYSTORE_ALIAS_WRAPPING = "settings"; + public static void wipeKeys(Context context) { + File keyFile = new File(context.getFilesDir() + "/" + KEY_FILE); + keyFile.delete(); + + try { + final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + if (keyStore.containsAlias(KEYSTORE_ALIAS_WRAPPING)) + keyStore.deleteEntry(KEYSTORE_ALIAS_WRAPPING); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + } + } + public static KeyPair loadOrGenerateAsymmetricKeyPair(Context context, String alias) throws GeneralSecurityException, IOException { final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); @@ -100,7 +114,7 @@ public class KeyStoreHelper { if (pair != null) { try { - return EncryptionHelper.loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE), pair); + return EncryptionHelper.loadOrGenerateWrappedKey(new File(context.getFilesDir() + "/" + KEY_FILE), pair); } catch (GeneralSecurityException | IOException e) { e.printStackTrace(); UIHelper.showGenericDialog(context, R.string.dialog_title_encryption_error, R.string.dialog_msg_unwrap_error); 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 d3abbcf8..d535be55 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 @@ -310,6 +310,15 @@ public class Settings { setInt(R.string.settings_key_auth_pin_iter, value); } + public Constants.EncryptionType getEncryption() { + String encType = getString(R.string.settings_key_encryption, R.string.settings_default_encryption); + return Constants.EncryptionType.valueOf(encType.toUpperCase()); + } + + public void setEncryption(String encryption) { + setString(R.string.settings_key_encryption, encryption); + } + public Set getPanicResponse() { return settings.getStringSet(getResString(R.string.settings_key_panic), Collections.emptySet()); } diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index e69816a2..76c5e103 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -15,6 +15,7 @@ pref_auth_pin_pbkdf2 pref_auth_pin_iter pref_auth_salt + pref_encryption pref_panic pref_lang @@ -47,6 +48,7 @@ 30 none + keystore system light 18 @@ -72,6 +74,11 @@ device + + keystore + password + + accounts settings diff --git a/app/src/main/res/values/strings_auth.xml b/app/src/main/res/values/strings_auth.xml index ded3911f..b4d8799d 100644 --- a/app/src/main/res/values/strings_auth.xml +++ b/app/src/main/res/values/strings_auth.xml @@ -7,8 +7,8 @@ PIN - Please enter your password to start andOTP. - Please enter your PIN to start andOTP. + Please authenticate to start andOTP! + Please confirm your authentication! Please set a password in the settings! diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 5183a39b..8a11f7bb 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -13,6 +13,7 @@ Authentication Password PIN + Database encryption Panic Trigger Language @@ -75,6 +76,11 @@ Device credentials + + Android KeyStore + Password / PIN + + Wipe all accounts Reset app settings diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index e680e3a2..bd95a097 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -33,9 +33,18 @@ android:entryValues="@array/settings_values_auth" android:defaultValue="@string/settings_default_auth" /> + + Date: Sat, 30 Dec 2017 08:17:08 +0100 Subject: [PATCH 14/41] A little bit of refactoring --- .../Activities/AuthenticateActivity.java | 12 ++++---- .../andotp/Activities/MainActivity.java | 29 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) 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 402e9d45..5fac7d72 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 @@ -53,10 +53,10 @@ import static org.shadowice.flocke.andotp.Utilities.Settings.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_RELOAD_ADAPTER = "reload_adapter"; - public static final String EXTRA_NAME_MESSAGE_ID = "message_id"; + public static final String EXTRA_NAME_SAVE_DATABASE = "save_database"; + public static final String EXTRA_NAME_MESSAGE = "message"; - boolean reloadAdapter = false; + boolean saveDatabase = false; private String password; @@ -79,8 +79,8 @@ public class AuthenticateActivity extends ThemedActivity View v = stub.inflate(); Intent callingIntent = getIntent(); - reloadAdapter = callingIntent.getBooleanExtra(EXTRA_NAME_RELOAD_ADAPTER, false); - int labelMsg = callingIntent.getIntExtra(EXTRA_NAME_MESSAGE_ID, R.string.auth_msg_authenticate); + saveDatabase = callingIntent.getBooleanExtra(EXTRA_NAME_SAVE_DATABASE, false); + int labelMsg = callingIntent.getIntExtra(EXTRA_NAME_MESSAGE, R.string.auth_msg_authenticate); TextView passwordLabel = v.findViewById(R.id.passwordLabel); TextInputLayout passwordLayout = v.findViewById(R.id.passwordLayout); @@ -193,7 +193,7 @@ public class AuthenticateActivity extends ThemedActivity public void finishWithResult(boolean success, byte[] key) { Intent data = new Intent(); - data.putExtra(EXTRA_NAME_RELOAD_ADAPTER, reloadAdapter); + data.putExtra(EXTRA_NAME_SAVE_DATABASE, saveDatabase); if (key != null) data.putExtra(EXTRA_NAME_PASSWORD_KEY, key); 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 5696fc5e..e955a5cf 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 @@ -74,9 +74,9 @@ import java.util.HashMap; import javax.crypto.SecretKey; -import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.EXTRA_NAME_MESSAGE_ID; +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_RELOAD_ADAPTER; +import static org.shadowice.flocke.andotp.Activities.AuthenticateActivity.EXTRA_NAME_SAVE_DATABASE; import static org.shadowice.flocke.andotp.Activities.BackupActivity.EXTRA_NAME_ENCRYPTION_KEY; import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; @@ -127,7 +127,7 @@ public class MainActivity extends BaseActivity .show(); } - public void authenticate(int messageId, boolean reloadAdapter) { + public void authenticate(int messageId, boolean saveDatabase) { Settings.AuthMethod authMethod = settings.getAuthMethod(); if (authMethod == Settings.AuthMethod.DEVICE) { @@ -138,8 +138,8 @@ public class MainActivity extends BaseActivity } } else if (authMethod == Settings.AuthMethod.PASSWORD || authMethod == Settings.AuthMethod.PIN) { Intent authIntent = new Intent(this, AuthenticateActivity.class); - authIntent.putExtra(EXTRA_NAME_RELOAD_ADAPTER, reloadAdapter); - authIntent.putExtra(EXTRA_NAME_MESSAGE_ID, messageId); + authIntent.putExtra(EXTRA_NAME_SAVE_DATABASE, saveDatabase); + authIntent.putExtra(EXTRA_NAME_MESSAGE, messageId); startActivityForResult(authIntent, INTENT_INTERNAL_AUTHENTICATE); } } @@ -320,7 +320,7 @@ public class MainActivity extends BaseActivity if (requireAuthentication) { if (settings.getAuthMethod() != Settings.AuthMethod.NONE) { requireAuthentication = false; - authenticate(R.string.auth_msg_authenticate, true); + authenticate(R.string.auth_msg_authenticate, false); } } else { if (encryptionType == Constants.EncryptionType.KEYSTORE) { @@ -331,7 +331,7 @@ public class MainActivity extends BaseActivity populateAdapter(); } else if (encryptionType == Constants.EncryptionType.PASSWORD) { if (adapter.getEncryptionKey() == null) { - authenticate(R.string.auth_msg_authenticate,true); + authenticate(R.string.auth_msg_authenticate,false); } else { populateAdapter(); } @@ -365,8 +365,13 @@ public class MainActivity extends BaseActivity adapter.saveEntries(); } else if (settings.getEncryption() == Constants.EncryptionType.PASSWORD) { encryptionType = Constants.EncryptionType.PASSWORD; - authenticate(R.string.auth_msg_confirm,false); + authenticate(R.string.auth_msg_confirm,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, true); } } @@ -420,14 +425,14 @@ public class MainActivity extends BaseActivity encryptionKey = EncryptionHelper.generateSymmetricKey(credentialSeed); } - boolean reloadAdapter = intent.getBooleanExtra(EXTRA_NAME_RELOAD_ADAPTER, false); + boolean saveDatabase = intent.getBooleanExtra(EXTRA_NAME_SAVE_DATABASE, false); adapter.setEncryptionKey(encryptionKey); - if (reloadAdapter) - populateAdapter(); - else + if (saveDatabase) adapter.saveEntries(); + + populateAdapter(); } } } From f2e6f5772579f1e7ef297f9c7d55388ef8cd8b71 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Sat, 30 Dec 2017 08:40:57 +0100 Subject: [PATCH 15/41] Add some safeguard when changing passwords and encryption We still need some more to prevent any mistakes --- .../andotp/Activities/SettingsActivity.java | 87 ++++++++++++------- app/src/main/res/values/strings_settings.xml | 7 ++ 2 files changed, 61 insertions(+), 33 deletions(-) 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 9d9b3f5c..57c18003 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 @@ -39,7 +39,9 @@ import org.openintents.openpgp.util.OpenPgpAppPreference; import org.openintents.openpgp.util.OpenPgpKeyPreference; import org.shadowice.flocke.andotp.Preferences.PBKDF2PasswordPreference; 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; public class SettingsActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener{ @@ -110,7 +112,7 @@ public class SettingsActivity extends BaseActivity OpenPgpAppPreference pgpProvider; OpenPgpKeyPreference pgpKey; - public void updateAuthPassword(String newAuth) { + 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)); @@ -119,34 +121,22 @@ public class SettingsActivity extends BaseActivity if (pinPref != null) catSecurity.removePreference(pinPref); - switch (newAuth) { - case "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); + 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); - encryption.setEnabled(true); + 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); - break; - - case "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); - encryption.setEnabled(true); - - break; - - default: - encryption.setEnabled(false); - break; + catSecurity.addPreference(authPIN); } } @@ -163,14 +153,25 @@ public class SettingsActivity extends BaseActivity ListPreference authPref = (ListPreference) findPreference(getString(R.string.settings_key_auth)); encryption = (ListPreference) findPreference(getString(R.string.settings_key_encryption)); - updateAuthPassword(authPref.getValue()); + 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)); - if (newAuth.equals("device")) { + 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) { @@ -182,7 +183,7 @@ public class SettingsActivity extends BaseActivity } } - updateAuthPassword(newAuth); + updateAuthPassword(authMethod); return true; } @@ -191,10 +192,30 @@ public class SettingsActivity extends BaseActivity encryption.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(final Preference preference, Object o) { - final String newEncryption = (String) 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()); - if (newEncryption.equals("password")) - KeyStoreHelper.wipeKeys(preference.getContext()); + if (encryptionType == Constants.EncryptionType.PASSWORD) { + if (authMethod != Settings.AuthMethod.PASSWORD && authMethod != Settings.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) + credentials = sharedPref.getString(getString(R.string.settings_key_auth_password_pbkdf2), ""); + else if (authMethod == Settings.AuthMethod.PIN) + credentials = sharedPref.getString(getString(R.string.settings_key_auth_pin_pbkdf2), ""); + + if (credentials.isEmpty()) { + Toast.makeText(getActivity(), R.string.settings_toast_encryption_invalid_without_credentials, Toast.LENGTH_LONG).show(); + return false; + } else { + KeyStoreHelper.wipeKeys(preference.getContext()); + } + } + } encryption.setValue(newEncryption); diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 8a11f7bb..b7753a43 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -65,6 +65,13 @@ This feature requires a secure lock screen to be set up (Settings -> Security -> Screenlock) + You can only use Password or PIN as + long as the database encryption is set to \"Password / PIN\"! + You first need to set the + Authentication to \"Password\" or \"PIN\"! + You first need to set a + Password or PIN before changing the encryption! + Failed to silently upgrade your password / PIN to the new encryption, please manually reset it in the settings! From 745676b4272cf820c1c2ab7b99ac35118cae1b33 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Tue, 2 Jan 2018 13:49:05 +0100 Subject: [PATCH 16/41] Block empty passwords --- .../andotp/Preferences/PBKDF2PasswordPreference.java | 7 ++++--- app/src/main/res/values/strings_settings.xml | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) 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 index 4db32f12..b1c7f99a 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PBKDF2PasswordPreference.java @@ -37,6 +37,7 @@ 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; @@ -77,9 +78,7 @@ public class PBKDF2PasswordPreference extends DialogPreference } private void persistEncryptedString(String value) { - if (value.isEmpty()) { - persistString(value); - } else { + if (! value.isEmpty()) { try { int iter = EncryptionHelper.generateRandomIterations(); EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(value, settings.getSalt(), iter); @@ -88,6 +87,8 @@ public class PBKDF2PasswordPreference extends DialogPreference } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { e.printStackTrace(); } + } else { + Toast.makeText(getContext(), R.string.settings_toast_password_empty, Toast.LENGTH_LONG).show(); } } diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index b7753a43..3620a6a5 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -65,6 +65,9 @@ This feature requires a secure lock screen to be set up (Settings -> Security -> Screenlock) + An empty password is not allowed, set the + Authentication to \"None\" to disable it! + You can only use Password or PIN as long as the database encryption is set to \"Password / PIN\"! You first need to set the From 2b74b9d6f761494e0c4b0a2c9ca24bdafd3039da Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Tue, 2 Jan 2018 13:50:28 +0100 Subject: [PATCH 17/41] Only load entries if the encryption key is set --- .../shadowice/flocke/andotp/View/EntriesCardAdapter.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java index c113c375..cce7acb4 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java @@ -131,8 +131,10 @@ public class EntriesCardAdapter extends RecyclerView.Adapter } public void loadEntries() { - entries = DatabaseHelper.loadDatabase(context, encryptionKey); - entriesChanged(); + if (encryptionKey != null) { + entries = DatabaseHelper.loadDatabase(context, encryptionKey); + entriesChanged(); + } } public void filterByTags(List tags) { From bd12b5a17f45c944f8eefbaf4c91a6802ec788c3 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Tue, 2 Jan 2018 14:07:40 +0100 Subject: [PATCH 18/41] Add some checks to prevent crashes when the encryption key is not set --- .../flocke/andotp/Activities/MainActivity.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 e955a5cf..3fa4cb9e 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 @@ -422,15 +422,18 @@ public class MainActivity extends BaseActivity encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(this); } else if (encryptionType == Constants.EncryptionType.PASSWORD) { byte[] credentialSeed = intent.getByteArrayExtra(EXTRA_NAME_PASSWORD_KEY); - encryptionKey = EncryptionHelper.generateSymmetricKey(credentialSeed); + if (credentialSeed != null && credentialSeed.length > 0) + encryptionKey = EncryptionHelper.generateSymmetricKey(credentialSeed); } boolean saveDatabase = intent.getBooleanExtra(EXTRA_NAME_SAVE_DATABASE, false); - adapter.setEncryptionKey(encryptionKey); + if (encryptionKey != null) { + adapter.setEncryptionKey(encryptionKey); - if (saveDatabase) - adapter.saveEntries(); + if (saveDatabase) + adapter.saveEntries(); + } populateAdapter(); } From 13db26f3777c136aa0ca2cc3dfce5a36d70047ca Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Tue, 2 Jan 2018 14:45:00 +0100 Subject: [PATCH 19/41] Show a generic error if the encryption key is empty --- .../andotp/Utilities/DatabaseHelper.java | 25 +++++++++++++------ app/src/main/res/values/strings_main.xml | 1 + 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java index 41706944..58d1998a 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java @@ -24,9 +24,11 @@ package org.shadowice.flocke.andotp.Utilities; import android.content.Context; +import android.widget.Toast; import org.json.JSONArray; import org.shadowice.flocke.andotp.Database.Entry; +import org.shadowice.flocke.andotp.R; import java.io.File; import java.util.ArrayList; @@ -43,6 +45,11 @@ public class DatabaseHelper { /* Database functions */ public static boolean saveDatabase(Context context, ArrayList entries, SecretKey encryptionKey) { + if (encryptionKey == null) { + Toast.makeText(context, R.string.toast_encryption_key_empty, Toast.LENGTH_LONG).show(); + return false; + } + String jsonString = entriesToString(entries); try { @@ -58,16 +65,20 @@ public class DatabaseHelper { return true; } - public static ArrayList loadDatabase(Context context, SecretKey encryptionKey){ + public static ArrayList loadDatabase(Context context, SecretKey encryptionKey) { ArrayList entries = new ArrayList<>(); - try { - byte[] data = FileHelper.readFileToBytes(new File(context.getFilesDir() + "/" + SETTINGS_FILE)); - data = EncryptionHelper.decrypt(encryptionKey, data); + if (encryptionKey != null) { + try { + byte[] data = FileHelper.readFileToBytes(new File(context.getFilesDir() + "/" + SETTINGS_FILE)); + data = EncryptionHelper.decrypt(encryptionKey, data); - entries = stringToEntries(new String(data)); - } catch (Exception error) { - error.printStackTrace(); + entries = stringToEntries(new String(data)); + } catch (Exception error) { + error.printStackTrace(); + } + } else { + Toast.makeText(context, R.string.toast_encryption_key_empty, Toast.LENGTH_LONG).show(); } return entries; diff --git a/app/src/main/res/values/strings_main.xml b/app/src/main/res/values/strings_main.xml index 935db31a..0875a19d 100644 --- a/app/src/main/res/values/strings_main.xml +++ b/app/src/main/res/values/strings_main.xml @@ -55,6 +55,7 @@ Copied to clipboard This entry already exists Invalid QR Code + Encryption key not loaded Authenticate From fc6764073005f4bc549fa1e2b8ee2bcb235ff41b Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Tue, 2 Jan 2018 14:47:12 +0100 Subject: [PATCH 20/41] Better description for re-authentication --- .../org/shadowice/flocke/andotp/Activities/MainActivity.java | 4 ++-- app/src/main/res/values/strings_auth.xml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) 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 3fa4cb9e..48622121 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 @@ -365,13 +365,13 @@ public class MainActivity extends BaseActivity adapter.saveEntries(); } else if (settings.getEncryption() == Constants.EncryptionType.PASSWORD) { encryptionType = Constants.EncryptionType.PASSWORD; - authenticate(R.string.auth_msg_confirm,true); + 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, true); + authenticate(R.string.auth_msg_confirm_encryption, true); } } diff --git a/app/src/main/res/values/strings_auth.xml b/app/src/main/res/values/strings_auth.xml index b4d8799d..bdba98ae 100644 --- a/app/src/main/res/values/strings_auth.xml +++ b/app/src/main/res/values/strings_auth.xml @@ -8,7 +8,8 @@ Please authenticate to start andOTP! - Please confirm your authentication! + Please confirm your authentication to update the + encryption key! Please set a password in the settings! From 3565c899b8e739efe04c2bc660f04e19a25e343a Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Tue, 2 Jan 2018 22:00:53 +0100 Subject: [PATCH 21/41] 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 @@ + + + + + + + + + + + + + + + + + + + +