diff --git a/res/values/strings.xml b/res/values/strings.xml index 7d26f94..650622f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9,6 +9,10 @@ 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): + 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 943d035..b88025f 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"; @@ -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_PINATTEMPTS = "pinAttempts"; // 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_PINATTEMPTS + " int, "+ DB_COL_TYPE + " int, " + DB_COL_CONFIGPATH + " text)"; Log.d(TAG, "onCreate() executing SQL: " + sqlCmd); db.execSQL(sqlCmd); @@ -75,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"); @@ -140,7 +148,29 @@ 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 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(); @@ -151,6 +181,18 @@ 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); + values.putNull(DB_COL_PINATTEMPTS); + db.update(DB_TABLE, values, DB_COL_NAME + "=? AND " + DB_COL_PATH + + "=?", new String[] { volume.getName(), volume.getPath() }); + } public void clearAllKeys() { SQLiteDatabase db = getWritableDatabase(); @@ -159,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(); @@ -174,9 +225,38 @@ public byte[] getCachedKey(Volume volume) { return Base64.decode(keyStr, Base64.DEFAULT); } } + 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 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/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."); } diff --git a/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java b/src/org/mrpdaemon/android/encdroid/VolumeListActivity.java index d88e4aa..e5cee08 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,17 @@ 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()); + } + launchVolumeBrowser(mSelectedVolIdx); + break; case DIALOG_VOL_CREATEPASS: // Launch async task to create volume TaskFragment createTask = new CreateVolumeTaskFragment( @@ -805,14 +843,52 @@ 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); + if(savedPin != null) { + mApp.getDbHelper().setPINAttempts(mSelectedVolume, 0); + } + } else if (userPin == null) { + // no PIN given, ask user for one + showDialog(DIALOG_VOL_PIN); + return; + } else { + // a wrong PIN was given + 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; + } } - + + // pin is ok or missing but no key is cached if (cachedKey == null) { showDialog(DIALOG_VOL_PASS); } else { @@ -842,16 +918,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,9 +1447,11 @@ 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); + break; } } - launchVolumeBrowser(mSelectedVolIdx); } }