added UI for opening a database with OTPs. Some TODOs and things not yet working!

This commit is contained in:
Philipp Crocoll 2013-11-17 07:17:15 +01:00
parent 59eace5834
commit c686cbeeb3
23 changed files with 2745 additions and 4945 deletions

View File

@ -3,6 +3,7 @@ using Android.App;
using System.IO;
using Android.Content;
using Android.OS;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using keepass2android.Io;
@ -22,7 +23,7 @@ namespace keepass2android
/// <summary>
/// Loads the specified data as the currently open database, as unlocked.
/// </summary>
void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, string s, string keyFile, ProgressDialogStatusLogger statusLogger);
void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compKey, ProgressDialogStatusLogger statusLogger);
/// <summary>
/// Returns the current database

View File

@ -107,16 +107,6 @@ namespace keepass2android.Io
}
}
public bool CompleteIoId()
{
throw new NotImplementedException();
}
public bool? FileExists()
{
throw new NotImplementedException();
}
public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
{
return UrlUtil.StripExtension(
@ -207,5 +197,19 @@ namespace keepass2android.Io
parent += "/";
return parent + newFilename;
}
public IOConnectionInfo GetParentPath(IOConnectionInfo ioc)
{
return IoUtil.GetParentPath(ioc);
}
public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename)
{
IOConnectionInfo res = folderPath.CloneDeep();
if (!res.Path.EndsWith("/"))
res.Path += "/";
res.Path += filename;
return res;
}
}
}

View File

@ -400,16 +400,6 @@ namespace keepass2android.Io
return new CachedWriteTransaction(ioc, useFileTransaction, this);
}
public bool CompleteIoId()
{
throw new NotImplementedException();
}
public bool? FileExists()
{
throw new NotImplementedException();
}
public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
{
return UrlUtil.StripExtension(
@ -487,6 +477,54 @@ namespace keepass2android.Io
return _cachedStorage.CreateFilePath(parent, newFilename);
}
public IOConnectionInfo GetParentPath(IOConnectionInfo ioc)
{
return _cachedStorage.GetParentPath(ioc);
}
public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename)
{
try
{
IOConnectionInfo res = _cachedStorage.GetFilePath(folderPath, filename);
//some file storage implementations require accessing the network to determine the file path (e.g. because
//they might contain file ids). In this case, we need to cache the result to enable cached access to such files
StoreFilePath(folderPath, filename, res);
return res;
}
catch (Exception)
{
IOConnectionInfo res;
if (!TryGetCachedFilePath(folderPath, filename, out res)) throw;
return res;
}
}
private void StoreFilePath(IOConnectionInfo folderPath, string filename, IOConnectionInfo res)
{
File.WriteAllText(CachedFilePath(GetPseudoIoc(folderPath, filename)) + ".filepath", res.Path);
}
private IOConnectionInfo GetPseudoIoc(IOConnectionInfo folderPath, string filename)
{
IOConnectionInfo res = folderPath.CloneDeep();
if (!res.Path.EndsWith("/"))
res.Path += "/";
res.Path += filename;
return res;
}
private bool TryGetCachedFilePath(IOConnectionInfo folderPath, string filename, out IOConnectionInfo res)
{
res = folderPath.CloneDeep();
string filePathCache = CachedFilePath(GetPseudoIoc(folderPath, filename)) + ".filepath";
if (!File.Exists(filePathCache))
return false;
res.Path = File.ReadAllText(filePathCache);
return true;
}
public string GetBaseVersionHash(IOConnectionInfo ioc)
{

View File

@ -82,19 +82,6 @@ namespace keepass2android.Io
/// <param name="useFileTransaction">if true, force to use file system level transaction. This might be ignored if the file storage has built in transaction support</param>
IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction);
/// <summary>
/// brings up a dialog to query credentials or something like this.
/// </summary>
/// <returns>true if success, false if error or cancelled by user</returns>
bool CompleteIoId( /*in/out ioId*/);
/// <summary>
/// Checks whether the given file exists.
/// </summary>
/// <returns>true if it exists, false if not. Null if the check couldn't be performed (e.g. because no credentials available or no connection established.)</returns>
bool? FileExists( /*ioId*/);
string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc);
/// <summary>
@ -135,10 +122,10 @@ namespace keepass2android.Io
void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId);
/// <summary>
/// Initiates the process for choosing a file in the given file storage.
/// Initiates the process for using a file in the given file storage.
/// The file storage should either call OnImmediateResult or StartFileUsageProcess
/// If alwaysReturnSuccess is true, the activity should be finished with ResultCode Ok.
/// This can make sense if a higher-level file storage has the file cached by still wants to
/// This can make sense if a higher-level file storage has the file cached but still wants to
/// give the cached storage the chance to initialize file access.
/// </summary>
void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode, bool alwaysReturnSuccess);
@ -157,6 +144,17 @@ namespace keepass2android.Io
//returns the path of a file "newFilename" in the folder "parent"
//this may create the file if this is required to get a path (if a UUID is part of the file path)
string CreateFilePath(string parent, string newFilename);
/// <summary>
/// returns the parent folder of ioc
/// </summary>
IOConnectionInfo GetParentPath(IOConnectionInfo ioc);
/// <summary>
/// returns the file path of the file "filename" in the folderPath.
/// </summary>
/// The method may throw FileNotFoundException or not in case the file doesn't exist.
IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename);
}
public interface IWriteTransaction: IDisposable

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Text;
using Java.IO;
using KeePassLib.Serialization;
namespace keepass2android.Io
{
@ -30,5 +31,20 @@ namespace keepass2android.Io
}
public static IOConnectionInfo GetParentPath(IOConnectionInfo ioc)
{
var iocParent = ioc.CloneDeep();
if (iocParent.Path.EndsWith("/"))
iocParent.Path = iocParent.Path.Substring(0, iocParent.Path.Length - 1);
int slashPos = iocParent.Path.LastIndexOf("/", StringComparison.Ordinal);
if (slashPos == -1)
iocParent.Path = "";
else
{
iocParent.Path = iocParent.Path.Substring(0, slashPos);
}
return iocParent;
}
}
}

