From a57dfbee7c1d7388fa4f0012979dcbc53c4dd087 Mon Sep 17 00:00:00 2001 From: Martin Florian Date: Sun, 1 Feb 2015 18:01:28 +0100 Subject: [PATCH 1/6] Setting and entering PINs per volume Work in progress... Done so far: once a volume's key is cached for the first time, the user can set a PIN. During unlocking, the cached key is used only if the correct PIN has been supplied. Needs a database update, so all existing volumes must be deleted and re-added manually (I think). --- res/values/strings.xml | 2 + .../mrpdaemon/android/encdroid/DBHelper.java | 41 ++++++++- .../android/encdroid/VolumeListActivity.java | 90 ++++++++++++++++--- 3 files changed, 121 insertions(+), 12 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 7d26f94..19fc1ac 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9,6 +9,8 @@ No EncFS volumes configured. Choose \"Import Volume\" or \"Create Volume\" from the menu to add volumes. Select Enter password: + Enter PIN: + Set PIN (leave empty for no PIN): OK Deriving password key Can take VERY long for some volumes. Consider enabling password key caching in Settings. diff --git a/src/org/mrpdaemon/android/encdroid/DBHelper.java b/src/org/mrpdaemon/android/encdroid/DBHelper.java index 943d035..a980ebf 100644 --- a/src/org/mrpdaemon/android/encdroid/DBHelper.java +++ b/src/org/mrpdaemon/android/encdroid/DBHelper.java @@ -51,6 +51,8 @@ public class DBHelper extends SQLiteOpenHelper { public static final String DB_COL_CONFIGPATH = "configPath"; public static final String DB_COL_TYPE = "type"; public static final String DB_COL_KEY = "key"; + public static final String DB_COL_PIN = "pin"; + public static final String DB_COL_PINCOUNTER = "pinctr"; // counts unsuccessful PIN attempts private static final String[] NO_ARGS = {}; @@ -67,7 +69,7 @@ public DBHelper(EDApplication application) { public void onCreate(SQLiteDatabase db) { String sqlCmd = "CREATE TABLE " + DB_TABLE + " (" + DB_COL_ID + " int primary key, " + DB_COL_NAME + " text, " + DB_COL_PATH - + " text, " + DB_COL_KEY + " text, " + DB_COL_TYPE + " int, " + + " text, " + DB_COL_KEY + " text, " + DB_COL_PIN + " text, "+ DB_COL_PINCOUNTER + " int, "+ DB_COL_TYPE + " int, " + DB_COL_CONFIGPATH + " text)"; Log.d(TAG, "onCreate() executing SQL: " + sqlCmd); db.execSQL(sqlCmd); @@ -140,6 +142,17 @@ public void cacheKey(Volume volume, byte[] key) { db.update(DB_TABLE, values, DB_COL_NAME + "=? AND " + DB_COL_PATH + "=?", new String[] { volume.getName(), volume.getPath() }); } + + public void setPIN(Volume volume, String pin) { + SQLiteDatabase db = getWritableDatabase(); + + Log.d(TAG, "setPIN() for volume" + volume.getName()); + + ContentValues values = new ContentValues(); + values.put(DB_COL_PIN, pin); + db.update(DB_TABLE, values, DB_COL_NAME + "=? AND " + DB_COL_PATH + + "=?", new String[] { volume.getName(), volume.getPath() }); + } public void clearKey(Volume volume) { SQLiteDatabase db = getWritableDatabase(); @@ -151,6 +164,17 @@ public void clearKey(Volume volume) { db.update(DB_TABLE, values, DB_COL_NAME + "=? AND " + DB_COL_PATH + "=?", new String[] { volume.getName(), volume.getPath() }); } + + public void clearPIN(Volume volume) { + SQLiteDatabase db = getWritableDatabase(); + + Log.d(TAG, "clearPIN() for volume" + volume.getName()); + + ContentValues values = new ContentValues(); + values.putNull(DB_COL_PIN); + db.update(DB_TABLE, values, DB_COL_NAME + "=? AND " + DB_COL_PATH + + "=?", new String[] { volume.getName(), volume.getPath() }); + } public void clearAllKeys() { SQLiteDatabase db = getWritableDatabase(); @@ -177,6 +201,21 @@ public byte[] getCachedKey(Volume volume) { return null; } + + public String getPIN(Volume volume) { + SQLiteDatabase db = getReadableDatabase(); + + Cursor cursor = db.query(DB_TABLE, NO_ARGS, DB_COL_NAME + "=? AND " + + DB_COL_PATH + "=?", + new String[] { volume.getName(), volume.getPath() }, null, + null, null); + + if (cursor.moveToFirst()) { + String pin = cursor.getString(cursor.getColumnIndex(DB_COL_PIN)); + return pin; + } + return null; + } public List getVolumes() { ArrayList volumes = new ArrayList(); diff --git a/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java b/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java index d88e4aa..569998b 100644 --- a/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java +++ b/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java @@ -86,6 +86,8 @@ public class VolumeListActivity extends ListActivity implements private final static int DIALOG_VOL_DELETE = 5; private final static int DIALOG_FS_TYPE = 6; private final static int DIALOG_ERROR = 7; + private final static int DIALOG_VOL_PIN = 8; + private final static int DIALOG_VOL_SETPIN = 9; // Volume operation types private final static int VOLUME_OP_IMPORT = 0; @@ -336,6 +338,20 @@ protected void onPrepareDialog(int id, final Dialog dialog) { | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } } + case DIALOG_VOL_PIN: + if (id == DIALOG_VOL_PIN) { + if (input != null) { + input.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD + | InputType.TYPE_CLASS_NUMBER); + } + } + case DIALOG_VOL_SETPIN: + if (id == DIALOG_VOL_SETPIN) { + if (input != null) { + input.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD + | InputType.TYPE_CLASS_NUMBER); + } + } case DIALOG_VOL_CREATEPASS: case DIALOG_VOL_NAME: case DIALOG_VOL_CREATE: @@ -418,6 +434,8 @@ protected Dialog onCreateDialog(int id) { switch (id) { case DIALOG_VOL_PASS: // Password dialog + case DIALOG_VOL_PIN: // Enter PIN dialog + case DIALOG_VOL_SETPIN: // Set PIN dialog if (mSelectedVolume == null) { // Can happen when restoring a killed activity return null; @@ -429,8 +447,17 @@ protected Dialog onCreateDialog(int id) { // Hide password input input.setTransformationMethod(new PasswordTransformationMethod()); - - alertBuilder.setTitle(getString(R.string.pwd_dialog_title_str)); + + String titleString; + if(id == DIALOG_VOL_PIN) { + titleString = getString(R.string.pin_dialog_title_str); + } else if (id == DIALOG_VOL_SETPIN) { + titleString = getString(R.string.set_pin_dialog_title_str); + } else { + titleString = getString(R.string.pwd_dialog_title_str); + } + + alertBuilder.setTitle(titleString); alertBuilder.setView(input); alertBuilder.setPositiveButton(getString(R.string.btn_ok_str), new DialogInterface.OnClickListener() { @@ -455,6 +482,19 @@ public void onClick(DialogInterface dialog, addTaskFragment(unlockTask); unlockTask.startTask(); break; + case DIALOG_VOL_PIN: + // Unlock with cached pass and PIN + unlockSelectedVolume(value.toString()); + break; + case DIALOG_VOL_SETPIN: + + if(value.length() > 0) { + mApp.getDbHelper().setPIN(mSelectedVolume,value.toString()); + } + + // TODO: does this open the correct volume? + launchVolumeBrowser(mSelectedVolIdx); + break; case DIALOG_VOL_CREATEPASS: // Launch async task to create volume TaskFragment createTask = new CreateVolumeTaskFragment( @@ -805,14 +845,40 @@ private void renameVolume(Volume volume, String newName) { * Unlock the currently selected volume */ private void unlockSelectedVolume() { + unlockSelectedVolume(null); + } + + /** + * Unlock the currently selected volume using a PIN + */ + private void unlockSelectedVolume(String userPin) { mVolumeFileSystem = mSelectedVolume.getFileSystem(); - - // If key caching is enabled, see if a key is cached + + // If key caching is enabled, see if a PIN is set and a key is cached + String savedPin = null; byte[] cachedKey = null; + if (mPrefs.getBoolean("cache_key", false)) { - cachedKey = mApp.getDbHelper().getCachedKey(mSelectedVolume); + + savedPin = mApp.getDbHelper().getPIN(mSelectedVolume); + + if ((savedPin == null) || ((userPin != null) && userPin.equals(savedPin.toString()))) { + // all is well, give out cachedKey + cachedKey = mApp.getDbHelper().getCachedKey(mSelectedVolume); + } else if (userPin == null) { + // no PIN given, ask user for one + showDialog(DIALOG_VOL_PIN); + return; + } else { + // a wrong PIN was given + // TODO: increment / check fail counter + //userPin = null; + showDialog(DIALOG_VOL_PIN); + return; + } } - + + // pin is ok or missing but no key is cached if (cachedKey == null) { showDialog(DIALOG_VOL_PASS); } else { @@ -842,16 +908,16 @@ private class UnlockVolumeTaskFragment extends TaskFragment { // Volume type private FileSystem mFileSystem; - + // Cached key private byte[] mCachedKey; - + // Path of the volume to unlock private String mVolumePath; // Password for unlocking (optional if cached key is given) private String mPassword; - + // Optional custom config path private String mConfigPath; @@ -1371,10 +1437,12 @@ public void onTaskResult(int taskId, Object result) { byte[] keyToCache = ovtr.volume.getDerivedKeyData(); mApp.getDbHelper().cacheKey(mSelectedVolume, keyToCache); + // Ask user for a pin for this volume + showDialog(DIALOG_VOL_SETPIN); } } - - launchVolumeBrowser(mSelectedVolIdx); + // TODO: does this work as it should? + //launchVolumeBrowser(mSelectedVolIdx); } } break; From c2c9848d0e897398e02f470b0825bfc0ee67ce5a Mon Sep 17 00:00:00 2001 From: Martin Florian Date: Sun, 15 Feb 2015 11:17:47 +0100 Subject: [PATCH 2/6] Feedback on wrong PIN --- res/values/strings.xml | 1 + src/org/mrpdaemon/android/encdroid/VolumeListActivity.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 19fc1ac..4f3f718 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11,6 +11,7 @@ Enter password: Enter PIN: Set PIN (leave empty for no PIN): + Wrong PIN entered! OK Deriving password key Can take VERY long for some volumes. Consider enabling password key caching in Settings. diff --git a/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java b/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java index 569998b..99bf8ad 100644 --- a/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java +++ b/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java @@ -873,7 +873,8 @@ private void unlockSelectedVolume(String userPin) { // a wrong PIN was given // TODO: increment / check fail counter //userPin = null; - showDialog(DIALOG_VOL_PIN); + mErrDialogText = getString(R.string.error_wrong_pin); + showDialog(DIALOG_ERROR); return; } } From 026177d9a971e638cf4b4bc14765ffd10c53ea97 Mon Sep 17 00:00:00 2001 From: Martin Florian Date: Sun, 15 Feb 2015 11:44:28 +0100 Subject: [PATCH 3/6] Counting failed PIN attempts Also delete cached key on three failed attempts --- res/values/strings.xml | 1 + .../mrpdaemon/android/encdroid/DBHelper.java | 32 +++++++++++++++++-- .../android/encdroid/VolumeListActivity.java | 15 ++++++++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 4f3f718..650622f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12,6 +12,7 @@ Enter PIN: Set PIN (leave empty for no PIN): Wrong PIN entered! + Wrong PIN entered three times! Volume password has been deleted. OK Deriving password key Can take VERY long for some volumes. Consider enabling password key caching in Settings. diff --git a/src/org/mrpdaemon/android/encdroid/DBHelper.java b/src/org/mrpdaemon/android/encdroid/DBHelper.java index a980ebf..e9a2ea6 100644 --- a/src/org/mrpdaemon/android/encdroid/DBHelper.java +++ b/src/org/mrpdaemon/android/encdroid/DBHelper.java @@ -52,7 +52,7 @@ public class DBHelper extends SQLiteOpenHelper { public static final String DB_COL_TYPE = "type"; public static final String DB_COL_KEY = "key"; public static final String DB_COL_PIN = "pin"; - public static final String DB_COL_PINCOUNTER = "pinctr"; // counts unsuccessful PIN attempts + public static final String DB_COL_PINATTEMPTS = "pinAttempts"; // counts unsuccessful PIN attempts private static final String[] NO_ARGS = {}; @@ -69,7 +69,7 @@ public DBHelper(EDApplication application) { public void onCreate(SQLiteDatabase db) { String sqlCmd = "CREATE TABLE " + DB_TABLE + " (" + DB_COL_ID + " int primary key, " + DB_COL_NAME + " text, " + DB_COL_PATH - + " text, " + DB_COL_KEY + " text, " + DB_COL_PIN + " text, "+ DB_COL_PINCOUNTER + " int, "+ DB_COL_TYPE + " int, " + + " text, " + DB_COL_KEY + " text, " + DB_COL_PIN + " text, "+ DB_COL_PINATTEMPTS + " int, "+ DB_COL_TYPE + " int, " + DB_COL_CONFIGPATH + " text)"; Log.d(TAG, "onCreate() executing SQL: " + sqlCmd); db.execSQL(sqlCmd); @@ -153,7 +153,18 @@ public void setPIN(Volume volume, String pin) { db.update(DB_TABLE, values, DB_COL_NAME + "=? AND " + DB_COL_PATH + "=?", new String[] { volume.getName(), volume.getPath() }); } + + public void setPINAttempts(Volume volume, int newVal) { + SQLiteDatabase db = getWritableDatabase(); + + Log.d(TAG, "setPINAttempts() " + volume.getName() + " to " + newVal); + ContentValues values = new ContentValues(); + values.put(DB_COL_PINATTEMPTS, newVal); + db.update(DB_TABLE, values, DB_COL_NAME + "=? AND " + DB_COL_PATH + + "=?", new String[] { volume.getName(), volume.getPath() }); + } + public void clearKey(Volume volume) { SQLiteDatabase db = getWritableDatabase(); @@ -172,6 +183,7 @@ public void clearPIN(Volume volume) { ContentValues values = new ContentValues(); values.putNull(DB_COL_PIN); + values.putNull(DB_COL_PINATTEMPTS); db.update(DB_TABLE, values, DB_COL_NAME + "=? AND " + DB_COL_PATH + "=?", new String[] { volume.getName(), volume.getPath() }); } @@ -198,7 +210,6 @@ public byte[] getCachedKey(Volume volume) { return Base64.decode(keyStr, Base64.DEFAULT); } } - return null; } @@ -216,6 +227,21 @@ public String getPIN(Volume volume) { } return null; } + + public int getPINAttempts(Volume volume) { + SQLiteDatabase db = getReadableDatabase(); + + Cursor cursor = db.query(DB_TABLE, NO_ARGS, DB_COL_NAME + "=? AND " + + DB_COL_PATH + "=?", + new String[] { volume.getName(), volume.getPath() }, null, + null, null); + + if (cursor.moveToFirst()) { + int pinAttempts = cursor.getInt((cursor.getColumnIndex(DB_COL_PINATTEMPTS))); + return pinAttempts; + } + return 0; + } public List getVolumes() { ArrayList volumes = new ArrayList(); diff --git a/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java b/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java index 99bf8ad..66efc00 100644 --- a/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java +++ b/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java @@ -865,6 +865,9 @@ private void unlockSelectedVolume(String userPin) { if ((savedPin == null) || ((userPin != null) && userPin.equals(savedPin.toString()))) { // all is well, give out cachedKey cachedKey = mApp.getDbHelper().getCachedKey(mSelectedVolume); + if(savedPin != null) { + mApp.getDbHelper().setPINAttempts(mSelectedVolume, 0); + } } else if (userPin == null) { // no PIN given, ask user for one showDialog(DIALOG_VOL_PIN); @@ -873,7 +876,17 @@ private void unlockSelectedVolume(String userPin) { // a wrong PIN was given // TODO: increment / check fail counter //userPin = null; - mErrDialogText = getString(R.string.error_wrong_pin); + int pinAttempts = mApp.getDbHelper().getPINAttempts(mSelectedVolume); + + // we tolerate 3 wrong attempts, after that we delete the cached key + if(pinAttempts < 2) { + mApp.getDbHelper().setPINAttempts(mSelectedVolume, pinAttempts+1); + mErrDialogText = getString(R.string.error_wrong_pin); + } else { + mApp.getDbHelper().clearKey(mSelectedVolume); + mApp.getDbHelper().clearPIN(mSelectedVolume); + mErrDialogText = getString(R.string.error_wrong_pin_three_times); + } showDialog(DIALOG_ERROR); return; } From d09b579216d4bbaa03a66f4795f4c6ed659796ca Mon Sep 17 00:00:00 2001 From: Martin Florian Date: Sun, 15 Feb 2015 11:55:28 +0100 Subject: [PATCH 4/6] Minor code cleanup --- .../mrpdaemon/android/encdroid/VolumeListActivity.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java b/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java index 66efc00..e5cee08 100644 --- a/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java +++ b/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java @@ -491,8 +491,6 @@ public void onClick(DialogInterface dialog, if(value.length() > 0) { mApp.getDbHelper().setPIN(mSelectedVolume,value.toString()); } - - // TODO: does this open the correct volume? launchVolumeBrowser(mSelectedVolIdx); break; case DIALOG_VOL_CREATEPASS: @@ -874,8 +872,6 @@ private void unlockSelectedVolume(String userPin) { return; } else { // a wrong PIN was given - // TODO: increment / check fail counter - //userPin = null; int pinAttempts = mApp.getDbHelper().getPINAttempts(mSelectedVolume); // we tolerate 3 wrong attempts, after that we delete the cached key @@ -1453,10 +1449,10 @@ public void onTaskResult(int taskId, Object result) { keyToCache); // Ask user for a pin for this volume showDialog(DIALOG_VOL_SETPIN); + break; } } - // TODO: does this work as it should? - //launchVolumeBrowser(mSelectedVolIdx); + launchVolumeBrowser(mSelectedVolIdx); } } break; From 7472778461b6f054208a42ab9293db5bca28228e Mon Sep 17 00:00:00 2001 From: Martin Florian Date: Tue, 10 Mar 2015 21:28:52 +0100 Subject: [PATCH 5/6] Incremented DB_VERSION and updated onUpgrade The PIN and PINATTEMPTS columns should now be created on upgrade from older versions. --- src/org/mrpdaemon/android/encdroid/DBHelper.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/org/mrpdaemon/android/encdroid/DBHelper.java b/src/org/mrpdaemon/android/encdroid/DBHelper.java index e9a2ea6..e7fcb15 100644 --- a/src/org/mrpdaemon/android/encdroid/DBHelper.java +++ b/src/org/mrpdaemon/android/encdroid/DBHelper.java @@ -39,7 +39,7 @@ public class DBHelper extends SQLiteOpenHelper { public static final String DB_NAME = "volume.db"; // Database version - public static final int DB_VERSION = 4; + public static final int DB_VERSION = 5; // Volume table name public static final String DB_TABLE = "volumes"; @@ -77,11 +77,17 @@ public void onCreate(SQLiteDatabase db) { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // Adding column DB_COL_CONFIGPATH on upgrade - if (oldVersion == 3) { + // Adding missing columns on upgrade + if (oldVersion == 3 || oldVersion == 4) { Log.d(TAG, "onUpgrade() Upgrading DB"); + if(oldVersion == 3) { + db.execSQL("ALTER TABLE " + DB_TABLE + " ADD COLUMN " + + DB_COL_CONFIGPATH + " TEXT"); + } + db.execSQL("ALTER TABLE " + DB_TABLE + " ADD COLUMN " + + DB_COL_PIN + " TEXT"); db.execSQL("ALTER TABLE " + DB_TABLE + " ADD COLUMN " - + DB_COL_CONFIGPATH + " TEXT"); + + DB_COL_PINATTEMPTS + " INT"); } else { db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE); Log.d(TAG, "onUpgrade() recreating DB"); From 00c0d2f812bb65738ad60756b9b3168ab8da05c8 Mon Sep 17 00:00:00 2001 From: Martin Florian Date: Tue, 10 Mar 2015 21:54:33 +0100 Subject: [PATCH 6/6] Fixed inconsistency when disabling password caching PINs were not deleted alongside passwords when password caching is disabled in the settings. --- src/org/mrpdaemon/android/encdroid/DBHelper.java | 9 +++++++++ .../mrpdaemon/android/encdroid/EDPreferenceActivity.java | 2 ++ 2 files changed, 11 insertions(+) diff --git a/src/org/mrpdaemon/android/encdroid/DBHelper.java b/src/org/mrpdaemon/android/encdroid/DBHelper.java index e7fcb15..b88025f 100644 --- a/src/org/mrpdaemon/android/encdroid/DBHelper.java +++ b/src/org/mrpdaemon/android/encdroid/DBHelper.java @@ -201,6 +201,15 @@ public void clearAllKeys() { db.execSQL("UPDATE " + DB_TABLE + " SET " + DB_COL_KEY + " = NULL"); } + + public void clearAllPINs() { + SQLiteDatabase db = getWritableDatabase(); + + Log.d(TAG, "clearAllPINs()"); + + db.execSQL("UPDATE " + DB_TABLE + " SET " + DB_COL_PIN + " = NULL"); + db.execSQL("UPDATE " + DB_TABLE + " SET " + DB_COL_PINATTEMPTS + " = 0"); + } public byte[] getCachedKey(Volume volume) { SQLiteDatabase db = getReadableDatabase(); diff --git a/src/org/mrpdaemon/android/encdroid/EDPreferenceActivity.java b/src/org/mrpdaemon/android/encdroid/EDPreferenceActivity.java index 516ec56..b57bce4 100644 --- a/src/org/mrpdaemon/android/encdroid/EDPreferenceActivity.java +++ b/src/org/mrpdaemon/android/encdroid/EDPreferenceActivity.java @@ -137,6 +137,8 @@ public void onSharedPreferenceChanged(SharedPreferences prefs, Log.d(TAG, "Key caching disabled, clearing cached keys."); // Need to clear all cached keys mApp.getDbHelper().clearAllKeys(); + // Might need to clear all saved PINs as well + mApp.getDbHelper().clearAllPINs(); } else { Log.d(TAG, "Key caching enabled."); }