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> /// </summary>
public class CachingFileStorage: IFileStorage public class CachingFileStorage: IFileStorage
{ {
private readonly IFileStorage _cachedStorage; protected readonly IFileStorage _cachedStorage;
private readonly ICacheSupervisor _cacheSupervisor; private readonly ICacheSupervisor _cacheSupervisor;
private readonly string _streamCacheDir; private readonly string _streamCacheDir;
@ -179,14 +179,21 @@ namespace keepass2android.Io
{ {
if (TryUpdateRemoteFile(localData, ioc, true, hash)) if (TryUpdateRemoteFile(localData, ioc, true, hash))
_cacheSupervisor.UpdatedRemoteFileOnLoad(ioc); _cacheSupervisor.UpdatedRemoteFileOnLoad(ioc);
return File.OpenRead(cachedFilePath);
} }
} }
else else
{ {
//conflict: both files changed. //conflict: both files changed.
//signal that we're loading from local return OpenFileForReadWithConflict(ioc, cachedFilePath);
_cacheSupervisor.NotifyOpenFromLocalDueToConflict(ioc);
} }
}
protected virtual Stream OpenFileForReadWithConflict(IOConnectionInfo ioc, string cachedFilePath)
{
//signal that we're loading from local
_cacheSupervisor.NotifyOpenFromLocalDueToConflict(ioc);
return File.OpenRead(cachedFilePath); return File.OpenRead(cachedFilePath);
} }
@ -214,29 +221,17 @@ namespace keepass2android.Io
private Stream OpenFileForReadWhenNoLocalChanges(IOConnectionInfo ioc, string cachedFilePath) private Stream OpenFileForReadWhenNoLocalChanges(IOConnectionInfo ioc, string cachedFilePath)
{ {
//open stream:
using (Stream file = _cachedStorage.OpenFileForRead(ioc)) //remember current hash
{ string previousHash = null;
string baseVersionFilePath = BaseVersionFilePath(ioc);
if (File.Exists(baseVersionFilePath))
previousHash = File.ReadAllText(baseVersionFilePath);
//copy to cache: //copy to cache:
//note: we might use the file version to check if it's already in the cache and if copying is required. var fileHash = UpdateCacheFromRemote(ioc, cachedFilePath);
//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;
string baseVersionFilePath = BaseVersionFilePath(ioc);
if (File.Exists(baseVersionFilePath))
previousHash = File.ReadAllText(baseVersionFilePath);
//save hash in cache files:
File.WriteAllText(VersionFilePath(ioc), fileHash);
File.WriteAllText(baseVersionFilePath, fileHash);
//notify supervisor what we did: //notify supervisor what we did:
if (previousHash != fileHash) if (previousHash != fileHash)
@ -245,8 +240,35 @@ namespace keepass2android.Io
_cacheSupervisor.LoadedFromRemoteInSync(ioc); _cacheSupervisor.LoadedFromRemoteInSync(ioc);
return File.OpenRead(cachedFilePath); 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) 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: //try to write to remote:
using ( using (

View File

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

View File

@ -44,6 +44,13 @@ namespace keepass2android
return KpDatabase == null ? null : KpDatabase.IOConnectionInfo; 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 string LastFileVersion;
public SearchDbHelper SearchHelper; public SearchDbHelper SearchHelper;
@ -192,6 +199,7 @@ namespace keepass2android
KpDatabase = null; KpDatabase = null;
_loaded = false; _loaded = false;
_reloadRequested = false; _reloadRequested = false;
OtpAuxFileIoc = null;
} }
public void MarkAllGroupsAsDirty() { public void MarkAllGroupsAsDirty() {

View File

@ -25,6 +25,7 @@ using Android.Widget;
using KeePassLib; using KeePassLib;
using Android.Preferences; using Android.Preferences;
using KeePassLib.Interfaces; using KeePassLib.Interfaces;
using KeePassLib.Serialization;
using KeePassLib.Utility; using KeePassLib.Utility;
using keepass2android.Io; using keepass2android.Io;
using keepass2android.database.edit; using keepass2android.database.edit;
@ -367,6 +368,37 @@ namespace keepass2android
return base.OnOptionsItemSelected(item); 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() private void Synchronize()
{ {
var filestorage = App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc); var filestorage = App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc);

View File

@ -3,6 +3,7 @@ using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.OS; using Android.OS;
using Android.Widget;
using Java.Util.Regex; using Java.Util.Regex;
namespace keepass2android namespace keepass2android
@ -54,8 +55,30 @@ namespace keepass2android
i.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop); i.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
i.PutExtra(Intents.OtpExtraKey, GetOtpFromIntent(Intent)); try
StartActivity(i); {
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(); Finish();
} }

View File

@ -17,7 +17,10 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Database; using Android.Database;
@ -27,15 +30,17 @@ using Android.Views;
using Android.Widget; using Android.Widget;
using Java.Net; using Java.Net;
using Android.Preferences; using Android.Preferences;
using Java.IO;
using Android.Text; using Android.Text;
using Android.Content.PM; using Android.Content.PM;
using KeePassLib.Keys; using KeePassLib.Keys;
using KeePassLib.Serialization; using KeePassLib.Serialization;
using KeePassLib.Utility;
using OtpKeyProv; using OtpKeyProv;
using keepass2android.Io; using keepass2android.Io;
using keepass2android.Utils; using keepass2android.Utils;
using Exception = System.Exception; using Exception = System.Exception;
using File = Java.IO.File;
using FileNotFoundException = Java.IO.FileNotFoundException;
using MemoryStream = System.IO.MemoryStream; using MemoryStream = System.IO.MemoryStream;
using Object = Java.Lang.Object; using Object = Java.Lang.Object;
using Process = Android.OS.Process; using Process = Android.OS.Process;
@ -55,11 +60,10 @@ namespace keepass2android
//int values correspond to indices in passwordSpinner //int values correspond to indices in passwordSpinner
None = 0, None = 0,
KeyFile = 1, KeyFile = 1,
Otp = 2 Otp = 2,
OtpRecovery = 3
} }
bool _showPassword;
public const String KeyDefaultFilename = "defaultFileName"; public const String KeyDefaultFilename = "defaultFileName";
public const String KeyFilename = "fileName"; public const String KeyFilename = "fileName";
@ -71,19 +75,23 @@ namespace keepass2android
private const String ViewIntent = "android.intent.action.VIEW"; private const String ViewIntent = "android.intent.action.VIEW";
private const string ShowpasswordKey = "ShowPassword"; private const string ShowpasswordKey = "ShowPassword";
private const string KeyProviderIdOtp = "KP2A-OTP"; 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 Task<MemoryStream> _loadDbTask;
private IOConnectionInfo _ioConnection; private IOConnectionInfo _ioConnection;
private String _keyFileOrProvider; private String _keyFileOrProvider;
bool _showPassword;
internal AppTask AppTask; internal AppTask AppTask;
private bool _killOnDestroy; private bool _killOnDestroy;
private string _password = ""; private string _password = "";
//OTPs which should be entered into the OTP fields as soon as these become visible //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 KeyProviders KeyProviderType
@ -94,6 +102,8 @@ namespace keepass2android
return KeyProviders.None; return KeyProviders.None;
if (_keyFileOrProvider == KeyProviderIdOtp) if (_keyFileOrProvider == KeyProviderIdOtp)
return KeyProviders.Otp; return KeyProviders.Otp;
if (_keyFileOrProvider == KeyProviderIdOtpRecovery)
return KeyProviders.OtpRecovery;
return KeyProviders.KeyFile; return KeyProviders.KeyFile;
} }
} }
@ -103,7 +113,12 @@ namespace keepass2android
private bool _starting; private bool _starting;
private OtpInfo _otpInfo; 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) public PasswordActivity (IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer) : base(javaReference, transfer)
@ -217,7 +232,7 @@ namespace keepass2android
KcpKeyFile kcpKeyfile = (KcpKeyFile)App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey(typeof(KcpKeyFile)); KcpKeyFile kcpKeyfile = (KcpKeyFile)App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey(typeof(KcpKeyFile));
SetEditText(Resource.Id.pass_keyfile, kcpKeyfile.Path); SetEditText(Resource.Id.pass_keyfile, kcpKeyfile.Path);
_keyFileOrProvider = kcpKeyfile.Path;
} }
} }
App.Kp2a.LockDatabase(false); App.Kp2a.LockDatabase(false);
@ -234,7 +249,7 @@ namespace keepass2android
EditText fn = (EditText) FindViewById(Resource.Id.pass_keyfile); EditText fn = (EditText) FindViewById(Resource.Id.pass_keyfile);
fn.Text = filename; fn.Text = filename;
_keyFileOrProvider = filename;
} }
} }
break; break;
@ -265,51 +280,54 @@ namespace keepass2android
Toast.MakeText(this, GetString(Resource.String.CouldntLoadOtpAuxFile), ToastLength.Long).Show(); Toast.MakeText(this, GetString(Resource.String.CouldntLoadOtpAuxFile), ToastLength.Long).Show();
return; return;
} }
FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone;
FindViewById(Resource.Id.otpEntry).Visibility = ViewStates.Visible;
int c = 0;
foreach (int otpId in _otpTextViewIds) IList<string> prefilledOtps = _pendingOtps;
{ ShowOtpEntry(prefilledOtps);
c++;
var otpTextView = FindViewById<EditText>(otpId);
if (c <= _pendingOtps.Count)
{
otpTextView.Text = _pendingOtps[c-1];
}
else
{
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();
};
}
}
_pendingOtps.Clear(); _pendingOtps.Clear();
} }
).Execute(); ).Execute();
} }
private void ShowOtpEntry(IList<string> prefilledOtps)
{
FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone;
FindViewById(Resource.Id.otpEntry).Visibility = ViewStates.Visible;
int c = 0;
foreach (int otpId in _otpTextViewIds)
{
c++;
var otpTextView = FindViewById<EditText>(otpId);
if (c <= prefilledOtps.Count)
{
otpTextView.Text = prefilledOtps[c - 1];
}
else
{
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(); };
}
}
}
protected override void OnCreate(Bundle savedInstanceState) protected override void OnCreate(Bundle savedInstanceState)
{ {
base.OnCreate(savedInstanceState); base.OnCreate(savedInstanceState);
if (savedInstanceState != null)
_showPassword = savedInstanceState.GetBoolean(ShowpasswordKey, false); Intent i = Intent;
AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent); AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent);
Intent i = Intent;
String action = i.Action; String action = i.Action;
_prefs = PreferenceManager.GetDefaultSharedPreferences(this); _prefs = PreferenceManager.GetDefaultSharedPreferences(this);
@ -320,84 +338,11 @@ namespace keepass2android
if (action != null && action.Equals(ViewIntent)) if (action != null && action.Equals(ViewIntent))
{ {
//started from "view" intent (e.g. from file browser) if (!GetIocFromViewIntent(i)) return;
_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);
} }
else if ((action != null) && (action.Equals(Intents.StartWithOtp))) else if ((action != null) && (action.Equals(Intents.StartWithOtp)))
{ {
//create called after detecting an OTP via NFC if (!GetIocFromOtpIntent(savedInstanceState, i)) return;
//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);
} }
else else
{ {
@ -419,16 +364,17 @@ namespace keepass2android
SetContentView(Resource.Layout.password); 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 += FindViewById<EditText>(Resource.Id.pass_keyfile).TextChanged +=
(sender, args) => (sender, args) =>
{ {
_keyFileOrProvider = FindViewById<EditText>(Resource.Id.pass_keyfile).Text; _keyFileOrProvider = FindViewById<EditText>(Resource.Id.pass_keyfile).Text;
UpdateOkButtonState(); UpdateOkButtonState();
}; };
FindViewById<EditText>(Resource.Id.password).TextChanged += FindViewById<EditText>(Resource.Id.password).TextChanged +=
(sender, args) => (sender, args) =>
@ -437,20 +383,167 @@ namespace keepass2android
UpdateOkButtonState(); UpdateOkButtonState();
}; };
FindViewById<EditText>(Resource.Id.pass_otpsecret).TextChanged += (sender, args) => UpdateOkButtonState();
EditText passwordEdit = FindViewById<EditText>(Resource.Id.password);
passwordEdit.RequestFocus(); passwordEdit.RequestFocus();
Window.SetSoftInputMode(SoftInput.StateVisible); Window.SetSoftInputMode(SoftInput.StateVisible);
Button confirmButton = (Button)FindViewById(Resource.Id.pass_ok); 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) => confirmButton.Click += (sender, e) =>
{ {
App.Kp2a.GetFileStorage(_ioConnection) 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); Spinner passwordModeSpinner = FindViewById<Spinner>(Resource.Id.password_mode_spinner);
if (passwordModeSpinner != null) if (passwordModeSpinner != null)
{ {
UpdateKeyProviderUiState(); UpdateKeyProviderUiState();
passwordModeSpinner.SetSelection((int) KeyProviderType); passwordModeSpinner.SetSelection((int) KeyProviderType);
passwordModeSpinner.ItemSelected += (sender, args) => passwordModeSpinner.ItemSelected += (sender, args) =>
@ -466,16 +559,20 @@ namespace keepass2android
case 2: case 2:
_keyFileOrProvider = KeyProviderIdOtp; _keyFileOrProvider = KeyProviderIdOtp;
break; break;
case 3:
_keyFileOrProvider = KeyProviderIdOtpRecovery;
break;
default: 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(); UpdateKeyProviderUiState();
}; };
FindViewById(Resource.Id.init_otp).Click += (sender, args) => FindViewById(Resource.Id.init_otp).Click += (sender, args) =>
{ {
App.Kp2a.GetOtpAuxFileStorage(_ioConnection) App.Kp2a.GetOtpAuxFileStorage(_ioConnection)
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, RequestCodePrepareOtpAuxFile, false); .PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection,
RequestCodePrepareOtpAuxFile, false);
}; };
} }
else else
@ -483,51 +580,35 @@ namespace keepass2android
//android 2.x //android 2.x
//TODO test //TODO test
} }
}
private void RestoreState(Bundle savedInstanceState)
UpdateOkButtonState(); {
if (savedInstanceState != null)
/*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) =>
{
_showPassword = !_showPassword;
MakePasswordMaskedOrVisible();
};
ImageButton browse = (ImageButton)FindViewById(Resource.Id.browse_button);
browse.Click += (sender, evt) =>
{ {
string filename = null; _showPassword = savedInstanceState.GetBoolean(ShowpasswordKey, false);
if (!String.IsNullOrEmpty(_ioConnection.Path)) 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));
string otpInfoString = savedInstanceState.GetString(OtpInfoKey);
if (otpInfoString != null)
{ {
File keyfile = new File(_ioConnection.Path);
File parent = keyfile.ParentFile; XmlSerializer xs = new XmlSerializer(typeof(OtpInfo));
if (parent != null) _otpInfo = (OtpInfo)xs.Deserialize(new StringReader(otpInfoString));
{
filename = parent.AbsolutePath; var enteredOtps = savedInstanceState.GetStringArrayList(EnteredOtpsKey);
}
ShowOtpEntry(enteredOtps);
} }
Util.ShowBrowseDialog(filename, this, Intents.RequestCodeFileBrowseForKeyfile, false);
}; UpdateKeyProviderUiState();
RetrieveSettings(); }
} }
private void UpdateOkButtonState() private void UpdateOkButtonState()
@ -562,6 +643,9 @@ namespace keepass2android
FindViewById(Resource.Id.pass_ok).Enabled = enabled; FindViewById(Resource.Id.pass_ok).Enabled = enabled;
break; break;
case KeyProviders.OtpRecovery:
FindViewById(Resource.Id.pass_ok).Enabled = FindViewById<EditText>(Resource.Id.pass_otpsecret).Text != "" && _password != "";
break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
} }
@ -575,6 +659,10 @@ namespace keepass2android
FindViewById(Resource.Id.otpView).Visibility = KeyProviderType == KeyProviders.Otp FindViewById(Resource.Id.otpView).Visibility = KeyProviderType == KeyProviders.Otp
? ViewStates.Visible ? ViewStates.Visible
: ViewStates.Gone; : ViewStates.Gone;
FindViewById(Resource.Id.otpSecretLine).Visibility = KeyProviderType == KeyProviders.OtpRecovery
? ViewStates.Visible
: ViewStates.Gone;
if (KeyProviderType == KeyProviders.Otp) if (KeyProviderType == KeyProviders.Otp)
{ {
FindViewById(Resource.Id.otps_pending).Visibility = _pendingOtps.Count > 0 ? ViewStates.Visible : ViewStates.Gone; FindViewById(Resource.Id.otps_pending).Visibility = _pendingOtps.Count > 0 ? ViewStates.Visible : ViewStates.Gone;
@ -597,7 +685,8 @@ namespace keepass2android
catch (Exception e) catch (Exception e)
{ {
Kp2aLog.Log(e.ToString()); 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) else if (KeyProviderType == KeyProviders.Otp)
@ -605,26 +694,34 @@ namespace keepass2android
try try
{ {
List<string> lOtps = new List<string>(); var lOtps = GetOtpsFromUI();
foreach (int otpId in _otpTextViewIds)
{
string otpText = FindViewById<EditText>(otpId).Text;
if (!String.IsNullOrEmpty(otpText))
lOtps.Add(otpText);
}
CreateOtpSecret(lOtps); CreateOtpSecret(lOtps);
} }
catch (Exception) 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; return;
} }
compositeKey.AddUserKey(new KcpCustomKey(OathHotpKeyProv.Name, _otpInfo.Secret, true)); 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); CheckBox cbQuickUnlock = (CheckBox) FindViewById(Resource.Id.enable_quickunlock);
App.Kp2a.SetQuickUnlockEnabled(cbQuickUnlock.Checked); App.Kp2a.SetQuickUnlockEnabled(cbQuickUnlock.Checked);
@ -642,6 +739,18 @@ namespace keepass2android
new ProgressTask(App.Kp2a, this, task).Run(); 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) private void CreateOtpSecret(List<string> lOtps)
{ {
byte[] pbSecret; byte[] pbSecret;
@ -785,13 +894,32 @@ namespace keepass2android
base.OnSaveInstanceState(outState); base.OnSaveInstanceState(outState);
AppTask.ToBundle(outState); AppTask.ToBundle(outState);
outState.PutBoolean(ShowpasswordKey, _showPassword); 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: //more OTP TODO:
// * NfcOtp: Ask for close when db open
// * Caching of aux file // * Caching of aux file
// * -> implement IFileStorage in JavaFileStorage based on ListFiles // * -> implement IFileStorage in JavaFileStorage based on ListFiles
// * -> Sync
} }
protected override void OnNewIntent(Intent intent) 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); CheckBox cbQuickUnlock = (CheckBox)FindViewById(Resource.Id.enable_quickunlock);
cbQuickUnlock.Checked = _prefs.GetBoolean(GetString(Resource.String.QuickUnlockDefaultEnabled_key), true); 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)); SetEditText(Resource.Id.filename, App.Kp2a.GetFileStorage(_ioConnection).GetDisplayName(_ioConnection));
if (App.Kp2a.FileDbHelper.NumberOfRecentFiles() < 2) if (App.Kp2a.FileDbHelper.NumberOfRecentFiles() < 2)
{ {
@ -919,8 +1047,7 @@ namespace keepass2android
{ {
FindViewById(Resource.Id.filename_group).Visibility = ViewStates.Visible; FindViewById(Resource.Id.filename_group).Visibility = ViewStates.Visible;
} }
if (KeyProviderType == KeyProviders.KeyFile)
SetEditText(Resource.Id.pass_keyfile, _keyFileOrProvider);
} }
protected override void OnDestroy() 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) { private void SetEditText(int resId, String str) {
TextView te = (TextView) FindViewById(resId); TextView te = (TextView) FindViewById(resId);
//assert(te == null); //assert(te == null);
@ -1002,13 +1120,40 @@ namespace keepass2android
public override void Run() { 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 ) if ( Success )
{ {
_act.SetEditText(Resource.Id.password, ""); _act.SetEditText(Resource.Id.password, "");
_act.SetEditText(Resource.Id.pass_otpsecret, "");
foreach (int otpId in _act._otpTextViewIds)
{
_act.SetEditText(otpId, "");
}
_act.LaunchNextActivity(); _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 else
{ {

File diff suppressed because it is too large Load Diff

View File

@ -1,188 +1,213 @@
<?xml version="1.0" encoding="utf-8"?> <?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"
android:layout_width="fill_parent" xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:layout_marginLeft="12dip" android:layout_height="match_parent"
android:layout_marginRight="12dip" >
android:layout_marginBottom="12dip" <LinearLayout
android:orientation="vertical"
>
<RelativeLayout
android:id="@+id/filename_group"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content"> android:layout_height="match_parent"
<TextView
android:id="@+id/filename_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance_SmallHeading"
android:text="@string/pass_filename" />
<ImageView
android:id="@+id/divider1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/filename_label"
android:scaleType="fitXY"
android:src="@android:drawable/divider_horizontal_dark" />
<HorizontalScrollView
android:id="@+id/filenamescroll"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/divider1">
<TextView
android:id="@+id/filename"
style="@style/GroupText"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="none"
android:focusable="true"
android:focusableInTouchMode="true" />
</HorizontalScrollView>
<ImageView
android:id="@+id/divider2"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/filenamescroll"
android:scaleType="fitXY"
android:src="@android:drawable/divider_horizontal_dark" />
</RelativeLayout>
<TextView
android:id="@+id/password_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="" />
<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:orientation="horizontal">
<EditText
android:id="@+id/password"
android:layout_width="0px"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textPassword"
android:layout_weight="1"
android:hint="@string/hint_login_pass" />
<ImageButton
android:id="@+id/toggle_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_menu_view" />
</LinearLayout>
<LinearLayout
android:id="@+id/keyfileLine"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/pass_keyfile"
android:layout_width="0px"
android:layout_height="wrap_content"
android:singleLine="true"
android:layout_weight="1"
android:hint="@string/entry_keyfile" />
<ImageButton
android:id="@+id/browse_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_folder_small" />
</LinearLayout>
<LinearLayout
android:id="@+id/otpView"
android:layout_marginLeft="12dip" android:layout_marginLeft="12dip"
android:layout_marginRight="12dip" android:layout_marginRight="12dip"
android:layout_width="fill_parent" android:layout_marginBottom="12dip"
android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
> >
<LinearLayout <RelativeLayout
android:id="@+id/otpInitView" android:id="@+id/filename_group"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/filename_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance_SmallHeading"
android:text="@string/pass_filename" />
<ImageView
android:id="@+id/divider1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/filename_label"
android:scaleType="fitXY"
android:src="@android:drawable/divider_horizontal_dark" />
<HorizontalScrollView
android:id="@+id/filenamescroll"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/divider1">
<TextView
android:id="@+id/filename"
style="@style/GroupText"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="none"
android:focusable="true"
android:focusableInTouchMode="true" />
</HorizontalScrollView>
<ImageView
android:id="@+id/divider2"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/filenamescroll"
android:scaleType="fitXY"
android:src="@android:drawable/divider_horizontal_dark" />
</RelativeLayout>
<TextView
android:id="@+id/password_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/master_key_type" />
<Spinner
android:id="@+id/password_mode_spinner"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
> android:entries="@array/password_modes"
<Button />
android:id="@+id/init_otp"
android:text="@string/init_otp" <LinearLayout
android:layout_width="wrap_content" android:id="@+id/passwordLine"
android:layout_height="wrap_content"/> android:layout_width="fill_parent"
<TextView android:layout_height="wrap_content"
android:id="@+id/otps_pending" android:orientation="horizontal">
android:text="@string/otps_pending" <EditText
android:layout_width="wrap_content" android:id="@+id/password"
android:layout_height="wrap_content" /> android:layout_width="0px"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textPassword"
android:layout_weight="1"
android:hint="@string/hint_login_pass" />
<ImageButton
android:id="@+id/toggle_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_menu_view" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/otpEntry" android:id="@+id/keyfileLine"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" android:orientation="horizontal">
android:orientation="vertical" <EditText
android:id="@+id/pass_keyfile"
android:layout_width="0px"
android:layout_height="wrap_content"
android:singleLine="true"
android:layout_weight="1"
android:hint="@string/entry_keyfile" />
<ImageButton
android:id="@+id/browse_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_folder_small" />
</LinearLayout>
<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"
> >
<TextView <LinearLayout
android:id="@+id/otp_expl" android:id="@+id/otpInitView"
android:layout_width="wrap_content" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/otp_explanation" /> android:orientation="vertical"
>
<Button
android:id="@+id/init_otp"
android:text="@string/init_otp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/otps_pending"
android:text="@string/otps_pending"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<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 <EditText
android:id="@+id/otp1" android:id="@+id/otp1"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="93317749" android:text="93317749"
android:singleLine="true" /> android:singleLine="true" />
<EditText <EditText
android:id="@+id/otp2" android:id="@+id/otp2"
android:text="54719327" android:text="54719327"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" /> android:singleLine="true" />
<EditText <EditText
android:id="@+id/otp3" android:id="@+id/otp3"
android:text="49844651" android:text="49844651"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" /> android:singleLine="true" />
<EditText <EditText
android:id="@+id/otp4" android:id="@+id/otp4"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" /> android:singleLine="true" />
<EditText <EditText
android:id="@+id/otp5" android:id="@+id/otp5"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" /> android:singleLine="true" />
<EditText <EditText
android:id="@+id/otp6" android:id="@+id/otp6"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" /> android:singleLine="true" />
</LinearLayout>
</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"
android:text="@android:string/ok"
android:layout_width="fill_parent"
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" />
<CheckBox
android:id="@+id/enable_quickunlock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable_quickunlock" />
</LinearLayout> </LinearLayout>
<Button </ScrollView>
android:id="@+id/pass_ok"
android:text="@android:string/ok"
android:layout_width="fill_parent"
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" />
<CheckBox
android:id="@+id/enable_quickunlock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable_quickunlock" />
</LinearLayout>

View File

@ -143,6 +143,7 @@
<string name="omitbackup_summary">Omit \'Backup\' and Recycle Bin group from search results</string> <string name="omitbackup_summary">Omit \'Backup\' and Recycle Bin group from search results</string>
<string name="pass_filename">KeePass database filename</string> <string name="pass_filename">KeePass database filename</string>
<string name="password_title">Enter database password</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="progress_create">Creating new database…</string>
<string name="create_database">Create database</string> <string name="create_database">Create database</string>
<string name="progress_title">Working…</string> <string name="progress_title">Working…</string>
@ -299,9 +300,17 @@
<string name="UpdatedRemoteFileOnLoad">Updated remote file.</string> <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="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="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="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="ErrorOcurred">An error occured:</string>
<string name="synchronize_database_menu">Synchronize database…</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="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_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="otp_hint">OTP %1$d</string>
<string name="CouldntLoadOtpAuxFile">Could not load auxiliary OTP file!</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_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_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="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> <string name="loading">Loading…</string>
@ -477,5 +491,6 @@ Initial public release
<item>Password only</item> <item>Password only</item>
<item>Password + Key file</item> <item>Password + Key file</item>
<item>Password + OTP</item> <item>Password + OTP</item>
<item>Password + OTP secret (recovery mode)</item>
</string-array> </string-array>
</resources> </resources>

View File

@ -49,13 +49,19 @@ namespace OtpKeyProv
private static IOConnectionInfo GetAuxFileIoc(KeyProviderQueryContext ctx) private static IOConnectionInfo GetAuxFileIoc(KeyProviderQueryContext ctx)
{ {
IOConnectionInfo ioc = ctx.DatabaseIOInfo.CloneDeep(); IOConnectionInfo ioc = ctx.DatabaseIOInfo.CloneDeep();
IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(ioc); var iocAux = GetAuxFileIoc(ioc);
IOConnectionInfo iocAux = fileStorage.GetFilePath(fileStorage.GetParentPath(ioc),
fileStorage.GetFilenameWithoutPathAndExt(ioc) + AuxFileExt);
return iocAux; 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) public static OtpInfo LoadOtpInfo(KeyProviderQueryContext ctx)
{ {
return OtpInfo.Load(GetAuxFileIoc(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) KeyProviderQueryContext ctx)
{ {
otpInfo.Type = ProvType; 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) using (var trans = App.Kp2a.GetOtpAuxFileStorage(ioc)
.OpenWriteTransaction(ioc, App.Kp2a.GetBooleanPreference(PreferenceKey.UseFileTransactions))) .OpenWriteTransaction(ioc, App.Kp2a.GetBooleanPreference(PreferenceKey.UseFileTransactions)))
{ {
XmlWriterSettings xws = new XmlWriterSettings(); var stream = trans.OpenFile();
xws.CloseOutput = true; WriteToStream(otpInfo, stream);
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();
trans.CommitWrite(); trans.CommitWrite();
} }
return true; return true;
@ -222,6 +212,31 @@ namespace OtpKeyProv
return false; 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() public void EncryptSecret()
{ {
if(m_pbSecret == null) throw new InvalidOperationException(); if(m_pbSecret == null) throw new InvalidOperationException();

View File

@ -32,6 +32,7 @@ using Android.Preferences;
using TwofishCipher; using TwofishCipher;
#endif #endif
using keepass2android.Io; using keepass2android.Io;
using keepass2android.addons.OtpKeyProv;
namespace keepass2android namespace keepass2android
{ {
@ -294,8 +295,11 @@ namespace keepass2android
builder.SetNegativeButton(GetResourceString(noString), noHandler); builder.SetNegativeButton(GetResourceString(noString), noHandler);
builder.SetNeutralButton(ctx.GetString(Android.Resource.String.Cancel), if (cancelHandler != null)
cancelHandler); {
builder.SetNeutralButton(ctx.GetString(Android.Resource.String.Cancel),
cancelHandler);
}
Dialog dialog = builder.Create(); Dialog dialog = builder.Create();
dialog.Show(); dialog.Show();
@ -447,7 +451,7 @@ namespace keepass2android
return _db; return _db;
} }
void ShowToast(string message) internal void ShowToast(string message)
{ {
var handler = new Handler(Looper.MainLooper); var handler = new Handler(Looper.MainLooper);
handler.Post(() => { Toast.MakeText(Application.Context, message, ToastLength.Long).Show(); }); handler.Post(() => { Toast.MakeText(Application.Context, message, ToastLength.Long).Show(); });
@ -466,7 +470,8 @@ namespace keepass2android
public void UpdatedCachedFileOnLoad(IOConnectionInfo ioc) 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) public void UpdatedRemoteFileOnLoad(IOConnectionInfo ioc)
@ -510,7 +515,7 @@ namespace keepass2android
if (DatabaseCacheEnabled) if (DatabaseCacheEnabled)
{ {
return new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this); return new OtpAuxCachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, new OtpAuxCacheSupervisor(this));
} }
else else
{ {
@ -532,7 +537,7 @@ namespace keepass2android
} }
///Application class for Keepass2Android: Contains static Database variable to be used by all components. ///Application class for Keepass2Android: Contains static Database variable to be used by all components.
#if NoNet #if NoNet
[Application(Debuggable=false, Label=AppNames.AppName)] [Application(Debuggable=false, Label=AppNames.AppName)]
#else #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 #if !EXCLUDE_FILECHOOSER
StartFileChooser(ioc.Path); StartFileChooser(ioc.Path);
#else #else
LaunchPasswordActivityForIoc(new IOConnectionInfo { Path = "/mnt/sdcard/keepass/yubi2.kdbx"}); LaunchPasswordActivityForIoc(new IOConnectionInfo { Path = "/mnt/sdcard/keepass/yubi.kdbx"});
#endif #endif
} }
if ((resultCode == Result.Canceled) && (data != null) && (data.HasExtra("EXTRA_ERROR_MESSAGE"))) if ((resultCode == Result.Canceled) && (data != null) && (data.HasExtra("EXTRA_ERROR_MESSAGE")))

View File

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