* Introduced IFileStorage interface: Better abstraction than current IOConnection (suitable for cloud support). Currently only implemented by the built-in IOConnection (local/http/ftp)

* Implemented Merge functionality for SaveDB. UI is not yet implemented!
* Added tests for merge functionality
This commit is contained in:
Philipp Crocoll
2013-07-09 09:59:17 +02:00
parent 64e62cae70
commit 84aeb31fd0
42 changed files with 912 additions and 161 deletions

View File

@@ -17,10 +17,12 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file
using System;
using System.Collections.Generic;
using System.IO;
using Android.Content;
using KeePassLib;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using keepass2android.Io;
namespace keepass2android
{
@@ -34,7 +36,7 @@ namespace keepass2android
public PwGroup Root;
public PwDatabase KpDatabase;
public IOConnectionInfo Ioc { get { return KpDatabase.IOConnectionInfo; } }
public DateTime LastChangeDate;
public string LastFileVersion;
public SearchDbHelper SearchHelper;
public IDrawableFactory DrawableFactory;
@@ -83,15 +85,16 @@ namespace keepass2android
public bool DidOpenFileChange()
{
if ((Loaded == false) || (Ioc.IsLocalFile() == false))
if (Loaded == false)
{
return false;
}
return System.IO.File.GetLastWriteTimeUtc(Ioc.Path) > LastChangeDate;
return _app.GetFileStorage(Ioc).CheckForFileChangeFast(Ioc, LastFileVersion);
}
public void LoadData(IKp2aApp app, IOConnectionInfo iocInfo, String password, String keyfile, UpdateStatus status)
public void LoadData(IKp2aApp app, IOConnectionInfo iocInfo, String password, String keyfile, ProgressDialogStatusLogger status)
{
PwDatabase pwDatabase = new PwDatabase();
@@ -103,15 +106,17 @@ namespace keepass2android
try
{
compositeKey.AddUserKey(new KcpKeyFile(keyfile));
} catch (Exception)
} catch (Exception e)
{
Kp2aLog.Log(e.ToString());
throw new KeyFileException();
}
}
try
{
pwDatabase.Open(iocInfo, compositeKey, status);
IFileStorage fileStorage = _app.GetFileStorage(iocInfo);
pwDatabase.Open(fileStorage.OpenFileForRead(iocInfo), fileStorage.GetFilenameWithoutPathAndExt(iocInfo), iocInfo, compositeKey, status);
}
catch (Exception)
{
@@ -125,14 +130,9 @@ namespace keepass2android
else throw;
}
status.UpdateSubMessage("");
if (iocInfo.IsLocalFile())
{
LastChangeDate = System.IO.File.GetLastWriteTimeUtc(iocInfo.Path);
} else
{
LastChangeDate = DateTime.MinValue;
}
LastFileVersion = _app.GetFileStorage(iocInfo).GetCurrentFileVersionFast(iocInfo);
Root = pwDatabase.RootGroup;
PopulateGlobals(Root);
@@ -184,9 +184,14 @@ namespace keepass2android
public void SaveData(Context ctx) {
KpDatabase.UseFileTransactions = _app.GetBooleanPreference(PreferenceKey.UseFileTransactions);
KpDatabase.Save(null);
using (IWriteTransaction trans = _app.GetFileStorage(Ioc).OpenWriteTransaction(Ioc, KpDatabase.UseFileTransactions))
{
KpDatabase.Save(trans.OpenFile(), null);
trans.CommitWrite();
}
}
private void PopulateGlobals (PwGroup currentGroup)
{

View File

@@ -21,31 +21,38 @@ using KeePassLib;
namespace keepass2android
{
public class AddEntry : RunnableOnFinish {
protected Database Db;
protected Database Db
{
get { return _app.GetDb(); }
}
private readonly IKp2aApp _app;
private readonly PwEntry _entry;
private readonly PwGroup _parentGroup;
private readonly Context _ctx;
public static AddEntry GetInstance(Context ctx, Database db, PwEntry entry, PwGroup parentGroup, OnFinish finish) {
public static AddEntry GetInstance(Context ctx, IKp2aApp app, PwEntry entry, PwGroup parentGroup, OnFinish finish) {
return new AddEntry(ctx, db, entry, parentGroup, finish);
return new AddEntry(ctx, app, entry, parentGroup, finish);
}
protected AddEntry(Context ctx, Database db, PwEntry entry, PwGroup parentGroup, OnFinish finish):base(finish) {
protected AddEntry(Context ctx, IKp2aApp app, PwEntry entry, PwGroup parentGroup, OnFinish finish):base(finish) {
_ctx = ctx;
_parentGroup = parentGroup;
Db = db;
_app = app;
_entry = entry;
OnFinishToRun = new AfterAdd(db, entry, OnFinishToRun);
_onFinishToRun = new AfterAdd(app.GetDb(), entry, OnFinishToRun);
}
public override void Run() {
public override void Run() {
StatusLogger.UpdateMessage(UiStringKey.AddingEntry);
_parentGroup.AddEntry(_entry, true);
// Commit to disk
SaveDb save = new SaveDb(_ctx, Db, OnFinishToRun);
SaveDb save = new SaveDb(_ctx, _app, OnFinishToRun);
save.SetStatusLogger(StatusLogger);
save.Run();
}
@@ -72,7 +79,9 @@ namespace keepass2android
// Add entry to global
_db.Entries[_entry.Uuid] = _entry;
} else {
} else
{
StatusLogger.UpdateMessage(UiStringKey.UndoingChanges);
//TODO test fail
_entry.ParentGroup.Entries.Remove(_entry);
}

View File

@@ -23,7 +23,11 @@ namespace keepass2android
{
public class AddGroup : RunnableOnFinish {
internal Database Db;
internal Database Db
{
get { return _app.GetDb(); }
}
private IKp2aApp _app;
private readonly String _name;
private readonly int _iconId;
internal PwGroup Group;
@@ -32,31 +36,32 @@ namespace keepass2android
readonly Context _ctx;
public static AddGroup GetInstance(Context ctx, Database db, String name, int iconid, PwGroup parent, OnFinish finish, bool dontSave) {
return new AddGroup(ctx, db, name, iconid, parent, finish, dontSave);
public static AddGroup GetInstance(Context ctx, IKp2aApp app, String name, int iconid, PwGroup parent, OnFinish finish, bool dontSave) {
return new AddGroup(ctx, app, name, iconid, parent, finish, dontSave);
}
private AddGroup(Context ctx, Database db, String name, int iconid, PwGroup parent, OnFinish finish, bool dontSave): base(finish) {
private AddGroup(Context ctx, IKp2aApp app, String name, int iconid, PwGroup parent, OnFinish finish, bool dontSave): base(finish) {
_ctx = ctx;
Db = db;
_name = name;
_iconId = iconid;
Parent = parent;
DontSave = dontSave;
OnFinishToRun = new AfterAdd(this, OnFinishToRun);
_app = app;
_onFinishToRun = new AfterAdd(this, OnFinishToRun);
}
public override void Run() {
StatusLogger.UpdateMessage(UiStringKey.AddingGroup);
// Generate new group
Group = new PwGroup(true, true, _name, (PwIcon)_iconId);
Parent.AddGroup(Group, true);
// Commit to disk
SaveDb save = new SaveDb(_ctx, Db, OnFinishToRun, DontSave);
SaveDb save = new SaveDb(_ctx, _app, OnFinishToRun, DontSave);
save.SetStatusLogger(StatusLogger);
save.Run();
}
@@ -77,6 +82,7 @@ namespace keepass2android
// Add group to global list
_addGroup.Db.Groups[_addGroup.Group.Uuid] = _addGroup.Group;
} else {
StatusLogger.UpdateMessage(UiStringKey.UndoingChanges);
_addGroup.Parent.Groups.Remove(_addGroup.Group);
}

View File

@@ -40,6 +40,7 @@ namespace keepass2android
public override void Run() {
StatusLogger.UpdateMessage(UiStringKey.progress_create);
Database db = _app.CreateNewDatabase();
db.KpDatabase = new KeePassLib.PwDatabase();
@@ -58,14 +59,15 @@ namespace keepass2android
db.SearchHelper = new SearchDbHelper(_app);
// Add a couple default groups
AddGroup internet = AddGroup.GetInstance(_ctx, db, "Internet", 1, db.KpDatabase.RootGroup, null, true);
AddGroup internet = AddGroup.GetInstance(_ctx, _app, "Internet", 1, db.KpDatabase.RootGroup, null, true);
internet.Run();
AddGroup email = AddGroup.GetInstance(_ctx, db, "eMail", 19, db.KpDatabase.RootGroup, null, true);
AddGroup email = AddGroup.GetInstance(_ctx, _app, "eMail", 19, db.KpDatabase.RootGroup, null, true);
email.Run();
// Commit changes
SaveDb save = new SaveDb(_ctx, db, OnFinishToRun, _dontSave);
OnFinishToRun = null;
SaveDb save = new SaveDb(_ctx, _app, OnFinishToRun, _dontSave);
save.SetStatusLogger(StatusLogger);
_onFinishToRun = null;
save.Run();

View File

@@ -48,8 +48,9 @@ namespace keepass2android
}
}
public override void Run() {
public override void Run()
{
StatusLogger.UpdateMessage(UiStringKey.DeletingEntry);
PwDatabase pd = Db.KpDatabase;
PwGroup pgRecycleBin = pd.RootGroup.FindGroup(pd.RecycleBinUuid, true);
@@ -68,7 +69,7 @@ namespace keepass2android
PwDeletedObject pdo = new PwDeletedObject(pe.Uuid, dtNow);
pd.DeletedObjects.Add(pdo);
OnFinishToRun = new ActionOnFinish((success, message) =>
_onFinishToRun = new ActionOnFinish((success, message) =>
{
if (success)
{
@@ -89,7 +90,7 @@ namespace keepass2android
pgRecycleBin.AddEntry(pe, true, true);
pe.Touch(false);
OnFinishToRun = new ActionOnFinish( (success, message) =>
_onFinishToRun = new ActionOnFinish( (success, message) =>
{
if ( success ) {
// Mark previous parent dirty
@@ -106,7 +107,8 @@ namespace keepass2android
}
// Commit database
SaveDb save = new SaveDb(Ctx, Db, OnFinishToRun, false);
SaveDb save = new SaveDb(Ctx, App, OnFinishToRun, false);
save.SetStatusLogger(StatusLogger);
save.Run();

View File

@@ -70,6 +70,7 @@ namespace keepass2android
public override void Run() {
StatusLogger.UpdateMessage(UiStringKey.DeletingGroup);
//from KP Desktop
PwGroup pg = _group;
PwGroup pgParent = pg.ParentGroup;
@@ -86,7 +87,7 @@ namespace keepass2android
PwDeletedObject pdo = new PwDeletedObject(pg.Uuid, DateTime.Now);
pd.DeletedObjects.Add(pdo);
OnFinishToRun = new AfterDeletePermanently(OnFinishToRun, App, _group);
_onFinishToRun = new AfterDeletePermanently(OnFinishToRun, App, _group);
}
else // Recycle
{
@@ -95,7 +96,7 @@ namespace keepass2android
pgRecycleBin.AddGroup(pg, true, true);
pg.Touch(false);
OnFinishToRun = new ActionOnFinish((success, message) =>
_onFinishToRun = new ActionOnFinish((success, message) =>
{
if ( success ) {
// Mark new parent (Recycle bin) dirty
@@ -113,7 +114,8 @@ namespace keepass2android
}
// Save
SaveDb save = new SaveDb(Ctx, Db, OnFinishToRun, DontSave);
SaveDb save = new SaveDb(Ctx, App, OnFinishToRun, DontSave);
save.SetStatusLogger(StatusLogger);
save.Run();
}

View File

@@ -109,12 +109,12 @@ namespace keepass2android
(dlgSender, dlgEvt) =>
{
DeletePermanently = true;
ProgressTask pt = new ProgressTask(App, Ctx, this, UiStringKey.saving_database);
ProgressTask pt = new ProgressTask(App, Ctx, this);
pt.Run();
},
(dlgSender, dlgEvt) => {
DeletePermanently = false;
ProgressTask pt = new ProgressTask(App, Ctx, this, UiStringKey.saving_database);
ProgressTask pt = new ProgressTask(App, Ctx, this);
pt.Run();
},
(dlgSender, dlgEvt) => {},
@@ -124,7 +124,7 @@ namespace keepass2android
} else
{
ProgressTask pt = new ProgressTask(App, Ctx, this, UiStringKey.saving_database);
ProgressTask pt = new ProgressTask(App, Ctx, this);
pt.Run();
}
}

View File

@@ -43,7 +43,8 @@ namespace keepass2android
{
try
{
_app.GetDb().LoadData (_app, _ioc, _pass, _key, Status);
StatusLogger.UpdateMessage(UiStringKey.loading_database);
_app.GetDb().LoadData (_app, _ioc, _pass, _key, StatusLogger);
SaveFileData (_ioc, _key);
} catch (KeyFileException) {

View File

@@ -29,6 +29,13 @@ namespace keepass2android
protected OnFinish BaseOnFinish;
protected Handler Handler;
private ProgressDialogStatusLogger _statusLogger = new ProgressDialogStatusLogger(); //default: no logging but not null -> can be used whenever desired
public ProgressDialogStatusLogger StatusLogger
{
get { return _statusLogger; }
set { _statusLogger = value; }
}
protected OnFinish() {
}
@@ -47,7 +54,7 @@ namespace keepass2android
BaseOnFinish = finish;
Handler = null;
}
public void SetResult(bool success, String message) {
Success = success;
Message = message;

View File

@@ -21,13 +21,19 @@ namespace keepass2android
public abstract class RunnableOnFinish {
public OnFinish OnFinishToRun;
public UpdateStatus Status;
protected OnFinish _onFinishToRun;
public ProgressDialogStatusLogger StatusLogger = new ProgressDialogStatusLogger(); //default: empty but not null
protected RunnableOnFinish(OnFinish finish) {
OnFinishToRun = finish;
_onFinishToRun = finish;
}
public OnFinish OnFinishToRun
{
get { return _onFinishToRun; }
set { _onFinishToRun = value; }
}
protected void Finish(bool result, String message) {
if ( OnFinishToRun != null ) {
OnFinishToRun.SetResult(result, message);
@@ -42,8 +48,12 @@ namespace keepass2android
}
}
public void SetStatus(UpdateStatus status) {
Status = status;
public void SetStatusLogger(ProgressDialogStatusLogger status) {
if (OnFinishToRun != null)
{
OnFinishToRun.StatusLogger = status;
}
StatusLogger = status;
}
abstract public void Run();

View File

@@ -14,26 +14,40 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file
You should have received a copy of the GNU General Public License
along with Keepass2Android. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.IO;
using System.Security.Cryptography;
using Android.Content;
using Android.OS;
using Java.Lang;
using KeePassLib;
using KeePassLib.Serialization;
using KeePassLib.Utility;
using keepass2android.Io;
using Debug = System.Diagnostics.Debug;
using Exception = System.Exception;
namespace keepass2android
{
public class SaveDb : RunnableOnFinish {
private readonly Database _db;
private readonly IKp2aApp _app;
private readonly bool _dontSave;
private readonly Context _ctx;
public SaveDb(Context ctx, Database db, OnFinish finish, bool dontSave): base(finish) {
private Thread _workerThread;
public SaveDb(Context ctx, IKp2aApp app, OnFinish finish, bool dontSave): base(finish) {
_ctx = ctx;
_db = db;
_app = app;
_dontSave = dontSave;
}
public SaveDb(Context ctx, Database db, OnFinish finish):base(finish) {
public SaveDb(Context ctx, IKp2aApp app, OnFinish finish)
: base(finish)
{
_ctx = ctx;
_db = db;
_app = app;
_dontSave = false;
}
@@ -42,10 +56,67 @@ namespace keepass2android
{
if (! _dontSave) {
try {
_db.SaveData (_ctx);
if (_db.Ioc.IsLocalFile())
_db.LastChangeDate = System.IO.File.GetLastWriteTimeUtc(_db.Ioc.Path);
try
{
StatusLogger.UpdateMessage(UiStringKey.saving_database);
IOConnectionInfo ioc = _app.GetDb().Ioc;
IFileStorage fileStorage = _app.GetFileStorage(ioc);
if ((!_app.GetBooleanPreference(PreferenceKey.CheckForFileChangesOnSave))
|| (_app.GetDb().KpDatabase.HashOfFileOnDisk == null)) //first time saving
{
PerformSaveWithoutCheck(fileStorage, ioc);
Finish(true);
return;
}
if (fileStorage.CheckForFileChangeFast(ioc, _app.GetDb().LastFileVersion) //first try to use the fast change detection
|| (FileHashChanged(ioc, _app.GetDb().KpDatabase.HashOfFileOnDisk))) //if that fails, hash the file and compare:
{
//ask user...
_app.AskYesNoCancel(UiStringKey.TitleSyncQuestion, UiStringKey.MessageSyncQuestions,
//yes = sync
(sender, args) =>
{
Action runHandler = () =>
{
//note: when synced, the file might be downloaded once again from the server. Caching the data
//in the hashing function would solve this but increases complexity. I currently assume the files are
//small.
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.SynchronizingDatabase));
MergeIn(fileStorage, ioc);
PerformSaveWithoutCheck(fileStorage, ioc);
Finish(true);
};
RunInWorkerThread(runHandler);
},
//no = overwrite
(sender, args) =>
{
RunInWorkerThread( () =>
{
PerformSaveWithoutCheck(fileStorage, ioc);
Finish(true);
});
},
//cancel
(sender, args) =>
{
RunInWorkerThread(() => Finish(false));
},
_ctx
);
}
else
{
PerformSaveWithoutCheck(fileStorage, ioc);
Finish(true);
}
} catch (Exception e) {
/* TODO KPDesktop:
* catch(Exception exSave)
@@ -54,13 +125,85 @@ namespace keepass2android
bSuccess = false;
}
*/
Finish (false, e.Message);
Finish (false, e.ToString());
return;
}
}
Finish(true);
}
private void RunInWorkerThread(Action runHandler)
{
try
{
_workerThread = new Thread(runHandler);
_workerThread.Run();
}
catch (Exception e)
{
Kp2aLog.Log("Error in worker thread of SaveDb: "+e);
Finish(false, e.Message);
}
}
public void JoinWorkerThread()
{
if (_workerThread != null)
_workerThread.Join();
}
private void MergeIn(IFileStorage fileStorage, IOConnectionInfo ioc)
{
PwDatabase pwImp = new PwDatabase();
PwDatabase pwDatabase = _app.GetDb().KpDatabase;
pwImp.New(new IOConnectionInfo(), pwDatabase.MasterKey);
pwImp.MemoryProtection = pwDatabase.MemoryProtection.CloneDeep();
pwImp.MasterKey = pwDatabase.MasterKey;
KdbxFile kdbx = new KdbxFile(pwImp);
kdbx.Load(fileStorage.OpenFileForRead(ioc), KdbxFormat.Default, null);
pwDatabase.MergeIn(pwImp, PwMergeMethod.Synchronize, null);
}
private void PerformSaveWithoutCheck(IFileStorage fileStorage, IOConnectionInfo ioc)
{
_app.GetDb().SaveData(_ctx);
_app.GetDb().LastFileVersion = fileStorage.GetCurrentFileVersionFast(ioc);
}
public byte[] HashFile(IOConnectionInfo iocFile)
{
if (iocFile == null) { Debug.Assert(false); return null; } // Assert only
Stream sIn;
try
{
sIn = _app.GetFileStorage(iocFile).OpenFileForRead(iocFile);
if (sIn == null) throw new FileNotFoundException();
}
catch (Exception) { return null; }
byte[] pbHash;
try
{
SHA256Managed sha256 = new SHA256Managed();
pbHash = sha256.ComputeHash(sIn);
}
catch (Exception) { Debug.Assert(false); sIn.Close(); return null; }
sIn.Close();
return pbHash;
}
private bool FileHashChanged(IOConnectionInfo ioc, byte[] hashOfFileOnDisk)
{
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.CheckingTargetFileForChanges));
return !MemUtil.ArraysEqual(HashFile(ioc), hashOfFileOnDisk);
}
}

View File

@@ -25,21 +25,23 @@ namespace keepass2android
private readonly String _password;
private readonly String _keyfile;
private readonly Database _db;
private readonly IKp2aApp _app;
private readonly bool _dontSave;
private readonly Context _ctx;
public SetPassword(Context ctx, Database db, String password, String keyfile, OnFinish finish): base(finish) {
public SetPassword(Context ctx, IKp2aApp app, String password, String keyfile, OnFinish finish): base(finish) {
_ctx = ctx;
_db = db;
_app = app;
_password = password;
_keyfile = keyfile;
_dontSave = false;
}
public SetPassword(Context ctx, Database db, String password, String keyfile, OnFinish finish, bool dontSave): base(finish) {
public SetPassword(Context ctx, IKp2aApp app, String password, String keyfile, OnFinish finish, bool dontSave)
: base(finish)
{
_ctx = ctx;
_db = db;
_app = app;
_password = password;
_keyfile = keyfile;
_dontSave = dontSave;
@@ -48,7 +50,8 @@ namespace keepass2android
public override void Run ()
{
PwDatabase pm = _db.KpDatabase;
StatusLogger.UpdateMessage(UiStringKey.SettingPassword);
PwDatabase pm = _app.GetDb().KpDatabase;
CompositeKey newKey = new CompositeKey ();
if (String.IsNullOrEmpty (_password) == false) {
newKey.AddUserKey (new KcpPassword (_password));
@@ -69,8 +72,9 @@ namespace keepass2android
pm.MasterKey = newKey;
// Save Database
OnFinishToRun = new AfterSave(previousKey, previousMasterKeyChanged, pm, OnFinishToRun);
SaveDb save = new SaveDb(_ctx, _db, OnFinishToRun, _dontSave);
_onFinishToRun = new AfterSave(previousKey, previousMasterKeyChanged, pm, OnFinishToRun);
SaveDb save = new SaveDb(_ctx, _app, OnFinishToRun, _dontSave);
save.SetStatusLogger(StatusLogger);
save.Run();
}

View File

@@ -22,32 +22,33 @@ namespace keepass2android
{
public class UpdateEntry : RunnableOnFinish {
private readonly Database _db;
private readonly IKp2aApp _app;
private readonly Context _ctx;
public UpdateEntry(Context ctx, Database db, PwEntry oldE, PwEntry newE, OnFinish finish):base(finish) {
public UpdateEntry(Context ctx, IKp2aApp app, PwEntry oldE, PwEntry newE, OnFinish finish):base(finish) {
_ctx = ctx;
_db = db;
_app = app;
OnFinishToRun = new AfterUpdate(oldE, newE, db, finish);
_onFinishToRun = new AfterUpdate(oldE, newE, app, finish);
}
public override void Run() {
// Commit to disk
SaveDb save = new SaveDb(_ctx, _db, OnFinishToRun);
SaveDb save = new SaveDb(_ctx, _app, OnFinishToRun);
save.SetStatusLogger(StatusLogger);
save.Run();
}
private class AfterUpdate : OnFinish {
private readonly PwEntry _backup;
private readonly PwEntry _updatedEntry;
private readonly Database _db;
private readonly IKp2aApp _app;
public AfterUpdate(PwEntry backup, PwEntry updatedEntry, Database db, OnFinish finish):base(finish) {
public AfterUpdate(PwEntry backup, PwEntry updatedEntry, IKp2aApp app, OnFinish finish):base(finish) {
_backup = backup;
_updatedEntry = updatedEntry;
_db = db;
_app = app;
}
public override void Run() {
@@ -65,11 +66,12 @@ namespace keepass2android
if ( parent != null ) {
// Mark parent group dirty
_db.Dirty.Add(parent);
_app.GetDb().Dirty.Add(parent);
}
}
} else {
StatusLogger.UpdateMessage(UiStringKey.UndoingChanges);
// If we fail to save, back out changes to global structure
//TODO test fail
_updatedEntry.AssignProperties(_backup, false, true, false);