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);
}
}