/* This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file is based on Keepassdroid, Copyright Brian Pellin. Keepass2Android is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Keepass2Android is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Keepass2Android. If not, see . */ 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 IKp2aApp _app; private readonly bool _dontSave; /// /// stream for reading the data from the original file. If this is set to a non-null value, we know we need to sync /// private readonly Stream _streamForOrigFile; private readonly Context _ctx; private Thread _workerThread; public SaveDb(Context ctx, IKp2aApp app, OnFinish finish, bool dontSave) : base(finish) { _ctx = ctx; _app = app; _dontSave = dontSave; } /// /// Constructor for sync /// /// /// /// /// /// Stream for reading the data from the (changed) original location public SaveDb(Context ctx, IKp2aApp app, OnFinish finish, bool dontSave, Stream streamForOrigFile) : base(finish) { _ctx = ctx; _app = app; _dontSave = dontSave; _streamForOrigFile = streamForOrigFile; } public SaveDb(Context ctx, IKp2aApp app, OnFinish finish) : base(finish) { _ctx = ctx; _app = app; _dontSave = false; } public override void Run () { if (!_dontSave) { try { StatusLogger.UpdateMessage(UiStringKey.saving_database); IOConnectionInfo ioc = _app.GetDb().Ioc; IFileStorage fileStorage = _app.GetFileStorage(ioc); if (_streamForOrigFile == null) { if ((!_app.GetBooleanPreference(PreferenceKey.CheckForFileChangesOnSave)) || (_app.GetDb().KpDatabase.HashOfFileOnDisk == null)) //first time saving { PerformSaveWithoutCheck(fileStorage, ioc); Finish(true); return; } } if ( (_streamForOrigFile != null) || 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.MessageSyncQuestion, UiStringKey.YesSynchronize, UiStringKey.NoOverwrite, //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. 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) { MessageService.ShowSaveWarning(pd.IOConnectionInfo, exSave, true); bSuccess = false; } */ Kp2aLog.Log("Error while saving: " + e.ToString()); Finish(false, e.Message); return; } } else { Finish(true); } } private void RunInWorkerThread(Action runHandler) { try { _workerThread = new Thread(() => { try { runHandler(); } catch (Exception e) { Kp2aLog.Log("Error in worker thread of SaveDb: " + e); Finish(false, e.Message); } }); _workerThread.Start(); } catch (Exception e) { Kp2aLog.Log("Error starting worker thread of SaveDb: "+e); Finish(false, e.Message); } } public void JoinWorkerThread() { if (_workerThread != null) _workerThread.Join(); } private void MergeIn(IFileStorage fileStorage, IOConnectionInfo ioc) { StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.SynchronizingDatabase)); 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(GetStreamForBaseFile(fileStorage, ioc), KdbpFile.GetFormatToUse(ioc), null); pwDatabase.MergeIn(pwImp, PwMergeMethod.Synchronize, null); } private Stream GetStreamForBaseFile(IFileStorage fileStorage, IOConnectionInfo ioc) { //if we have the original file already available: use it if (_streamForOrigFile != null) return _streamForOrigFile; //if the file storage caches, it might return the local data in case of a conflict. This would result in data loss // so we need to ensure we get the data from remote (only if the remote file is available. if not, we won't overwrite anything) CachingFileStorage cachingFileStorage = fileStorage as CachingFileStorage; if (cachingFileStorage != null) { return cachingFileStorage.OpenRemoteForReadIfAvailable(ioc); } return fileStorage.OpenFileForRead(ioc); } private void PerformSaveWithoutCheck(IFileStorage fileStorage, IOConnectionInfo ioc) { StatusLogger.UpdateSubMessage(""); _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); } } }