View File

@ -151,16 +151,6 @@ namespace keepass2android.Io
}
}
public bool CompleteIoId()
{
throw new NotImplementedException();
}
public bool? FileExists()
{
throw new NotImplementedException();
}
public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
{
return UrlUtil.StripExtension(

View File

@ -82,29 +82,14 @@ namespace keepass2android
}
/// <summary>
/// Do not call this method directly. Call App.Kp2a.LoadDatabase instead.
/// </summary>
public void LoadData(IKp2aApp app, IOConnectionInfo iocInfo, MemoryStream databaseData, String password, String keyfile, ProgressDialogStatusLogger status)
public void LoadData(IKp2aApp app, IOConnectionInfo iocInfo, MemoryStream databaseData, CompositeKey compositeKey, ProgressDialogStatusLogger status)
{
PwDatabase pwDatabase = new PwDatabase();
CompositeKey compositeKey = new CompositeKey();
compositeKey.AddUserKey(new KcpPassword(password));
if (!String.IsNullOrEmpty(keyfile))
{
try
{
compositeKey.AddUserKey(new KcpKeyFile(keyfile));
} catch (Exception e)
{
Kp2aLog.Log(e.ToString());
throw new KeyFileException();
}
}
IFileStorage fileStorage = _app.GetFileStorage(iocInfo);
var filename = fileStorage.GetFilenameWithoutPathAndExt(iocInfo);
try
@ -115,7 +100,9 @@ namespace keepass2android
}
catch (InvalidCompositeKeyException)
{
if ((password == "") && (keyfile != null))
KcpPassword passwordKey = (KcpPassword)compositeKey.GetUserKey(typeof(KcpPassword));
if ((passwordKey != null) && (passwordKey.Password.ReadString() == "") && (compositeKey.UserKeyCount > 1))
{
//if we don't get a password, we don't know whether this means "empty password" or "no password"
//retry without password:

View File

@ -19,6 +19,7 @@ using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using KeePassLib.Keys;
using KeePassLib.Serialization;
namespace keepass2android
@ -26,20 +27,20 @@ namespace keepass2android
public class LoadDb : RunnableOnFinish {
private readonly IOConnectionInfo _ioc;
private readonly Task<MemoryStream> _databaseData;
private readonly String _pass;
private readonly String _key;
private readonly CompositeKey _compositeKey;
private readonly string _keyfileOrProvider;
private readonly IKp2aApp _app;
private readonly bool _rememberKeyfile;
public LoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, String pass, String key, OnFinish finish): base(finish)
public LoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, CompositeKey compositeKey, String keyfileOrProvider, OnFinish finish): base(finish)
{
_app = app;
_ioc = ioc;
_databaseData = databaseData;
_pass = pass;
_key = key;
_compositeKey = compositeKey;
_keyfileOrProvider = keyfileOrProvider;
_rememberKeyfile = app.GetBooleanPreference(PreferenceKey.remember_keyfile);
}
@ -50,8 +51,8 @@ namespace keepass2android
{
StatusLogger.UpdateMessage(UiStringKey.loading_database);
MemoryStream memoryStream = _databaseData == null ? null : _databaseData.Result;
_app.LoadDatabase(_ioc, memoryStream, _pass, _key, StatusLogger);
SaveFileData(_ioc, _key);
_app.LoadDatabase(_ioc, memoryStream, _compositeKey, StatusLogger);
SaveFileData(_ioc, _keyfileOrProvider);
}
catch (KeyFileException)
@ -88,13 +89,13 @@ namespace keepass2android
Finish(true);
}
private void SaveFileData(IOConnectionInfo ioc, String key) {
private void SaveFileData(IOConnectionInfo ioc, String keyfileOrProvider) {
if (!_rememberKeyfile)
{
key = "";
keyfileOrProvider = "";
}
_app.StoreOpenedFileAsRecent(ioc, key);
_app.StoreOpenedFileAsRecent(ioc, keyfileOrProvider);
}

View File

@ -85,15 +85,6 @@ namespace Kp2aUnitTests
}
}
public bool CompleteIoId()
{
throw new NotImplementedException();
}
public bool? FileExists()
{
throw new NotImplementedException();
}
public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
{

View File

@ -38,7 +38,7 @@ namespace Kp2aUnitTests
public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, string password, string keyFile,
ProgressDialogStatusLogger statusLogger)
{
_db.LoadData(this, ioConnectionInfo, memoryStream, password, keyFile, statusLogger);
_db.LoadData(this, ioConnectionInfo, memoryStream, password, statusLogger);
}

View File

