From 5fc22b9530f8573b465ecd5e9032db586a8ae5ea Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Wed, 11 Apr 2018 06:27:56 +0200 Subject: [PATCH] introduced automatic local backups after successfully opening a database. This should make sure that users can access their database even if the file gets corrupted (#238) --- src/Kp2aBusinessLogic/IKp2aApp.cs | 2 +- .../Io/BuiltInFileStorage.cs | 26 ++++- src/Kp2aBusinessLogic/Io/IoUtil.cs | 52 +++++++++- src/Kp2aBusinessLogic/UiStringKey.cs | 3 +- src/Kp2aBusinessLogic/database/Database.cs | 27 ++--- src/keepass2android/PasswordActivity.cs | 98 ++++++++++++------- .../Resources/values/config.xml | 1 + .../Resources/values/strings.xml | 19 +++- .../Resources/xml/preferences.xml | 9 ++ src/keepass2android/app/App.cs | 71 ++++++++++++-- .../fileselect/FileDbHelper.cs | 30 ++++-- .../fileselect/FileSelectActivity.cs | 11 ++- .../settings/DatabaseSettingsActivity.cs | 30 +----- 13 files changed, 276 insertions(+), 103 deletions(-) diff --git a/src/Kp2aBusinessLogic/IKp2aApp.cs b/src/Kp2aBusinessLogic/IKp2aApp.cs index de30aae6..1c81bc50 100644 --- a/src/Kp2aBusinessLogic/IKp2aApp.cs +++ b/src/Kp2aBusinessLogic/IKp2aApp.cs @@ -52,7 +52,7 @@ namespace keepass2android /// /// Tell the app that the file from ioc was opened with keyfile. /// - void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile); + void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile, string displayName = ""); /// /// Creates a new database and returns it diff --git a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs index 45c8f8b5..6992fd13 100644 --- a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs @@ -11,9 +11,11 @@ using Android.App; using Android.Content; using Android.Content.PM; using Android.OS; +using Android.Preferences; using Android.Support.V13.App; using Android.Support.V4.App; using Java.IO; +using Java.Util; using KeePassLib.Serialization; using KeePassLib.Utility; using ActivityCompat = Android.Support.V13.App.ActivityCompat; @@ -379,7 +381,14 @@ namespace keepass2android.Io { if (ioc.IsLocalFile()) { - if (IsLocalFileFlaggedReadOnly(ioc)) + if (IsLocalBackup(ioc)) + { + if (reason != null) + reason.Result = UiStringKey.ReadOnlyReason_LocalBackup; + return true; + } + + if (IsLocalFileFlaggedReadOnly(ioc)) { if (reason != null) reason.Result = UiStringKey.ReadOnlyReason_ReadOnlyFlag; @@ -400,7 +409,20 @@ namespace keepass2android.Io return false; } - private bool IsLocalFileFlaggedReadOnly(IOConnectionInfo ioc) + private readonly Dictionary _isLocalBackupCache = new Dictionary(); + private bool IsLocalBackup(IOConnectionInfo ioc) + { + bool result; + if (_isLocalBackupCache.TryGetValue(ioc.Path, out result)) + return result; + + result = (PreferenceManager.GetDefaultSharedPreferences(Application.Context) + .GetBoolean(IoUtil.GetIocPrefKey(ioc, "is_local_backup"), false)); + _isLocalBackupCache[ioc.Path] = result; + return result; + } + + private bool IsLocalFileFlaggedReadOnly(IOConnectionInfo ioc) { //see http://stackoverflow.com/a/33292700/292233 try diff --git a/src/Kp2aBusinessLogic/Io/IoUtil.cs b/src/Kp2aBusinessLogic/Io/IoUtil.cs index ee1463f8..eeb8ba56 100644 --- a/src/Kp2aBusinessLogic/Io/IoUtil.cs +++ b/src/Kp2aBusinessLogic/Io/IoUtil.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography; using System.Text; using Android.Content; using Android.OS; using Java.IO; using KeePassLib.Serialization; +using KeePassLib.Utility; namespace keepass2android.Io { @@ -125,5 +127,53 @@ namespace keepass2android.Io return ctx.FilesDir; } - } + //creates a local ioc where the sourceIoc can be stored to + public static IOConnectionInfo GetInternalIoc(IOConnectionInfo sourceIoc, Context ctx) + { + Java.IO.File internalDirectory = IoUtil.GetInternalDirectory(ctx); + string targetPath = UrlUtil.GetFileName(sourceIoc.Path); + targetPath = targetPath.Trim("|\\?*<\":>+[]/'".ToCharArray()); + if (targetPath == "") + targetPath = "internal"; + if (new File(internalDirectory, targetPath).Exists()) + { + int c = 1; + var ext = UrlUtil.GetExtension(targetPath); + var filenameWithoutExt = UrlUtil.StripExtension(targetPath); + do + { + c++; + targetPath = filenameWithoutExt + c; + if (!String.IsNullOrEmpty(ext)) + targetPath += "." + ext; + } while (new File(internalDirectory, targetPath).Exists()); + } + return IOConnectionInfo.FromPath(new File(internalDirectory, targetPath).CanonicalPath); + } + + public static IOConnectionInfo ImportFileToInternalDirectory(IOConnectionInfo sourceIoc, Context ctx, IKp2aApp app) + { + var targetIoc = GetInternalIoc(sourceIoc, ctx); + + + IoUtil.Copy(targetIoc, sourceIoc, app); + return targetIoc; + } + + public static string GetIocPrefKey(IOConnectionInfo ioc, string suffix) + { + var iocAsHexString = IocAsHexString(ioc); + + return "kp2a_ioc_key_" + iocAsHexString + suffix; + } + + + public static string IocAsHexString(IOConnectionInfo ioc) + { + SHA256Managed sha256 = new SHA256Managed(); + string iocAsHexString = + MemUtil.ByteArrayToHexString(sha256.ComputeHash(Encoding.Unicode.GetBytes(ioc.Path.ToCharArray()))); + return iocAsHexString; + } + } } diff --git a/src/Kp2aBusinessLogic/UiStringKey.cs b/src/Kp2aBusinessLogic/UiStringKey.cs index b80896b5..3f8ab026 100644 --- a/src/Kp2aBusinessLogic/UiStringKey.cs +++ b/src/Kp2aBusinessLogic/UiStringKey.cs @@ -85,6 +85,7 @@ namespace keepass2android AskAddTemplatesMessage, ReadOnlyReason_PreKitKat, ReadOnlyReason_ReadOnlyFlag, - ReadOnlyReason_ReadOnlyKitKat + ReadOnlyReason_ReadOnlyKitKat, + ReadOnlyReason_LocalBackup } } diff --git a/src/Kp2aBusinessLogic/database/Database.cs b/src/Kp2aBusinessLogic/database/Database.cs index c236a806..7d09bf39 100644 --- a/src/Kp2aBusinessLogic/database/Database.cs +++ b/src/Kp2aBusinessLogic/database/Database.cs @@ -152,27 +152,20 @@ namespace keepass2android set { _databaseFormat = value; } } - public static string GetFingerprintPrefKey(IOConnectionInfo ioc) - { - var iocAsHexString = IocAsHexString(ioc); + public string IocAsHexString() + { + return IoUtil.IocAsHexString(Ioc); + } - return "kp2a_ioc_" + iocAsHexString; - } + public static string GetFingerprintPrefKey(IOConnectionInfo ioc) + { + var iocAsHexString = IoUtil.IocAsHexString(ioc); - public string IocAsHexString() - { - return IocAsHexString(Ioc); - } + return "kp2a_ioc_" + iocAsHexString; + } - private static string IocAsHexString(IOConnectionInfo ioc) - { - SHA256Managed sha256 = new SHA256Managed(); - string iocAsHexString = - MemUtil.ByteArrayToHexString(sha256.ComputeHash(Encoding.Unicode.GetBytes(ioc.Path.ToCharArray()))); - return iocAsHexString; - } - public static string GetFingerprintModePrefKey(IOConnectionInfo ioc) + public static string GetFingerprintModePrefKey(IOConnectionInfo ioc) { return GetFingerprintPrefKey(ioc) + "_mode"; } diff --git a/src/keepass2android/PasswordActivity.cs b/src/keepass2android/PasswordActivity.cs index 5fb93842..ea968f18 100644 --- a/src/keepass2android/PasswordActivity.cs +++ b/src/keepass2android/PasswordActivity.cs @@ -310,7 +310,7 @@ namespace keepass2android // to retry with typing the full password, but that's intended to avoid showing the password to a // a potentially unauthorized user (feature request https://keepass2android.codeplex.com/workitem/274) Handler handler = new Handler(); - OnFinish onFinish = new AfterLoad(handler, this); + OnFinish onFinish = new AfterLoad(handler, this, _ioConnection); _performingLoad = true; LoadDb task = new LoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, _keyFileOrProvider, onFinish); _loadDbFileTask = null; // prevent accidental re-use @@ -1468,7 +1468,7 @@ namespace keepass2android MakePasswordMaskedOrVisible(); Handler handler = new Handler(); - OnFinish onFinish = new AfterLoad(handler, this); + OnFinish onFinish = new AfterLoad(handler, this, _ioConnection); LoadDb task = (KeyProviderType == KeyProviders.Otp) ? new SaveOtpAuxFileAndLoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, _keyFileOrProvider, onFinish, this) @@ -2045,11 +2045,13 @@ namespace keepass2android private class AfterLoad : OnFinish { readonly PasswordActivity _act; + private readonly IOConnectionInfo _ioConnection; - public AfterLoad(Handler handler, PasswordActivity act):base(handler) - { - _act = act; - } + public AfterLoad(Handler handler, PasswordActivity act, IOConnectionInfo ioConnection):base(handler) + { + _act = act; + _ioConnection = ioConnection; + } public override void Run() @@ -2060,41 +2062,63 @@ namespace keepass2android _act.ClearEnteredPassword(); _act.BroadcastOpenDatabase(); _act.InvalidCompositeKeyCount = 0; + _act.LoadingErrorCount = 0; - GC.Collect(); // Ensure temporary memory used while loading is collected + GC.Collect(); // Ensure temporary memory used while loading is collected } + if (Exception != null) + { + _act.LoadingErrorCount++; + } - if (Exception is InvalidCompositeKeyException) - { - _act.InvalidCompositeKeyCount++; - if (_act.UsedFingerprintUnlock) - { - //disable fingerprint unlock if master password changed - _act.ClearFingerprintUnlockData(); - _act.InitFingerprintUnlock(); + if ((Exception != null) && (Exception.Message == KeePassLib.Resources.KLRes.FileCorrupted)) + { + Message = _act.GetString(Resource.String.CorruptDatabaseHelp); + } - Message = _act.GetString(Resource.String.fingerprint_disabled_wrong_masterkey) + " " + _act.GetString(Resource.String.fingerprint_reenable2); - } - else - { - if (_act.InvalidCompositeKeyCount > 1) - { - Message = _act.GetString(Resource.String.RepeatedInvalidCompositeKeyHelp); - } - else - { - Message = _act.GetString(Resource.String.FirstInvalidCompositeKeyError); - } - } - + if (Exception is InvalidCompositeKeyException) + { + _act.InvalidCompositeKeyCount++; + if (_act.UsedFingerprintUnlock) + { + //disable fingerprint unlock if master password changed + _act.ClearFingerprintUnlockData(); + _act.InitFingerprintUnlock(); - } - if ((Exception != null) && (Exception.Message == KeePassLib.Resources.KLRes.FileCorrupted)) - { - Message = _act.GetString(Resource.String.CorruptDatabaseHelp); - } + Message = _act.GetString(Resource.String.fingerprint_disabled_wrong_masterkey) + " " + + _act.GetString(Resource.String.fingerprint_reenable2); + } + else + { + if (_act.InvalidCompositeKeyCount > 1) + { + Message = _act.GetString(Resource.String.RepeatedInvalidCompositeKeyHelp); + if (_act._prefs.GetBoolean(IoUtil.GetIocPrefKey(_ioConnection, "has_local_backup"), false)) + { + Java.Lang.Object changeDb = _act.GetString(Resource.String.menu_change_db); + Message += _act.GetString(Resource.String.HintLocalBackupInvalidCompositeKey, new Java.Lang.Object[] {changeDb}); + } + } + else + { + Message = _act.GetString(Resource.String.FirstInvalidCompositeKeyError); + } + } + + + } + else if (_act.LoadingErrorCount > 1) + { + if (_act._prefs.GetBoolean(IoUtil.GetIocPrefKey(_ioConnection, "has_local_backup"), false)) + { + Java.Lang.Object changeDb = _act.GetString(Resource.String.menu_change_db); + Message += _act.GetString(Resource.String.HintLocalBackupOtherError, new Java.Lang.Object[] { changeDb }); + } + + } + if ((Message != null) && (Message.Length > 150)) //show long messages as dialog @@ -2135,8 +2159,12 @@ namespace keepass2android { get; set; } + public int LoadingErrorCount + { + get; set; + } - private void BroadcastOpenDatabase() + private void BroadcastOpenDatabase() { App.Kp2a.BroadcastDatabaseAction(this, Strings.ActionOpenDatabase); } diff --git a/src/keepass2android/Resources/values/config.xml b/src/keepass2android/Resources/values/config.xml index b3ba7a6f..6e333614 100644 --- a/src/keepass2android/Resources/values/config.xml +++ b/src/keepass2android/Resources/values/config.xml @@ -119,6 +119,7 @@ NoDonationReminder UseOfflineCache + CreateBackups_key AcceptAllServerCertificates CheckForFileChangesOnSave CheckForDuplicateUuids_key diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index 8c3ca733..09acef7a 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -360,7 +360,14 @@ Database caching Keep a copy of remote database files in the application cache directory. This allows to use remote databases even when offline. - SSL certificates + Local backups + Create a local backup copy after successfully loading a database. + Updating local backup... + Local backup of %1$s + + + + SSL certificates Define the behavior when certificate validation fails. Note: you can install certificates on your device if validation fails! @@ -636,6 +643,7 @@ It seems like you opened the file from an external app. This way does not support writing. If you want to make changes to the database, please close the database and select Change database. Then open the file from one of the available options if possible. The read-only flag is set. Remove this flag if you want to make changes to the database. Writing is not possible because of restrictions introduced in Android KitKat. If you want to make changes to the database, close the database and select Change database. Then open the file using System file picker. + Local backups cannot be modified. You can use Database settings - Export database to export this backup to another location from which you can re-open it. It will then be writable again. Add icon from file... @@ -685,6 +693,15 @@ • Make sure you have selected the correct database file. + + + \n + • Hint: If you think your database file might be corrupt or you do not remember the master key after modifying it, you can try with the last successfully opened file version by clicking "%1$s" and selecting the local backup. + + + \n + • Hint: Keepass2Android has stored the last successfully opened file version in internal memory. You can open it by clicking "%1$s" and selecting the local backup. + File is corrupted. \n diff --git a/src/keepass2android/Resources/xml/preferences.xml b/src/keepass2android/Resources/xml/preferences.xml index 8d81b653..91dd9d9e 100644 --- a/src/keepass2android/Resources/xml/preferences.xml +++ b/src/keepass2android/Resources/xml/preferences.xml @@ -462,6 +462,15 @@ android:defaultValue="true" android:title="@string/UseOfflineCache_title" android:key="@string/UseOfflineCache_key" /> + + + +[]/'"; + foreach (char c in charsToRemove) + { + targetPath = targetPath.Replace(c.ToString(), string.Empty); + } + if (targetPath == "") + targetPath = "local_backup"; + + var targetIoc = IOConnectionInfo.FromPath(new Java.IO.File(internalDirectory, targetPath).CanonicalPath); + + using (var transaction = new LocalFileStorage(App.Kp2a).OpenWriteTransaction(targetIoc, false)) + { + var file = transaction.OpenFile(); + backupCopy.CopyTo(file); + transaction.CommitWrite(); + } + Java.Lang.Object baseIocDisplayName = baseDisplayName; + + string keyfile = App.Kp2a.FileDbHelper.GetKeyFileForFile(ioConnectionInfo.Path); + App.Kp2a.StoreOpenedFileAsRecent(targetIoc, keyfile, Application.Context. + GetString(Resource.String.LocalBackupOf, new Java.Lang.Object[]{baseIocDisplayName})); + + prefs.Edit() + .PutBoolean(IoUtil.GetIocPrefKey(ioConnectionInfo, "has_local_backup"), true) + .PutBoolean(IoUtil.GetIocPrefKey(targetIoc, "is_local_backup"), true) + .Commit(); + + + } + else + { + prefs.Edit() + .PutBoolean(IoUtil.GetIocPrefKey(ioConnectionInfo, "has_local_backup"), false) //there might be an older local backup, but we won't "advertise" this anymore + .Commit(); + } + + UpdateOngoingNotification(); } internal void UnlockDatabase() @@ -301,9 +360,9 @@ namespace keepass2android dialog.Show(); } - public void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile) + public void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile, string displayName = "") { - FileDbHelper.CreateFile(ioc, keyfile); + FileDbHelper.CreateFile(ioc, keyfile, displayName); } public string GetResourceString(UiStringKey key) diff --git a/src/keepass2android/fileselect/FileDbHelper.cs b/src/keepass2android/fileselect/FileDbHelper.cs index 7e01e3d5..edd5910d 100644 --- a/src/keepass2android/fileselect/FileDbHelper.cs +++ b/src/keepass2android/fileselect/FileDbHelper.cs @@ -34,13 +34,14 @@ namespace keepass2android private const String DatabaseName = "keepass2android"; private const String FileTable = "files"; - private const int DatabaseVersion = 1; + private const int DatabaseVersion = 2; private const int MaxFiles = 15; public const String KeyFileId = "_id"; public const String KeyFileFilename = "fileName"; - public const String KeyFileUsername = "username"; + public const String KeyFileDisplayname = "displayname"; + public const String KeyFileUsername = "username"; public const String KeyFilePassword = "password"; public const String KeyFileCredsavemode = "credSaveMode"; public const String KeyFileKeyfile = "keyFile"; @@ -48,12 +49,14 @@ namespace keepass2android private const String DatabaseCreate = "create table " + FileTable + " ( " + KeyFileId + " integer primary key autoincrement, " - + KeyFileFilename + " text not null, " - + KeyFileKeyfile + " text, " + + KeyFileFilename + " text not null, " + + KeyFileKeyfile + " text, " + KeyFileUsername + " text, " + KeyFilePassword + " text, " + KeyFileCredsavemode + " integer not null," - + KeyFileUpdated + " integer not null);"; + + KeyFileUpdated + " integer not null," + + KeyFileDisplayname + " text " + +");"; private readonly Context mCtx; private DatabaseHelper mDbHelper; @@ -71,7 +74,11 @@ namespace keepass2android public override void OnUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // Only one database version so far + if (oldVersion == 1) + { + db.ExecSQL("alter table " + FileTable + " add column " + KeyFileDisplayname + " text "); + } + } } @@ -94,7 +101,7 @@ namespace keepass2android mDb.Close(); } - public long CreateFile(IOConnectionInfo ioc, String keyFile) { + public long CreateFile(IOConnectionInfo ioc, string keyFile, string displayName = "") { // Check to see if this filename is already used ICursor cursor; @@ -126,6 +133,7 @@ namespace keepass2android vals.Put(KeyFileUsername, iocToStore.UserName); vals.Put(KeyFilePassword, iocToStore.Password); vals.Put(KeyFileCredsavemode, (int)iocToStore.CredSaveMode); + vals.Put(KeyFileDisplayname, displayName); result = mDb.Update(FileTable, vals, KeyFileId + " = " + id, null); @@ -138,8 +146,9 @@ namespace keepass2android vals.Put(KeyFilePassword, iocToStore.Password); vals.Put(KeyFileCredsavemode, (int)iocToStore.CredSaveMode); vals.Put(KeyFileUpdated, Java.Lang.JavaSystem.CurrentTimeMillis()); - - result = mDb.Insert(FileTable, null, vals); + vals.Put(KeyFileDisplayname, displayName); + + result = mDb.Insert(FileTable, null, vals); } // Delete all but the last X records @@ -193,7 +202,8 @@ namespace keepass2android KeyFileKeyfile, KeyFileUsername, KeyFilePassword, - KeyFileCredsavemode + KeyFileCredsavemode, + KeyFileDisplayname }; } diff --git a/src/keepass2android/fileselect/FileSelectActivity.cs b/src/keepass2android/fileselect/FileSelectActivity.cs index 25bfe5fe..1097c566 100644 --- a/src/keepass2android/fileselect/FileSelectActivity.cs +++ b/src/keepass2android/fileselect/FileSelectActivity.cs @@ -229,12 +229,21 @@ namespace keepass2android public override void BindView(View view, Context context, ICursor cursor) { + String path = cursor.GetString(1); TextView textView = view.FindViewById(Resource.Id.file_filename); IOConnectionInfo ioc = new IOConnectionInfo { Path = path }; var fileStorage = _app.GetFileStorage(ioc); - textView.Text = fileStorage.GetDisplayName(ioc); + + String displayName = cursor.GetString(6); + if (string.IsNullOrEmpty(displayName)) + { + displayName = fileStorage.GetDisplayName(ioc); + + } + + textView.Text = displayName; textView.Tag = ioc.Path; } diff --git a/src/keepass2android/settings/DatabaseSettingsActivity.cs b/src/keepass2android/settings/DatabaseSettingsActivity.cs index 2788df73..5b7f4094 100644 --- a/src/keepass2android/settings/DatabaseSettingsActivity.cs +++ b/src/keepass2android/settings/DatabaseSettingsActivity.cs @@ -865,7 +865,7 @@ namespace keepass2android { CompositeKey masterKey = App.Kp2a.GetDb().KpDatabase.MasterKey; var sourceIoc = ((KcpKeyFile)masterKey.GetUserKey(typeof(KcpKeyFile))).Ioc; - var newIoc = ImportFileToInternalDirectory(sourceIoc); + var newIoc = IoUtil.ImportFileToInternalDirectory(sourceIoc, Activity, App.Kp2a); ((KcpKeyFile)masterKey.GetUserKey(typeof(KcpKeyFile))).ResetIoc(newIoc); var keyfileString = IOConnectionInfo.SerializeToString(newIoc); App.Kp2a.StoreOpenedFileAsRecent(App.Kp2a.GetDb().Ioc, keyfileString); @@ -934,7 +934,7 @@ namespace keepass2android try { var sourceIoc = App.Kp2a.GetDb().Ioc; - var newIoc = ImportFileToInternalDirectory(sourceIoc); + var newIoc = IoUtil.ImportFileToInternalDirectory(sourceIoc, Activity, App.Kp2a); return () => { var builder = new AlertDialog.Builder(Activity); @@ -968,32 +968,6 @@ namespace keepass2android } - private IOConnectionInfo ImportFileToInternalDirectory(IOConnectionInfo sourceIoc) - { - Java.IO.File internalDirectory = IoUtil.GetInternalDirectory(Activity); - string targetPath = UrlUtil.GetFileName(sourceIoc.Path); - targetPath = targetPath.Trim("|\\?*<\":>+[]/'".ToCharArray()); - if (targetPath == "") - targetPath = "imported"; - if (new File(internalDirectory, targetPath).Exists()) - { - int c = 1; - var ext = UrlUtil.GetExtension(targetPath); - var filenameWithoutExt = UrlUtil.StripExtension(targetPath); - do - { - c++; - targetPath = filenameWithoutExt + c; - if (!String.IsNullOrEmpty(ext)) - targetPath += "." + ext; - } while (new File(internalDirectory, targetPath).Exists()); - } - var targetIoc = IOConnectionInfo.FromPath(new File(internalDirectory, targetPath).CanonicalPath); - - IoUtil.Copy(targetIoc, sourceIoc, App.Kp2a); - return targetIoc; - } - private void SetAlgorithm(Database db, Preference algorithm)