extended implementation of OTP

This commit is contained in:
Philipp Crocoll 2013-11-20 19:14:57 +01:00
parent 66cd05b9f4
commit aeaba47573
16 changed files with 1443 additions and 969 deletions

View File

@ -60,7 +60,7 @@ namespace keepass2android.Io
/// </summary>
public class CachingFileStorage: IFileStorage
{
private readonly IFileStorage _cachedStorage;
protected readonly IFileStorage _cachedStorage;
private readonly ICacheSupervisor _cacheSupervisor;
private readonly string _streamCacheDir;
@ -179,14 +179,21 @@ namespace keepass2android.Io
{
if (TryUpdateRemoteFile(localData, ioc, true, hash))
_cacheSupervisor.UpdatedRemoteFileOnLoad(ioc);
return File.OpenRead(cachedFilePath);
}
}
else
{
//conflict: both files changed.
return OpenFileForReadWithConflict(ioc, cachedFilePath);
}
}
protected virtual Stream OpenFileForReadWithConflict(IOConnectionInfo ioc, string cachedFilePath)
{
//signal that we're loading from local
_cacheSupervisor.NotifyOpenFromLocalDueToConflict(ioc);
}
return File.OpenRead(cachedFilePath);
}
@ -214,19 +221,6 @@ namespace keepass2android.Io
private Stream OpenFileForReadWhenNoLocalChanges(IOConnectionInfo ioc, string cachedFilePath)
{
//open stream:
using (Stream file = _cachedStorage.OpenFileForRead(ioc))
{
//copy to cache:
//note: we might use the file version to check if it's already in the cache and if copying is required.
//However, this is safer.
string fileHash;
using (HashingStreamEx cachedFile = new HashingStreamEx(File.Create(cachedFilePath), true, new SHA256Managed()))
{
file.CopyTo(cachedFile);
cachedFile.Close();
fileHash = MemUtil.ByteArrayToHexString(cachedFile.Hash);
}
//remember current hash
string previousHash = null;
@ -234,9 +228,10 @@ namespace keepass2android.Io
if (File.Exists(baseVersionFilePath))
previousHash = File.ReadAllText(baseVersionFilePath);
//save hash in cache files:
File.WriteAllText(VersionFilePath(ioc), fileHash);
File.WriteAllText(baseVersionFilePath, fileHash);
//copy to cache:
var fileHash = UpdateCacheFromRemote(ioc, cachedFilePath);
//notify supervisor what we did:
if (previousHash != fileHash)
@ -245,8 +240,35 @@ namespace keepass2android.Io
_cacheSupervisor.LoadedFromRemoteInSync(ioc);
return File.OpenRead(cachedFilePath);
}
/// <summary>
/// copies the file in ioc to the local cache. Updates the cache version files and returns the new file hash.
/// </summary>
protected string UpdateCacheFromRemote(IOConnectionInfo ioc, string cachedFilePath)
{
//note: we might use the file version to check if it's already in the cache and if copying is required.
//However, this is safer.
string fileHash;
//open stream:
using (Stream remoteFile = _cachedStorage.OpenFileForRead(ioc))
{
using (HashingStreamEx cachedFile = new HashingStreamEx(File.Create(cachedFilePath), true, new SHA256Managed()))
{
remoteFile.CopyTo(cachedFile);
cachedFile.Close();
fileHash = MemUtil.ByteArrayToHexString(cachedFile.Hash);
}
}
//save hash in cache files:
File.WriteAllText(VersionFilePath(ioc), fileHash);
File.WriteAllText(BaseVersionFilePath(ioc), fileHash);
return fileHash;
}
private bool TryUpdateRemoteFile(Stream cachedData, IOConnectionInfo ioc, bool useFileTransaction, string hash)
@ -266,7 +288,7 @@ namespace keepass2android.Io
}
}
private void UpdateRemoteFile(Stream cachedData, IOConnectionInfo ioc, bool useFileTransaction, string hash)
protected void UpdateRemoteFile(Stream cachedData, IOConnectionInfo ioc, bool useFileTransaction, string hash)
{
//try to write to remote:
using (

View File

@ -44,6 +44,8 @@ namespace keepass2android
CheckingDatabaseForChanges,
RemoteDatabaseUnchanged,
CannotMoveGroupHere,
ErrorOcurred
ErrorOcurred,
SynchronizingOtpAuxFile,
SavingOtpAuxFile
}
}

View File

@ -44,6 +44,13 @@ namespace keepass2android
return KpDatabase == null ? null : KpDatabase.IOConnectionInfo;
}
}
/// <summary>
/// if an OTP key was used, this property tells the location of the OTP auxiliary file.
/// Must be set after loading.
/// </summary>
public IOConnectionInfo OtpAuxFileIoc { get; set; }
public string LastFileVersion;
public SearchDbHelper SearchHelper;
@ -192,6 +199,7 @@ namespace keepass2android
KpDatabase = null;
_loaded = false;
_reloadRequested = false;
OtpAuxFileIoc = null;
}
public void MarkAllGroupsAsDirty() {

View File

@ -25,6 +25,7 @@ using Android.Widget;
using KeePassLib;
using Android.Preferences;
using KeePassLib.Interfaces;
using KeePassLib.Serialization;
using KeePassLib.Utility;
using keepass2android.Io;
using keepass2android.database.edit;
@ -367,6 +368,37 @@ namespace keepass2android
return base.OnOptionsItemSelected(item);
}
class SyncOtpAuxFile: OnFinish
{
private readonly IOConnectionInfo _ioc;
public SyncOtpAuxFile(IOConnectionInfo ioc)
{
_ioc = ioc;
}
public override void Run()
{
if (Handler != null)
{
Handler.Post(DoSyncOtpAuxFile);
}
else
DoSyncOtpAuxFile();
base.Run();
}
private void DoSyncOtpAuxFile()
{
StatusLogger.UpdateMessage(UiStringKey.SynchronizingOtpAuxFile);
//simply open the file. The file storage does a complete sync.
using (App.Kp2a.GetOtpAuxFileStorage(_ioc).OpenFileForRead(_ioc))
{
}
}
}
private void Synchronize()
{
var filestorage = App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc);