@ -16,13 +16,16 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file
*/
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Database;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Java.Lang;
using Java.Net;
using Android.Preferences;
using Java.IO;
@ -31,8 +34,14 @@ using Android.Content.PM;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using KeePassLib.Utility;
using OtpKeyProv;
using keepass2android.Io;
using keepass2android.Utils;
using Exception = System.Exception;
using MemoryStream = System.IO.MemoryStream;
using Object = Java.Lang.Object;
using Process = Android.OS.Process;
using String = System.String;
namespace keepass2android
{
@ -41,6 +50,15 @@ namespace keepass2android
Theme="@style/Base")]
public class PasswordActivity : LockingActivity {
enum KeyProviders
{
//int values correspond to indices in passwordSpinner
None = 0,
KeyFile = 1,
Otp = 2
}
bool _showPassword;
public const String KeyDefaultFilename = "defaultFileName";
@ -53,14 +71,37 @@ namespace keepass2android
private const String ViewIntent = "android.intent.action.VIEW";
private const string ShowpasswordKey = "ShowPassword";
private const string KeyProviderIdOtp = "KP2A-OTP";
private Task<MemoryStream> _loadDbTask;
private IOConnectionInfo _ioConnection;
private String _keyFile;
private String _keyFileOrProvider;
internal AppTask AppTask;
private bool _killOnDestroy;
private string _password = "";
private const int RequestCodePrepareDbFile = 1000;
private const int RequestCodePrepareOtpAuxFile = 1001;
KeyProviders KeyProviderType
{
get
{
if (_keyFileOrProvider == null)
return KeyProviders.None;
if (_keyFileOrProvider == KeyProviderIdOtp)
return KeyProviders.Otp;
return KeyProviders.KeyFile;
}
}
private bool _rememberKeyfile;
ISharedPreferences _prefs;
private bool _starting;
private OtpInfo _otpInfo;
private readonly int[] _otpTextViewIds = new int[] {Resource.Id.otp1, Resource.Id.otp2, Resource.Id.otp3, Resource.Id.otp4, Resource.Id.otp5, Resource.Id.otp6};
public PasswordActivity (IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
@ -170,6 +211,7 @@ namespace keepass2android
KcpKeyFile kcpKeyfile = (KcpKeyFile)App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey(typeof(KcpKeyFile));
SetEditText(Resource.Id.pass_keyfile, kcpKeyfile.Path);
_keyFileOrProvider = kcpKeyfile.Path;
}
}
App.Kp2a.LockDatabase(false);
@ -186,18 +228,62 @@ namespace keepass2android
EditText fn = (EditText) FindViewById(Resource.Id.pass_keyfile);
fn.Text = filename;
_keyFileOrProvider = filename;
}
}
break;
case (Result)FileStorageResults.FileUsagePrepared:
PeformLoadDatabase();
if (requestCode == RequestCodePrepareDbFile)
PerformLoadDatabase();
if (requestCode == RequestCodePrepareOtpAuxFile)
LoadOtpFile();
break;
}
}
internal AppTask AppTask;
private bool _killOnDestroy;
private void LoadOtpFile()
{
new LoadingDialog<object, object, object>(this, true,
//doInBackground
delegate
{
_otpInfo = OathHotpKeyProv.LoadOtpInfo(new KeyProviderQueryContext(_ioConnection, false, false));
return null;
},
//onPostExecute
delegate
{
if (_otpInfo == null)
{
Toast.MakeText(this, GetString(Resource.String.CouldntLoadOtpAuxFile), ToastLength.Long).Show();
return;
}
FindViewById(Resource.Id.init_otp).Visibility = ViewStates.Gone;
FindViewById(Resource.Id.otpEntry).Visibility = ViewStates.Visible;
int c = 0;
foreach (int otpId in _otpTextViewIds)
{
c++;
var otpTextView = FindViewById<EditText>(otpId);
otpTextView.Text = "";
otpTextView.Hint = GetString(Resource.String.otp_hint, new Object[] {c});
otpTextView.SetFilters(new IInputFilter[] {new InputFilterLengthFilter((int)_otpInfo.OtpLength) });
if (c > _otpInfo.OtpsRequired)
{
otpTextView.Visibility = ViewStates.Gone;
}
else
{
otpTextView.TextChanged += (sender, args) =>
{
UpdateOkButtonState();
};
}
}
}
).Execute();
}
protected override void OnCreate(Bundle savedInstanceState)
{
@ -245,15 +331,15 @@ namespace keepass2android
return;
}
_keyFile = GetKeyFile(_ioConnection.Path);
_keyFileOrProvider = GetKeyFile(_ioConnection.Path);
} else
{
SetIoConnectionFromIntent(_ioConnection, i);
_keyFile = i.GetStringExtra(KeyKeyfile);
if (string.IsNullOrEmpty(_keyFile))
_keyFileOrProvider = i.GetStringExtra(KeyKeyfile);
if (string.IsNullOrEmpty(_keyFileOrProvider))
{
_keyFile = GetKeyFile(_ioConnection.Path);
_keyFileOrProvider = GetKeyFile(_ioConnection.Path);
}
}
@ -271,6 +357,19 @@ namespace keepass2android
EditText passwordEdit = FindViewById<EditText>(Resource.Id.password);
FindViewById<EditText>(Resource.Id.pass_keyfile).TextChanged +=
(sender, args) =>
{
_keyFileOrProvider = FindViewById<EditText>(Resource.Id.pass_keyfile).Text;
UpdateOkButtonState();
};
FindViewById<EditText>(Resource.Id.password).TextChanged +=
(sender, args) =>
{
_password = FindViewById<EditText>(Resource.Id.password).Text;
UpdateOkButtonState();
};
passwordEdit.RequestFocus();
Window.SetSoftInputMode(SoftInput.StateVisible);
@ -279,10 +378,48 @@ namespace keepass2android
confirmButton.Click += (sender, e) =>
{
App.Kp2a.GetFileStorage(_ioConnection)
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, 0, false);
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, RequestCodePrepareDbFile, false);
};
Spinner passwordModeSpinner = FindViewById<Spinner>(Resource.Id.password_mode_spinner);
if (passwordModeSpinner != null)
{
UpdateKeyProviderUiState();
passwordModeSpinner.SetSelection((int) KeyProviderType);
passwordModeSpinner.ItemSelected += (sender, args) =>
{
switch (args.Position)
{
case 0:
_keyFileOrProvider = null;
break;
case 1:
_keyFileOrProvider = "";
break;
case 2:
_keyFileOrProvider = KeyProviderIdOtp;
break;
default:
throw new Exception("Unexpected position "+args.Position+" / " + ((ICursor)((AdapterView)sender).GetItemAtPosition(args.Position)).GetString(1));
}
UpdateKeyProviderUiState();
};
FindViewById(Resource.Id.init_otp).Click += (sender, args) =>
{
App.Kp2a.GetOtpAuxFileStorage(_ioConnection)
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, RequestCodePrepareOtpAuxFile, false);
};
}
else
{
//android 2.x
//TODO test
}
UpdateOkButtonState();
@ -327,14 +464,96 @@ namespace keepass2android
RetrieveSettings();
}
private void PeformLoadDatabase()
private void UpdateOkButtonState()
{
String pass = GetEditText(Resource.Id.password);
String key = GetEditText(Resource.Id.pass_keyfile);
if (pass.Length == 0 && key.Length == 0)
switch (KeyProviderType)
{
ErrorMessage(Resource.String.error_nopass);
return;
case KeyProviders.None:
FindViewById(Resource.Id.pass_ok).Enabled = true;
break;
case KeyProviders.KeyFile:
FindViewById(Resource.Id.pass_ok).Enabled = _keyFileOrProvider != "" || _password != "";
break;
case KeyProviders.Otp:
bool enabled = true;
if (_otpInfo == null)
enabled = false;
else
{
int c = 0;
foreach (int otpId in _otpTextViewIds)
{
c++;
var otpTextView = FindViewById<EditText>(otpId);
if ((c <= _otpInfo.OtpsRequired) && (otpTextView.Text == ""))
{
enabled = false;
break;
}
}
}
FindViewById(Resource.Id.pass_ok).Enabled = enabled;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void UpdateKeyProviderUiState()
{
FindViewById(Resource.Id.keyfileLine).Visibility = KeyProviderType == KeyProviders.KeyFile
? ViewStates.Visible
: ViewStates.Gone;
FindViewById(Resource.Id.otpView).Visibility = KeyProviderType == KeyProviders.Otp
? ViewStates.Visible
: ViewStates.Gone;
UpdateOkButtonState();
}
private void PerformLoadDatabase()
{
//no need to check for validity of password because if this method is called, the Ok button was enabled (i.e. there was a valid password)
CompositeKey compositeKey = new CompositeKey();
compositeKey.AddUserKey(new KcpPassword(_password));
if (KeyProviderType == KeyProviders.KeyFile)
{
try
{
compositeKey.AddUserKey(new KcpKeyFile(_keyFileOrProvider));
}
catch (Exception e)
{
Kp2aLog.Log(e.ToString());
throw new KeyFileException();
}
}
else if (KeyProviderType == KeyProviders.Otp)
{
try
{
List<string> lOtps = new List<string>();
foreach (int otpId in _otpTextViewIds)
{
string otpText = FindViewById<EditText>(otpId).Text;
if (!String.IsNullOrEmpty(otpText))
lOtps.Add(otpText);
}
CreateOtpSecret(lOtps);
}
catch (Exception)
{
const string strMain = "Failed to create OTP key!";
const string strLine1 = "Make sure you've entered the correct OTPs.";
Toast.MakeText(this, strMain + " " + strLine1, ToastLength.Long).Show();
return;
}
compositeKey.AddUserKey(new KcpCustomKey(OathHotpKeyProv.Name, _otpInfo.Secret, true));
}
CheckBox cbQuickUnlock = (CheckBox) FindViewById(Resource.Id.enable_quickunlock);
@ -345,7 +564,7 @@ namespace keepass2android
MakePasswordMaskedOrVisible();
Handler handler = new Handler();
LoadDb task = new LoadDb(App.Kp2a, _ioConnection, _loadDbTask, pass, key, new AfterLoad(handler, this));
LoadDb task = new LoadDb(App.Kp2a, _ioConnection, _loadDbTask, compositeKey, _keyFileOrProvider, new AfterLoad(handler, this));
_loadDbTask = null; // prevent accidental re-use
SetNewDefaultFile();
@ -353,6 +572,44 @@ namespace keepass2android
new ProgressTask(App.Kp2a, this, task).Run();
}
private void CreateOtpSecret(List<string> lOtps)
{
byte[] pbSecret;
if (!string.IsNullOrEmpty(_otpInfo.EncryptedSecret)) // < v2.0
{
byte[] pbKey32 = OtpUtil.KeyFromOtps(lOtps.ToArray(), 0,
lOtps.Count, Convert.FromBase64String(
_otpInfo.TransformationKey), _otpInfo.TransformationRounds);
if (pbKey32 == null) throw new InvalidOperationException();
pbSecret = OtpUtil.DecryptData(_otpInfo.EncryptedSecret,
pbKey32, Convert.FromBase64String(_otpInfo.EncryptionIV));
if (pbSecret == null) throw new InvalidOperationException();
_otpInfo.Secret = pbSecret;
_otpInfo.Counter += (ulong) _otpInfo.OtpsRequired;
}
else // >= v2.0, supporting look-ahead
{
bool bSuccess = false;
for (int i = 0; i < _otpInfo.EncryptedSecrets.Count; ++i)
{
OtpEncryptedData d = _otpInfo.EncryptedSecrets[i];
pbSecret = OtpUtil.DecryptSecret(d, lOtps.ToArray(), 0,
lOtps.Count);
if (pbSecret != null)
{
_otpInfo.Secret = pbSecret;
_otpInfo.Counter += ((ulong) _otpInfo.OtpsRequired +
(ulong) i);
bSuccess = true;
break;
}
}
if (!bSuccess) throw new InvalidOperationException();
}
}
private void MakePasswordMaskedOrVisible()
{
TextView password = (TextView) FindViewById(Resource.Id.password);
@ -521,13 +778,9 @@ namespace keepass2android
private String GetKeyFile(String filename) {
if ( _rememberKeyfile ) {
FileDbHelper dbHelp = App.Kp2a.FileDbHelper;
String keyfile = dbHelp.GetFileByName(filename);
return keyfile;
return App.Kp2a.FileDbHelper.GetKeyFileForFile(filename);
} else {
return "";
return null;
}
}
@ -541,7 +794,8 @@ namespace keepass2android
{
FindViewById(Resource.Id.filename_group).Visibility = ViewStates.Visible;
}
SetEditText(Resource.Id.pass_keyfile, _keyFile);
if (KeyProviderType == KeyProviders.KeyFile)
SetEditText(Resource.Id.pass_keyfile, _keyFileOrProvider);
}
protected override void OnDestroy()

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="12dip"
android:layout_marginRight="12dip"
android:layout_marginBottom="12dip">
android:layout_marginBottom="12dip"
android:orientation="vertical"
>
<RelativeLayout
android:id="@+id/filename_group"
android:layout_width="fill_parent"
@ -49,13 +51,18 @@
android:id="@+id/password_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/filename_group"
android:text="" />
<LinearLayout
<Spinner
android:id="@+id/password_mode_spinner"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:entries="@array/password_modes"
/>
<LinearLayout
android:id="@+id/passwordLine"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/password_label"
android:orientation="horizontal">
<EditText
android:id="@+id/password"
@ -71,11 +78,10 @@
android:layout_height="wrap_content"
android:src="@drawable/ic_menu_view" />
</LinearLayout>
<LinearLayout
<LinearLayout
android:id="@+id/keyfileLine"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/passwordLine"
android:orientation="horizontal">
<EditText
android:id="@+id/pass_keyfile"
@ -90,22 +96,82 @@
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_folder_small" />
</LinearLayout>
<Button
<LinearLayout
android:id="@+id/otpView"
android:layout_marginLeft="12dip"
android:layout_marginRight="12dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<Button
android:id="@+id/init_otp"
android:text="@string/init_otp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout
android:id="@+id/otpEntry"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:orientation="vertical"
>
<TextView
android:id="@+id/otp_expl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/otp_explanation" />
<EditText
android:id="@+id/otp1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="93317749"
android:singleLine="true" />
<EditText
android:id="@+id/otp2"
android:text="54719327"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true" />
<EditText
android:id="@+id/otp3"
android:text="49844651"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true" />
<EditText
android:id="@+id/otp4"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true" />
<EditText
android:id="@+id/otp5"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true" />
<EditText
android:id="@+id/otp6"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true" />
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/pass_ok"
android:text="@android:string/ok"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/keyfileLine" />
android:layout_height="wrap_content"/>
<Button
android:id="@+id/kill_app"
android:text="@string/kill_app_label"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/pass_ok" />
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/enable_quickunlock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/kill_app"
android:text="@string/enable_quickunlock" />
</RelativeLayout>
</LinearLayout>

View File

@ -343,6 +343,14 @@
<string name="error_adding_keyfile">Error while adding the keyfile!</string>
<string name="init_otp">Enter OTPs…</string>
<string name="otp_explanation">Enter the next One-time-passwords (OTPs). Swipe your Yubikey NEO at the back of your device to enter via NFC.</string>
<string name="otp_hint">OTP %1$d</string>
<string name="CouldntLoadOtpAuxFile">Could not load auxiliary OTP file!</string>
<string name="loading">Loading…</string>
<string name="ChangeLog_title">Change log</string>
<string name="ChangeLog_0_9_2">
@ -462,4 +470,9 @@ Initial public release
<item>Remember username only</item>
<item>Remember username and password</item>
</string-array>
<string-array name="password_modes">
<item>Password only</item>
<item>Password + Key file</item>
<item>Password + OTP</item>
</string-array>
</resources>

View File

@ -0,0 +1,164 @@
using System;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Java.Lang;
using Exception = System.Exception;
using Object = Java.Lang.Object;
namespace keepass2android.Utils
{
public class LoadingDialog<TParams, TProgress, TResult> : AsyncTask<TParams, TProgress, TResult>
{
private readonly Context _context;
private readonly string _message;
private readonly bool _cancelable;
readonly Func<Object[], Object> _doInBackground;
readonly Action<Object> _onPostExecute;
private ProgressDialog mDialog;
/**
* Default is {@code 500}ms
*/
private int mDelayTime = 500;
/**
* Flag to use along with {@link #mDelayTime}
*/
private bool mFinished = false;
private Exception mLastException;
public LoadingDialog(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
}
public LoadingDialog(Context context, string message, bool cancelable, Func<Object[], Object> doInBackground,
Action<Object> onPostExecute)
{
_context = context;
_message = message;
_cancelable = cancelable;
_doInBackground = doInBackground;
_onPostExecute = onPostExecute;
Initialize();
}
private void Initialize()
{
mDialog = new ProgressDialog(_context);
mDialog.SetMessage(_message);
mDialog.Indeterminate = true;
mDialog.SetCancelable(_cancelable);
if (_cancelable)
{
mDialog.SetCanceledOnTouchOutside(true);
mDialog.CancelEvent += (sender, args) => mDialog.Cancel();
}
}
public LoadingDialog(Context context, bool cancelable, Func<Object[], Object> doInBackground, Action<Object> onPostExecute)
{
_message = context.GetString(Resource.String.loading);
_context = context;
_cancelable = cancelable;
_doInBackground = doInBackground;
_onPostExecute = onPostExecute;
Initialize();
}
protected override void OnPreExecute()
{
new Handler().PostDelayed(() =>
{
if (!mFinished)
{
try
{
/*
* sometime the activity has been finished before we
* show this dialog, it will raise error
*/
mDialog.Show();
}
catch (Exception t)
{
Kp2aLog.Log(t.ToString());
}
}
}
, mDelayTime);
}
/**
* If you override this method, you must call {@code super.onCancelled()} at
* beginning of the method.
*/
protected override void OnCancelled() {
DoFinish();
base.OnCancelled();
}// onCancelled()
private void DoFinish() {
mFinished = true;
try {
/*
* Sometime the activity has been finished before we dismiss this
* dialog, it will raise error.
*/
mDialog.Dismiss();
} catch (Exception e)
{
Kp2aLog.Log(e.ToString());
}
}// doFinish()
/**
* Sets last exception. This method is useful in case an exception raises
* inside {@link #doInBackground(Void...)}
*
* @param t
* {@link Throwable}
*/
protected void SetLastException(Exception e) {
mLastException = e;
}// setLastException()
/**
* Gets last exception.
*
* @return {@link Throwable}
*/
protected Exception GetLastException() {
return mLastException;
}// getLastException()
protected override Object DoInBackground(params Object[] @params)
{
return _doInBackground(@params);
}
protected override TResult RunInBackground(params TParams[] @params)
{
throw new NotImplementedException();
}
protected override void OnPostExecute(Object result)
{
DoFinish();
if (_onPostExecute != null)
_onPostExecute(result);
}
}
}

View File

@ -20,7 +20,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using KeePassLib.Utility;
@ -39,9 +38,8 @@ namespace OtpKeyProv
FmtHex, FmtBase64, FmtBase32, FmtUtf8, FmtDec
};
public static OtpDataFmt? GetOtpDataFormat(ComboBox cmb)
public static OtpDataFmt? GetOtpDataFormat(String strFmt)
{
string strFmt = (cmb.SelectedItem as string);
if(strFmt == null) return null; // No assert
if(strFmt == FmtHex) return OtpDataFmt.Hex;

View File

@ -1,4 +1,6 @@
/*
This file was modified my Philipp Crocoll, 2013. Based on:
OtpKeyProv Plugin
Copyright (C) 2011-2012 Dominik Reichl <dominik.reichl@t-online.de>
@ -19,81 +21,49 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using OtpKeyProv.Forms;
using KeePass.UI;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using KeePassLib.Utility;
using keepass2android;
using keepass2android.Io;
namespace OtpKeyProv
{
public sealed class OathHotpKeyProv : KeyProvider
public sealed class OathHotpKeyProv
/*removed base class KeyProvider because "synchronous" interface is not suitable on Android*/
{
private const string AuxFileExt = ".otp.xml";
private const string ProvType = "OATH HOTP / RFC 4226";
private const string ProvVersion = "2.0"; // File version, not OtpKeyProv version
public override string Name
public static string Name
{
get { return "One-Time Passwords (OATH HOTP)"; }
}
public override bool SecureDesktopCompatible
{
get { return true; }
}
public override byte[] GetKey(KeyProviderQueryContext ctx)
{
try
{
if(ctx.CreatingNewKey) return Create(ctx);
return Open(ctx);
}
catch(Exception ex) { MessageService.ShowWarning(ex.Message); }
public const string ShortProductName = "OtpKeyProv";
public const string ProductName = "OtpKeyProv KeePass Plugin";
return null;
}
private static IOConnectionInfo GetAuxFileIoc(KeyProviderQueryContext ctx)
{
IOConnectionInfo ioc = ctx.DatabaseIOInfo.CloneDeep();
ioc.Path = UrlUtil.StripExtension(ioc.Path) + AuxFileExt;
return ioc;
IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(ioc);
IOConnectionInfo iocAux = fileStorage.GetFilePath(fileStorage.GetParentPath(ioc),
fileStorage.GetFilenameWithoutPathAndExt(ioc) + AuxFileExt);
return iocAux;
}
private static byte[] Create(KeyProviderQueryContext ctx)
public static OtpInfo LoadOtpInfo(KeyProviderQueryContext ctx)
{
IOConnectionInfo iocPrev = GetAuxFileIoc(ctx);
OtpInfo otpInfo = OtpInfo.Load(iocPrev);
if(otpInfo == null) otpInfo = new OtpInfo();
OtpKeyCreationForm dlg = new OtpKeyCreationForm();
dlg.InitEx(otpInfo, ctx);
if(UIUtil.ShowDialogAndDestroy(dlg) != DialogResult.OK)
return null;
if(!CreateAuxFile(otpInfo, ctx)) return null;
return otpInfo.Secret;
return OtpInfo.Load(GetAuxFileIoc(ctx));
}
private static byte[] Open(KeyProviderQueryContext ctx)
/*
private static byte[] Open(KeyProviderQueryContext ctx, OtpInfo otpInfo)
{
IOConnectionInfo ioc = GetAuxFileIoc(ctx);
OtpInfo otpInfo = OtpInfo.Load(ioc);
if(otpInfo == null)
{
MessageService.ShowWarning("Failed to load auxiliary OTP info file:",
ioc.GetDisplayName());
return null;
}
if(otpInfo.Type != ProvType)
{
MessageService.ShowWarning("Unknown OTP generator type!");
@ -108,13 +78,72 @@ namespace OtpKeyProv
if(!CreateAuxFile(otpInfo, ctx)) return null;
return otpInfo.Secret;
}
* */
/// <summary>
/// Sets the "Secret" field in otpInfo based on the list of entered OTPs (lOtps) or the entered secret itself which is in format fmt
/// </summary>
/// based on the code in OtpKeyPromptForm.cs
public void SetSecret(OtpInfo otpInfo, List<string> lOtps, string secret, OtpDataFmt? fmt)
{
try
{
byte[] pbSecret = EncodingUtil.ParseKey(secret,
(fmt.HasValue ? fmt.Value : OtpDataFmt.Hex));
if (pbSecret != null)
{
otpInfo.Secret = pbSecret;
return;
}
if (!string.IsNullOrEmpty(otpInfo.EncryptedSecret)) // < v2.0
{
byte[] pbKey32 = OtpUtil.KeyFromOtps(lOtps.ToArray(), 0,
lOtps.Count, Convert.FromBase64String(
otpInfo.TransformationKey), otpInfo.TransformationRounds);
if (pbKey32 == null) throw new InvalidOperationException();
pbSecret = OtpUtil.DecryptData(otpInfo.EncryptedSecret,
pbKey32, Convert.FromBase64String(otpInfo.EncryptionIV));
if (pbSecret == null) throw new InvalidOperationException();
otpInfo.Secret = pbSecret;
otpInfo.Counter += (ulong) otpInfo.OtpsRequired;
}
else // >= v2.0, supporting look-ahead
{
bool bSuccess = false;
for (int i = 0; i < otpInfo.EncryptedSecrets.Count; ++i)
{
OtpEncryptedData d = otpInfo.EncryptedSecrets[i];
pbSecret = OtpUtil.DecryptSecret(d, lOtps.ToArray(), 0,
lOtps.Count);
if (pbSecret != null)
{
otpInfo.Secret = pbSecret;
otpInfo.Counter += ((ulong) otpInfo.OtpsRequired +
(ulong) i);
bSuccess = true;
break;
}
}
if (!bSuccess) throw new InvalidOperationException();
}
}
catch (Exception)
{
//todo
throw;
}
}
private static bool CreateAuxFile(OtpInfo otpInfo,
KeyProviderQueryContext ctx)
{
otpInfo.Type = ProvType;
otpInfo.Version = ProvVersion;
otpInfo.Generator = OtpKeyProvExt.ProductName;
otpInfo.Generator = ProductName;
otpInfo.EncryptSecret();

View File

@ -1,4 +1,6 @@
/*
This file was modified my Philipp Crocoll, 2013. Based on:
OtpKeyProv Plugin
Copyright (C) 2011-2012 Dominik Reichl <dominik.reichl@t-online.de>
@ -31,6 +33,7 @@ using KeePassLib.Cryptography;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using KeePassLib.Utility;
using keepass2android;
namespace OtpKeyProv
{
@ -168,12 +171,15 @@ namespace OtpKeyProv
try
{
sIn = IOConnection.OpenRead(ioc);
sIn = App.Kp2a.GetOtpAuxFileStorage(ioc).OpenFileForRead(ioc);
XmlSerializer xs = new XmlSerializer(typeof(OtpInfo));
return (OtpInfo)xs.Deserialize(sIn);
XmlSerializer xs = new XmlSerializer(typeof (OtpInfo));
return (OtpInfo) xs.Deserialize(sIn);
}
catch (Exception e)
{
Kp2aLog.Log(e.ToString());
}
catch(Exception) { }
finally
{
if(sIn != null) sIn.Close();
@ -188,20 +194,23 @@ namespace OtpKeyProv
try
{
sOut = IOConnection.OpenWrite(ioc);
using (var trans = App.Kp2a.GetOtpAuxFileStorage(ioc)
.OpenWriteTransaction(ioc, App.Kp2a.GetBooleanPreference(PreferenceKey.UseFileTransactions)))
{
XmlWriterSettings xws = new XmlWriterSettings();
xws.CloseOutput = true;
xws.Encoding = StrUtil.Utf8;
xws.Indent = true;
xws.IndentChars = "\t";
XmlWriterSettings xws = new XmlWriterSettings();
xws.CloseOutput = true;
xws.Encoding = StrUtil.Utf8;
xws.Indent = true;
xws.IndentChars = "\t";
XmlWriter xw = XmlWriter.Create(trans.OpenFile(), xws);
XmlWriter xw = XmlWriter.Create(sOut, xws);
XmlSerializer xs = new XmlSerializer(typeof (OtpInfo));
xs.Serialize(xw, otpInfo);
XmlSerializer xs = new XmlSerializer(typeof(OtpInfo));
xs.Serialize(xw, otpInfo);
xw.Close();
xw.Close();
trans.CommitWrite();
}
return true;
}
catch(Exception) { Debug.Assert(false); }

View File

@ -107,9 +107,9 @@ namespace keepass2android
Application.Context.SendBroadcast(new Intent(Intents.DatabaseLocked));
}
public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, string password, string keyFile, ProgressDialogStatusLogger statusLogger)
public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compositeKey, ProgressDialogStatusLogger statusLogger)
{
_db.LoadData(this, ioConnectionInfo, memoryStream, password, keyFile, statusLogger);
_db.LoadData(this, ioConnectionInfo, memoryStream, compositeKey, statusLogger);
UpdateOngoingNotification();
}
@ -354,10 +354,9 @@ namespace keepass2android
{
IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo);
var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
if (prefs.GetBoolean(Application.Context.Resources.GetString(Resource.String.UseOfflineCache_key), true))
if (DatabaseCacheEnabled)
{
//TODO
return new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this);
}
else
@ -494,6 +493,42 @@ namespace keepass2android
{
return GetFileStorage(new IOConnectionInfo() {Path = protocolId + "://"});
}
/// <summary>
/// returns a file storage object to be used when accessing the auxiliary OTP file
/// </summary>
/// The reason why this requires a different file storage is the different caching behavior.
public IFileStorage GetOtpAuxFileStorage(IOConnectionInfo iocInfo)
{
if (iocInfo.IsLocalFile())
return new BuiltInFileStorage();
else
{
IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo);
if (DatabaseCacheEnabled)
{
return new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this);
}
else
{
return innerFileStorage;
}
}
}
private static bool DatabaseCacheEnabled
{
get
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
bool cacheEnabled = prefs.GetBoolean(Application.Context.Resources.GetString(Resource.String.UseOfflineCache_key),
true);
return cacheEnabled;
}
}
}

View File

@ -230,12 +230,12 @@ namespace keepass2android
}
public String GetFileByName(String name) {
public String GetKeyFileForFile(String name) {
ICursor cursor = mDb.Query(true, FileTable, GetColumnList(),
KeyFileFilename + "= ?", new[] {name}, null, null, null, null);
if ( cursor == null ) {
return "";
return null;
}
String keyfileFilename;
@ -244,9 +244,11 @@ namespace keepass2android
keyfileFilename = cursor.GetString(cursor.GetColumnIndexOrThrow(KeyFileKeyfile));
} else {
// Cursor is empty
keyfileFilename = "";
keyfileFilename = null;
}
cursor.Close();
if (keyfileFilename == "")
return null;
return keyfileFilename;
}

View File

@ -373,7 +373,7 @@ namespace keepass2android
#if !EXCLUDE_FILECHOOSER
StartFileChooser(ioc.Path);
#else
LaunchPasswordActivityForIoc(new IOConnectionInfo { Path = "/mnt/sdcard/keepass/keepass.kdbx"});
LaunchPasswordActivityForIoc(new IOConnectionInfo { Path = "/mnt/sdcard/keepass/yubi2.kdbx"});
#endif
}
if ((resultCode == Result.Canceled) && (data != null) && (data.HasExtra("EXTRA_ERROR_MESSAGE")))

View File

@ -82,6 +82,10 @@
<Reference Include="Mono.Android.Support.v4" />
</ItemGroup>
<ItemGroup>
<Compile Include="addons\OtpKeyProv\EncodingUtil.cs" />
<Compile Include="addons\OtpKeyProv\OathHotpKeyProv.cs" />
<Compile Include="addons\OtpKeyProv\OtpInfo.cs" />
<Compile Include="addons\OtpKeyProv\OtpUtil.cs" />
<Compile Include="app\NoFileStorageFoundException.cs" />
<Compile Include="CreateDatabaseActivity.cs" />
<Compile Include="fileselect\FileChooserFileProvider.cs" />
@ -102,6 +106,7 @@
<Compile Include="search\SearchProvider.cs" />
<Compile Include="services\OngoingNotificationsService.cs" />
<Compile Include="settings\DatabaseSettingsActivity.cs" />
<Compile Include="Utils\LoadingDialog.cs" />
<Compile Include="Utils\Util.cs" />
<Compile Include="intents\Intents.cs" />
<Compile Include="timeout\TimeoutHelper.cs" />
@ -629,7 +634,9 @@
<AndroidResource Include="Resources\layout-v14\SaveButton.xml" />
<AndroidResource Include="Resources\layout-v14\generate_password.xml" />
<AndroidResource Include="Resources\layout-v14\icon_picker.xml" />
<AndroidResource Include="Resources\layout-v14\password.xml" />
<AndroidResource Include="Resources\layout-v14\password.xml">
<SubType>Designer</SubType>
</AndroidResource>
<AndroidResource Include="Resources\layout\InViewButton.xml" />
<AndroidResource Include="Resources\drawable\collections_collection.png" />
<AndroidResource Include="Resources\drawable\collections_new_label.png" />