View File

@ -3,6 +3,7 @@ using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Widget;
using Java.Util.Regex;
namespace keepass2android
@ -54,8 +55,30 @@ namespace keepass2android
i.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
i.PutExtra(Intents.OtpExtraKey, GetOtpFromIntent(Intent));
try
{
string otp = GetOtpFromIntent(Intent);
if (otp == null)
throw new Exception("Otp must not be null!");
i.PutExtra(Intents.OtpExtraKey, otp);
}
catch (Exception e)
{
Kp2aLog.Log(e.ToString());
Toast.MakeText(this, "No Yubikey OTP found!", ToastLength.Long).Show();
Finish();
return;
}
if (App.Kp2a.GetDb().Loaded)
{
Toast.MakeText(this, GetString(Resource.String.otp_discarded_because_db_open), ToastLength.Long).Show();
}
else
{
StartActivity(i);
}
Finish();
}

View File

@ -17,7 +17,10 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using Android.App;
using Android.Content;
using Android.Database;
@ -27,15 +30,17 @@ using Android.Views;
using Android.Widget;
using Java.Net;
using Android.Preferences;
using Java.IO;
using Android.Text;
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 File = Java.IO.File;
using FileNotFoundException = Java.IO.FileNotFoundException;
using MemoryStream = System.IO.MemoryStream;
using Object = Java.Lang.Object;
using Process = Android.OS.Process;
@ -55,11 +60,10 @@ namespace keepass2android
//int values correspond to indices in passwordSpinner
None = 0,
KeyFile = 1,
Otp = 2
Otp = 2,
OtpRecovery = 3
}
bool _showPassword;
public const String KeyDefaultFilename = "defaultFileName";
public const String KeyFilename = "fileName";
@ -71,19 +75,23 @@ namespace keepass2android
private const String ViewIntent = "android.intent.action.VIEW";
private const string ShowpasswordKey = "ShowPassword";
private const string KeyProviderIdOtp = "KP2A-OTP";
private const string KeyProviderIdOtpRecovery = "KP2A-OTPSecret";
private const int RequestCodePrepareDbFile = 1000;
private const int RequestCodePrepareOtpAuxFile = 1001;
private Task<MemoryStream> _loadDbTask;
private IOConnectionInfo _ioConnection;
private String _keyFileOrProvider;
bool _showPassword;
internal AppTask AppTask;
private bool _killOnDestroy;
private string _password = "";
//OTPs which should be entered into the OTP fields as soon as these become visible
private readonly List<String> _pendingOtps = new List<string>();
private List<String> _pendingOtps = new List<string>();
private const int RequestCodePrepareDbFile = 1000;
private const int RequestCodePrepareOtpAuxFile = 1001;
KeyProviders KeyProviderType
@ -94,6 +102,8 @@ namespace keepass2android
return KeyProviders.None;
if (_keyFileOrProvider == KeyProviderIdOtp)
return KeyProviders.Otp;
if (_keyFileOrProvider == KeyProviderIdOtpRecovery)
return KeyProviders.OtpRecovery;
return KeyProviders.KeyFile;
}
}
@ -103,7 +113,12 @@ namespace keepass2android
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};
private readonly int[] _otpTextViewIds = new[] {Resource.Id.otp1, Resource.Id.otp2, Resource.Id.otp3, Resource.Id.otp4, Resource.Id.otp5, Resource.Id.otp6};
private const string OtpInfoKey = "OtpInfoKey";
private const string EnteredOtpsKey = "EnteredOtpsKey";
private const string PendingOtpsKey = "PendingOtpsKey";
private const string PasswordKey = "PasswordKey";
private const string KeyFileOrProviderKey = "KeyFileOrProviderKey";
public PasswordActivity (IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
@ -217,7 +232,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);
@ -234,7 +249,7 @@ namespace keepass2android
EditText fn = (EditText) FindViewById(Resource.Id.pass_keyfile);
fn.Text = filename;
_keyFileOrProvider = filename;
}
}
break;
@ -265,6 +280,17 @@ namespace keepass2android
Toast.MakeText(this, GetString(Resource.String.CouldntLoadOtpAuxFile), ToastLength.Long).Show();
return;
}
IList<string> prefilledOtps = _pendingOtps;
ShowOtpEntry(prefilledOtps);
_pendingOtps.Clear();
}
).Execute();
}
private void ShowOtpEntry(IList<string> prefilledOtps)
{
FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone;
FindViewById(Resource.Id.otpEntry).Visibility = ViewStates.Visible;
int c = 0;
@ -273,9 +299,9 @@ namespace keepass2android
{
c++;
var otpTextView = FindViewById<EditText>(otpId);
if (c <= _pendingOtps.Count)
if (c <= prefilledOtps.Count)
{
otpTextView.Text = _pendingOtps[c-1];
otpTextView.Text = prefilledOtps[c - 1];
}
else
{
@ -289,27 +315,19 @@ namespace keepass2android
}
else
{
otpTextView.TextChanged += (sender, args) =>
{
UpdateOkButtonState();
};
otpTextView.TextChanged += (sender, args) => { UpdateOkButtonState(); };
}
}
_pendingOtps.Clear();
}
).Execute();
}
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
if (savedInstanceState != null)
_showPassword = savedInstanceState.GetBoolean(ShowpasswordKey, false);
Intent i = Intent;
AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent);
Intent i = Intent;
String action = i.Action;
_prefs = PreferenceManager.GetDefaultSharedPreferences(this);
@ -320,84 +338,11 @@ namespace keepass2android
if (action != null && action.Equals(ViewIntent))
{
//started from "view" intent (e.g. from file browser)
_ioConnection.Path = i.DataString;
if (! _ioConnection.Path.Substring(0, 7).Equals("file://"))
{
//TODO: this might no longer be required as we can handle http(s) and ftp as well (but we need server credentials therefore)
Toast.MakeText(this, Resource.String.error_can_not_handle_uri, ToastLength.Long).Show();
Finish();
return;
}
_ioConnection.Path = URLDecoder.Decode(_ioConnection.Path.Substring(7));
if (_ioConnection.Path.Length == 0)
{
// No file name
Toast.MakeText(this, Resource.String.FileNotFound, ToastLength.Long).Show();
Finish();
return;
}
File dbFile = new File(_ioConnection.Path);
if (! dbFile.Exists())
{
// File does not exist
Toast.MakeText(this, Resource.String.FileNotFound, ToastLength.Long).Show();
Finish();
return;
}
_keyFileOrProvider = GetKeyFile(_ioConnection.Path);
if (!GetIocFromViewIntent(i)) return;
}
else if ((action != null) && (action.Equals(Intents.StartWithOtp)))
{
//create called after detecting an OTP via NFC
//this means the Activity was not on the back stack before, i.e. no database has been selected
_ioConnection = null;
//see if we can get a database from recent:
if (App.Kp2a.FileDbHelper.HasRecentFiles())
{
ICursor filesCursor = App.Kp2a.FileDbHelper.FetchAllFiles();
StartManagingCursor(filesCursor);
filesCursor.MoveToFirst();
IOConnectionInfo ioc = App.Kp2a.FileDbHelper.CursorToIoc(filesCursor);
if (App.Kp2a.GetFileStorage(ioc).RequiresSetup(ioc) == false)
{
IFileStorage fileStorage = App.Kp2a.GetFileStorage(ioc);
if (!fileStorage.RequiresCredentials(ioc))
{
//ok, we can use this file
_ioConnection = ioc;
}
}
}
if (_ioConnection == null)
{
//We need to go to FileSelectActivity first.
//For security reasons: discard the OTP (otherwise the user might not select a database now and forget
//about the OTP, but it would still be stored in the Intents and later be passed to PasswordActivity again.
Toast.MakeText(this, GetString(Resource.String.otp_discarded_because_no_db), ToastLength.Long).Show();
GoToFileSelectActivity();
Finish();
return;
}
//user obviously wants to use OTP:
_keyFileOrProvider = KeyProviderIdOtp;
//remember the OTP for later use
_pendingOtps.Add(Intent.GetStringExtra(Intents.OtpExtraKey));
Intent.RemoveExtra(Intents.OtpExtraKey);
if (!GetIocFromOtpIntent(savedInstanceState, i)) return;
}
else
{
@ -419,9 +364,10 @@ namespace keepass2android
SetContentView(Resource.Layout.password);
PopulateView();
InitializeFilenameView();
EditText passwordEdit = FindViewById<EditText>(Resource.Id.password);
if (KeyProviderType == KeyProviders.KeyFile)
SetEditText(Resource.Id.pass_keyfile, _keyFileOrProvider);
FindViewById<EditText>(Resource.Id.pass_keyfile).TextChanged +=
(sender, args) =>
@ -437,20 +383,167 @@ namespace keepass2android
UpdateOkButtonState();
};
FindViewById<EditText>(Resource.Id.pass_otpsecret).TextChanged += (sender, args) => UpdateOkButtonState();
EditText passwordEdit = FindViewById<EditText>(Resource.Id.password);
passwordEdit.RequestFocus();
Window.SetSoftInputMode(SoftInput.StateVisible);
InitializeOkButton();
InitializePasswordModeSpinner();
InitializeOtpSecretSpinner();
UpdateOkButtonState();
InitializeTogglePasswordButton();
InitializeKeyfileBrowseButton();
InitializeQuickUnlockCheckbox();
RestoreState(savedInstanceState);
}
private void InitializeOtpSecretSpinner()
{
Spinner spinner = FindViewById<Spinner>(Resource.Id.otpsecret_format_spinner);
ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<String>(this, Android.Resource.Layout.SimpleSpinnerDropDownItem, EncodingUtil.Formats);
spinner.Adapter = spinnerArrayAdapter;
}
private bool GetIocFromOtpIntent(Bundle savedInstanceState, Intent i)
{
//create called after detecting an OTP via NFC
//this means the Activity was not on the back stack before, i.e. no database has been selected
_ioConnection = null;
//see if we can get a database from recent:
if (App.Kp2a.FileDbHelper.HasRecentFiles())
{
ICursor filesCursor = App.Kp2a.FileDbHelper.FetchAllFiles();
StartManagingCursor(filesCursor);
filesCursor.MoveToFirst();
IOConnectionInfo ioc = App.Kp2a.FileDbHelper.CursorToIoc(filesCursor);
if (App.Kp2a.GetFileStorage(ioc).RequiresSetup(ioc) == false)
{
IFileStorage fileStorage = App.Kp2a.GetFileStorage(ioc);
if (!fileStorage.RequiresCredentials(ioc))
{
//ok, we can use this file
_ioConnection = ioc;
}
}
}
if (_ioConnection == null)
{
//We need to go to FileSelectActivity first.
//For security reasons: discard the OTP (otherwise the user might not select a database now and forget
//about the OTP, but it would still be stored in the Intents and later be passed to PasswordActivity again.
Toast.MakeText(this, GetString(Resource.String.otp_discarded_because_no_db), ToastLength.Long).Show();
GoToFileSelectActivity();
Finish();
return false;
}
//user obviously wants to use OTP:
_keyFileOrProvider = KeyProviderIdOtp;
if (savedInstanceState == null) //only when not re-creating
{
//remember the OTP for later use
_pendingOtps.Add(i.GetStringExtra(Intents.OtpExtraKey));
i.RemoveExtra(Intents.OtpExtraKey);
}
return true;
}
private bool GetIocFromViewIntent(Intent i)
{
//started from "view" intent (e.g. from file browser)
_ioConnection.Path = i.DataString;
if (! _ioConnection.Path.Substring(0, 7).Equals("file://"))
{
//TODO: this might no longer be required as we can handle http(s) and ftp as well (but we need server credentials therefore)
Toast.MakeText(this, Resource.String.error_can_not_handle_uri, ToastLength.Long).Show();
Finish();
return false;
}
_ioConnection.Path = URLDecoder.Decode(_ioConnection.Path.Substring(7));
if (_ioConnection.Path.Length == 0)
{
// No file name
Toast.MakeText(this, Resource.String.FileNotFound, ToastLength.Long).Show();
Finish();
return false;
}
File dbFile = new File(_ioConnection.Path);
if (! dbFile.Exists())
{
// File does not exist
Toast.MakeText(this, Resource.String.FileNotFound, ToastLength.Long).Show();
Finish();
return false;
}
_keyFileOrProvider = GetKeyFile(_ioConnection.Path);
return true;
}
private void InitializeOkButton()
{
Button confirmButton = (Button) FindViewById(Resource.Id.pass_ok);
confirmButton.Click += (sender, e) =>
{
App.Kp2a.GetFileStorage(_ioConnection)
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, RequestCodePrepareDbFile, false);
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection,
RequestCodePrepareDbFile, false);
};
}
private void InitializeTogglePasswordButton()
{
ImageButton btnTogglePassword = (ImageButton) FindViewById(Resource.Id.toggle_password);
btnTogglePassword.Click += (sender, e) =>
{
_showPassword = !_showPassword;
MakePasswordMaskedOrVisible();
};
}
private void InitializeKeyfileBrowseButton()
{
ImageButton browse = (ImageButton) FindViewById(Resource.Id.browse_button);
browse.Click += (sender, evt) =>
{
string filename = null;
if (!String.IsNullOrEmpty(_ioConnection.Path))
{
File keyfile = new File(_ioConnection.Path);
File parent = keyfile.ParentFile;
if (parent != null)
{
filename = parent.AbsolutePath;
}
}
Util.ShowBrowseDialog(filename, this, Intents.RequestCodeFileBrowseForKeyfile, false);
};
}
private void InitializePasswordModeSpinner()
{
Spinner passwordModeSpinner = FindViewById<Spinner>(Resource.Id.password_mode_spinner);
if (passwordModeSpinner != null)
{
UpdateKeyProviderUiState();
passwordModeSpinner.SetSelection((int) KeyProviderType);
passwordModeSpinner.ItemSelected += (sender, args) =>
@ -466,16 +559,20 @@ namespace keepass2android
case 2:
_keyFileOrProvider = KeyProviderIdOtp;
break;
case 3:
_keyFileOrProvider = KeyProviderIdOtpRecovery;
break;
default:
throw new Exception("Unexpected position "+args.Position+" / " + ((ICursor)((AdapterView)sender).GetItemAtPosition(args.Position)).GetString(1));
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);
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection,
RequestCodePrepareOtpAuxFile, false);
};
}
else
@ -483,51 +580,35 @@ namespace keepass2android
//android 2.x
//TODO test
}
UpdateOkButtonState();
/*CheckBox checkBox = (CheckBox) FindViewById(Resource.Id.show_password);
// Show or hide password
checkBox.CheckedChange += delegate(object sender, CompoundButton.CheckedChangeEventArgs e) {
TextView password = (TextView) FindViewById(Resource.Id.password);
if ( e.IsChecked ) {
password.InputType = InputTypes.ClassText | InputTypes.TextVariationVisiblePassword;
} else {
password.InputType = InputTypes.ClassText | InputTypes.TextVariationPassword;
}
};
*/
ImageButton btnTogglePassword = (ImageButton)FindViewById(Resource.Id.toggle_password);
btnTogglePassword.Click += (sender, e) =>
private void RestoreState(Bundle savedInstanceState)
{
_showPassword = !_showPassword;
if (savedInstanceState != null)
{
_showPassword = savedInstanceState.GetBoolean(ShowpasswordKey, false);
MakePasswordMaskedOrVisible();
};
_keyFileOrProvider = FindViewById<EditText>(Resource.Id.pass_keyfile).Text = savedInstanceState.GetString(KeyFileOrProviderKey);
_password = FindViewById<EditText>(Resource.Id.password).Text = savedInstanceState.GetString(PasswordKey);
_pendingOtps = new List<string>(savedInstanceState.GetStringArrayList(PendingOtpsKey));
ImageButton browse = (ImageButton)FindViewById(Resource.Id.browse_button);
browse.Click += (sender, evt) =>
string otpInfoString = savedInstanceState.GetString(OtpInfoKey);
if (otpInfoString != null)
{
string filename = null;
if (!String.IsNullOrEmpty(_ioConnection.Path))
{
File keyfile = new File(_ioConnection.Path);
File parent = keyfile.ParentFile;
if (parent != null)
{
filename = parent.AbsolutePath;
XmlSerializer xs = new XmlSerializer(typeof(OtpInfo));
_otpInfo = (OtpInfo)xs.Deserialize(new StringReader(otpInfoString));
var enteredOtps = savedInstanceState.GetStringArrayList(EnteredOtpsKey);
ShowOtpEntry(enteredOtps);
}
UpdateKeyProviderUiState();
}
Util.ShowBrowseDialog(filename, this, Intents.RequestCodeFileBrowseForKeyfile, false);
};
RetrieveSettings();
}
private void UpdateOkButtonState()
@ -562,6 +643,9 @@ namespace keepass2android
FindViewById(Resource.Id.pass_ok).Enabled = enabled;
break;
case KeyProviders.OtpRecovery:
FindViewById(Resource.Id.pass_ok).Enabled = FindViewById<EditText>(Resource.Id.pass_otpsecret).Text != "" && _password != "";
break;
default:
throw new ArgumentOutOfRangeException();
}
@ -575,6 +659,10 @@ namespace keepass2android
FindViewById(Resource.Id.otpView).Visibility = KeyProviderType == KeyProviders.Otp
? ViewStates.Visible
: ViewStates.Gone;
FindViewById(Resource.Id.otpSecretLine).Visibility = KeyProviderType == KeyProviders.OtpRecovery
? ViewStates.Visible
: ViewStates.Gone;
if (KeyProviderType == KeyProviders.Otp)
{
FindViewById(Resource.Id.otps_pending).Visibility = _pendingOtps.Count > 0 ? ViewStates.Visible : ViewStates.Gone;
@ -597,7 +685,8 @@ namespace keepass2android
catch (Exception e)
{
Kp2aLog.Log(e.ToString());
throw new KeyFileException();
Toast.MakeText(this, App.Kp2a.GetResourceString(UiStringKey.keyfile_does_not_exist), ToastLength.Long).Show();
return;
}
}
else if (KeyProviderType == KeyProviders.Otp)
@ -605,26 +694,34 @@ namespace keepass2android
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);
}
var lOtps = GetOtpsFromUI();
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();
Toast.MakeText(this, GetString(Resource.String.OtpKeyError), ToastLength.Long).Show();
return;
}
compositeKey.AddUserKey(new KcpCustomKey(OathHotpKeyProv.Name, _otpInfo.Secret, true));
}
else if (KeyProviderType == KeyProviders.OtpRecovery)
{
Spinner stpDataFmtSpinner = FindViewById<Spinner>(Resource.Id.otpsecret_format_spinner);
EditText secretEdit = FindViewById<EditText>(Resource.Id.pass_otpsecret);
byte[] pbSecret = EncodingUtil.ParseKey(secretEdit.Text, (OtpDataFmt)stpDataFmtSpinner.SelectedItemPosition);
if (pbSecret != null)
{
compositeKey.AddUserKey(new KcpCustomKey(OathHotpKeyProv.Name, pbSecret, true));
}
else
{
Toast.MakeText(this, Resource.String.CouldntParseOtpSecret, ToastLength.Long).Show();
return;
}
}
CheckBox cbQuickUnlock = (CheckBox) FindViewById(Resource.Id.enable_quickunlock);
App.Kp2a.SetQuickUnlockEnabled(cbQuickUnlock.Checked);
@ -642,6 +739,18 @@ namespace keepass2android
new ProgressTask(App.Kp2a, this, task).Run();
}
private List<string> GetOtpsFromUI()
{
List<string> lOtps = new List<string>();
foreach (int otpId in _otpTextViewIds)
{
string otpText = FindViewById<EditText>(otpId).Text;
if (!String.IsNullOrEmpty(otpText))
lOtps.Add(otpText);
}
return lOtps;
}
private void CreateOtpSecret(List<string> lOtps)
{
byte[] pbSecret;
@ -785,13 +894,32 @@ namespace keepass2android
base.OnSaveInstanceState(outState);
AppTask.ToBundle(outState);
outState.PutBoolean(ShowpasswordKey, _showPassword);
//TODO:
// * save OTP state
outState.PutString(KeyFileOrProviderKey, _keyFileOrProvider);
outState.PutString(PasswordKey, _password);
outState.PutStringArrayList(PendingOtpsKey, _pendingOtps);
if (_otpInfo != null)
{
outState.PutStringArrayList(EnteredOtpsKey, GetOtpsFromUI());
var sw = new StringWriter();
var xws = OtpInfo.XmlWriterSettings();
XmlWriter xw = XmlWriter.Create(sw, xws);
XmlSerializer xs = new XmlSerializer(typeof(OtpInfo));
xs.Serialize(xw, _otpInfo);
xw.Close();
outState.PutString(OtpInfoKey, sw.ToString());
}
//more OTP TODO:
// * NfcOtp: Ask for close when db open
// * Caching of aux file
// * -> implement IFileStorage in JavaFileStorage based on ListFiles
// * -> Sync
}
protected override void OnNewIntent(Intent intent)
@ -896,7 +1024,7 @@ namespace keepass2android
}
}
private void RetrieveSettings() {
private void InitializeQuickUnlockCheckbox() {
CheckBox cbQuickUnlock = (CheckBox)FindViewById(Resource.Id.enable_quickunlock);
cbQuickUnlock.Checked = _prefs.GetBoolean(GetString(Resource.String.QuickUnlockDefaultEnabled_key), true);
}
@ -909,7 +1037,7 @@ namespace keepass2android
}
}
private void PopulateView() {
private void InitializeFilenameView() {
SetEditText(Resource.Id.filename, App.Kp2a.GetFileStorage(_ioConnection).GetDisplayName(_ioConnection));
if (App.Kp2a.FileDbHelper.NumberOfRecentFiles() < 2)
{
@ -919,8 +1047,7 @@ namespace keepass2android
{
FindViewById(Resource.Id.filename_group).Visibility = ViewStates.Visible;
}
if (KeyProviderType == KeyProviders.KeyFile)
SetEditText(Resource.Id.pass_keyfile, _keyFileOrProvider);
}
protected override void OnDestroy()
@ -937,15 +1064,6 @@ namespace keepass2android
}
*/
private void ErrorMessage(int resId)
{
Toast.MakeText(this, resId, ToastLength.Long).Show();
}
private String GetEditText(int resId) {
return Util.GetEditText(this, resId);
}
private void SetEditText(int resId, String str) {
TextView te = (TextView) FindViewById(resId);
//assert(te == null);
@ -1002,13 +1120,40 @@ namespace keepass2android
public override void Run() {
if (_act.KeyProviderType == KeyProviders.Otp)
{
try
{
StatusLogger.UpdateMessage(UiStringKey.SavingOtpAuxFile);
if (!OathHotpKeyProv.CreateAuxFile(_act._otpInfo, new KeyProviderQueryContext(_act._ioConnection, false, false)))
Toast.MakeText(_act, _act.GetString(Resource.String.ErrorUpdatingOtpAuxFile), ToastLength.Long).Show();
}
catch (Exception e)
{
Kp2aLog.Log(e.Message);
Toast.MakeText(_act, _act.GetString(Resource.String.ErrorUpdatingOtpAuxFile)+" "+e.Message, ToastLength.Long).Show();
}
}
if ( Success )
{
_act.SetEditText(Resource.Id.password, "");
_act.SetEditText(Resource.Id.pass_otpsecret, "");
foreach (int otpId in _act._otpTextViewIds)
{
_act.SetEditText(otpId, "");
}
_act.LaunchNextActivity();
GC.Collect(); // Ensure temporary memory used while loading is collected - it will contain sensitive data such as username and password, and also the large data of the encrypted database file
if ((_act.KeyProviderType == KeyProviders.Otp) || (_act.KeyProviderType == KeyProviders.OtpRecovery))
App.Kp2a.GetDb().OtpAuxFileIoc = OathHotpKeyProv.GetAuxFileIoc(_act._ioConnection);
GC.Collect(); // Ensure temporary memory used while loading is collected
}
else
{

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="12dip"
android:layout_marginRight="12dip"
android:layout_marginBottom="12dip"
@ -51,7 +56,7 @@
android:id="@+id/password_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="" />
android:text="@string/master_key_type" />
<Spinner
android:id="@+id/password_mode_spinner"
android:layout_width="fill_parent"
@ -108,6 +113,7 @@
android:id="@+id/otpInitView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<Button
android:id="@+id/init_otp"
@ -169,6 +175,24 @@
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/otpSecretLine"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/pass_otpsecret"
android:layout_width="0px"
android:layout_height="wrap_content"
android:singleLine="true"
android:layout_weight="1"
android:hint="@string/otpsecret_hint" />
<Spinner
android:id="@+id/otpsecret_format_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
<Button
android:id="@+id/pass_ok"
@ -186,3 +210,4 @@
android:layout_height="wrap_content"
android:text="@string/enable_quickunlock" />
</LinearLayout>
</ScrollView>

View File

@ -143,6 +143,7 @@
<string name="omitbackup_summary">Omit \'Backup\' and Recycle Bin group from search results</string>
<string name="pass_filename">KeePass database filename</string>
<string name="password_title">Enter database password</string>
<string name="master_key_type">Select master key type:</string>
<string name="progress_create">Creating new database…</string>
<string name="create_database">Create database</string>
<string name="progress_title">Working…</string>
@ -299,9 +300,17 @@
<string name="UpdatedRemoteFileOnLoad">Updated remote file.</string>
<string name="NotifyOpenFromLocalDueToConflict">Opened local file due to conflict with changes in remote file. Use Synchronize menu to merge.</string>
<string name="LoadedFromRemoteInSync">Remote file and cache are synchronized.</string>
<string name="UpdatedCachedFileOnLoad">Updated local cache copy of database.</string>
<string name="UpdatedCachedFileOnLoad">Updated local cache copy of %1$s.</string>
<string name="RemoteDatabaseUnchanged">No changes detected.</string>
<string name="ResolvedCacheConflictByUsingRemoteOtpAux">Updated cached OTP auxiliary file: Remote counter was higher.</string>
<string name="ResolvedCacheConflictByUsingLocalOtpAux">Updated remote OTP auxiliary file: Local counter was higher.</string>
<string name="SynchronizingOtpAuxFile">Synchronizing OTP auxiliary file…</string>
<string name="database_file">database file</string>
<string name="otp_aux_file">OTP auxiliary file</string>
<string name="ErrorOcurred">An error occured:</string>
<string name="synchronize_database_menu">Synchronize database…</string>
@ -343,14 +352,19 @@
<string name="error_adding_keyfile">Error while adding the keyfile!</string>
<string name="init_otp">Enter OTPs</string>
<string name="init_otp">Load OTP auxiliary file</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="otp_discarded_because_no_db">Please select database first. OTP is discarded for security reasons.</string>
<string name="otp_discarded_no_space">OTP discarded: All OTPs already entered!</string>
<string name="otp_discarded_because_db_open">Please close database first. OTP is discarded.</string>
<string name="otps_pending">(One or more OTPs already available)</string>
<string name="otpsecret_hint">OTP secret (e.g. 01 23 ab cd…)</string>
<string name="CouldntParseOtpSecret">Error parsing OTP secret!</string>
<string name="OtpKeyError">Failed to create OTP key! Make sure you have entered the correct OTPs.</string>
<string name="ErrorUpdatingOtpAuxFile">Error updating OTP auxiliary file!</string>
<string name="SavingOtpAuxFile">Saving auxiliary OTP file…</string>
<string name="loading">Loading…</string>
@ -477,5 +491,6 @@ Initial public release
<item>Password only</item>
<item>Password + Key file</item>
<item>Password + OTP</item>
<item>Password + OTP secret (recovery mode)</item>
</string-array>
</resources>

View File

@ -49,13 +49,19 @@ namespace OtpKeyProv
private static IOConnectionInfo GetAuxFileIoc(KeyProviderQueryContext ctx)
{
IOConnectionInfo ioc = ctx.DatabaseIOInfo.CloneDeep();
IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(ioc);
IOConnectionInfo iocAux = fileStorage.GetFilePath(fileStorage.GetParentPath(ioc),
fileStorage.GetFilenameWithoutPathAndExt(ioc) + AuxFileExt);
var iocAux = GetAuxFileIoc(ioc);
return iocAux;
}
public static IOConnectionInfo GetAuxFileIoc(IOConnectionInfo databaseIoc)
{
IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(databaseIoc);
IOConnectionInfo iocAux = fileStorage.GetFilePath(fileStorage.GetParentPath(databaseIoc),
fileStorage.GetFilenameWithoutPathAndExt(databaseIoc) + AuxFileExt);
return iocAux;
}
public static OtpInfo LoadOtpInfo(KeyProviderQueryContext ctx)
{
return OtpInfo.Load(GetAuxFileIoc(ctx));
@ -130,7 +136,7 @@ namespace OtpKeyProv
}
private static bool CreateAuxFile(OtpInfo otpInfo,
public static bool CreateAuxFile(OtpInfo otpInfo,
KeyProviderQueryContext ctx)
{
otpInfo.Type = ProvType;

View File

@ -0,0 +1,70 @@
using System.IO;
using System.Xml.Serialization;
using KeePassLib.Serialization;
using OtpKeyProv;
using keepass2android.Io;
namespace keepass2android.addons.OtpKeyProv
{
/// <summary>
/// Class which provides caching for OtpInfo-files. This is an extension to CachingFileStorage required to handle conflicts directly when loading.
/// </summary>
class OtpAuxCachingFileStorage: CachingFileStorage
{
private readonly IOtpAuxCacheSupervisor _cacheSupervisor;
internal interface IOtpAuxCacheSupervisor: ICacheSupervisor
{
/// <summary>
/// called when there was a conflict which was resolved by using the remote file.
/// </summary>
void ResolvedCacheConflictByUsingRemote(IOConnectionInfo ioc);
/// <summary>
/// called when there was a conflict which was resolved by using the local file.
/// </summary>
void ResolvedCacheConflictByUsingLocal(IOConnectionInfo ioc);
}
public OtpAuxCachingFileStorage(IFileStorage cachedStorage, string cacheDir, IOtpAuxCacheSupervisor cacheSupervisor)
: base(cachedStorage, cacheDir, cacheSupervisor)
{
_cacheSupervisor = cacheSupervisor;
}
protected override Stream OpenFileForReadWithConflict(IOConnectionInfo ioc, string cachedFilePath)
{
OtpInfo remoteOtpInfo, localOtpInfo;
//load both files
XmlSerializer xs = new XmlSerializer(typeof (OtpInfo));
localOtpInfo = (OtpInfo) xs.Deserialize(File.OpenRead(cachedFilePath));
using (Stream remoteStream = _cachedStorage.OpenFileForRead(ioc))
{
remoteOtpInfo = (OtpInfo) xs.Deserialize(remoteStream);
}
//see which OtpInfo has the bigger Counter value and use this one:
if (localOtpInfo.Counter > remoteOtpInfo.Counter)
{
//overwrite the remote file
UpdateRemoteFile(File.OpenRead(cachedFilePath),
ioc,
App.Kp2a.GetBooleanPreference(PreferenceKey.UseFileTransactions),
GetBaseVersionHash(ioc)
);
_cacheSupervisor.ResolvedCacheConflictByUsingRemote(ioc);
}
else
{
//overwrite the local file:
UpdateCacheFromRemote(ioc, cachedFilePath);
_cacheSupervisor.ResolvedCacheConflictByUsingLocal(ioc);
}
//now return the local file in any way:
return File.OpenRead(cachedFilePath);
}
}
}

View File

@ -197,18 +197,8 @@ namespace OtpKeyProv
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";
XmlWriter xw = XmlWriter.Create(trans.OpenFile(), xws);
XmlSerializer xs = new XmlSerializer(typeof (OtpInfo));
xs.Serialize(xw, otpInfo);
xw.Close();
var stream = trans.OpenFile();
WriteToStream(otpInfo, stream);
trans.CommitWrite();
}
return true;
@ -222,6 +212,31 @@ namespace OtpKeyProv
return false;
}
public static void WriteToStream(OtpInfo otpInfo, Stream stream)
{
var xws = XmlWriterSettings();
XmlWriter xw = XmlWriter.Create(stream, xws);
XmlSerializer xs = new XmlSerializer(typeof (OtpInfo));
xs.Serialize(xw, otpInfo);
xw.Close();
}
public static XmlWriterSettings XmlWriterSettings()
{
XmlWriterSettings xws = new XmlWriterSettings
{
CloseOutput = true,
Encoding = StrUtil.Utf8,
Indent = true,
IndentChars = "\t"
};
return xws;
}
public void EncryptSecret()
{
if(m_pbSecret == null) throw new InvalidOperationException();

View File

@ -32,6 +32,7 @@ using Android.Preferences;
using TwofishCipher;
#endif
using keepass2android.Io;
using keepass2android.addons.OtpKeyProv;
namespace keepass2android
{
@ -294,8 +295,11 @@ namespace keepass2android
builder.SetNegativeButton(GetResourceString(noString), noHandler);
if (cancelHandler != null)
{
builder.SetNeutralButton(ctx.GetString(Android.Resource.String.Cancel),
cancelHandler);
}
Dialog dialog = builder.Create();
dialog.Show();
@ -447,7 +451,7 @@ namespace keepass2android
return _db;
}
void ShowToast(string message)
internal void ShowToast(string message)
{
var handler = new Handler(Looper.MainLooper);
handler.Post(() => { Toast.MakeText(Application.Context, message, ToastLength.Long).Show(); });
@ -466,7 +470,8 @@ namespace keepass2android
public void UpdatedCachedFileOnLoad(IOConnectionInfo ioc)
{
ShowToast(Application.Context.GetString(Resource.String.UpdatedCachedFileOnLoad));
ShowToast(Application.Context.GetString(Resource.String.UpdatedCachedFileOnLoad,
new Java.Lang.Object[] { Application.Context.GetString(Resource.String.database_file) }));
}
public void UpdatedRemoteFileOnLoad(IOConnectionInfo ioc)
@ -510,7 +515,7 @@ namespace keepass2android
if (DatabaseCacheEnabled)
{
return new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this);
return new OtpAuxCachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, new OtpAuxCacheSupervisor(this));
}
else
{

View File

@ -0,0 +1,59 @@
using System;
using Android.App;
using KeePassLib.Serialization;
using keepass2android.addons.OtpKeyProv;
namespace keepass2android
{
public class OtpAuxCacheSupervisor : OtpAuxCachingFileStorage.IOtpAuxCacheSupervisor
{
private readonly Kp2aApp _app;
public OtpAuxCacheSupervisor(Kp2aApp app)
{
_app = app;
}
public void CouldntSaveToRemote(IOConnectionInfo ioc, Exception ex)
{
_app.CouldntSaveToRemote(ioc, ex);
}
public void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex)
{
_app.CouldntOpenFromRemote(ioc, ex);
}
public void UpdatedCachedFileOnLoad(IOConnectionInfo ioc)
{
_app.ShowToast(Application.Context.GetString(Resource.String.UpdatedCachedFileOnLoad,
new Java.Lang.Object[] { Application.Context.GetString(Resource.String.otp_aux_file) }));
}
public void UpdatedRemoteFileOnLoad(IOConnectionInfo ioc)
{
_app.UpdatedRemoteFileOnLoad(ioc);
}
public void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc)
{
//must not be called . Conflicts should be resolved.
throw new InvalidOperationException();
}
public void LoadedFromRemoteInSync(IOConnectionInfo ioc)
{
_app.LoadedFromRemoteInSync(ioc);
}
public void ResolvedCacheConflictByUsingRemote(IOConnectionInfo ioc)
{
_app.ShowToast(Application.Context.GetString(Resource.String.ResolvedCacheConflictByUsingRemoteOtpAux));
}
public void ResolvedCacheConflictByUsingLocal(IOConnectionInfo ioc)
{
_app.ShowToast(Application.Context.GetString(Resource.String.ResolvedCacheConflictByUsingLocalOtpAux));
}
}
}

View File

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

View File

@ -84,9 +84,11 @@
<ItemGroup>
<Compile Include="addons\OtpKeyProv\EncodingUtil.cs" />
<Compile Include="addons\OtpKeyProv\OathHotpKeyProv.cs" />
<Compile Include="addons\OtpKeyProv\OtpAuxCachingFileStorage.cs" />
<Compile Include="addons\OtpKeyProv\OtpInfo.cs" />
<Compile Include="addons\OtpKeyProv\OtpUtil.cs" />
<Compile Include="app\NoFileStorageFoundException.cs" />
<Compile Include="app\OtpAuxCacheSupervisor.cs" />
<Compile Include="CreateDatabaseActivity.cs" />
<Compile Include="fileselect\FileChooserFileProvider.cs" />
<Compile Include="fileselect\FileStorageSetupActivity.cs" />