/* KeePass Password Safe - The Open-Source Password Manager Copyright (C) 2003-2012 Dominik Reichl Modified to be used with Mono for Android. Changes Copyright (C) 2013 Philipp Crocoll This program 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. This program 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 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Drawing; using KeePassLib.Collections; using KeePassLib.Cryptography; using KeePassLib.Cryptography.Cipher; using KeePassLib.Delegates; using KeePassLib.Interfaces; using KeePassLib.Keys; using KeePassLib.Resources; using KeePassLib.Security; using KeePassLib.Serialization; using KeePassLib.Utility; namespace KeePassLib { /// /// The core password manager class. It contains a number of groups, which /// contain the actual entries. /// public sealed class PwDatabase { internal const int DefaultHistoryMaxItems = 10; // -1 = unlimited internal const long DefaultHistoryMaxSize = 6 * 1024 * 1024; // -1 = unlimited private static bool m_bPrimaryCreated = false; // Initializations see Clear() private PwGroup m_pgRootGroup = null; private PwObjectList m_vDeletedObjects = new PwObjectList(); private PwUuid m_uuidDataCipher = StandardAesEngine.AesUuid; private PwCompressionAlgorithm m_caCompression = PwCompressionAlgorithm.GZip; private ulong m_uKeyEncryptionRounds = PwDefs.DefaultKeyEncryptionRounds; private CompositeKey m_pwUserKey = null; private MemoryProtectionConfig m_memProtConfig = new MemoryProtectionConfig(); private List m_vCustomIcons = new List(); private bool m_bUINeedsIconUpdate = true; private string m_strName = string.Empty; private DateTime m_dtNameChanged = PwDefs.DtDefaultNow; private string m_strDesc = string.Empty; private DateTime m_dtDescChanged = PwDefs.DtDefaultNow; private string m_strDefaultUserName = string.Empty; private DateTime m_dtDefaultUserChanged = PwDefs.DtDefaultNow; private uint m_uMntncHistoryDays = 365; private Color m_clr = Color.Empty; private DateTime m_dtKeyLastChanged = PwDefs.DtDefaultNow; private long m_lKeyChangeRecDays = -1; private long m_lKeyChangeForceDays = -1; private IOConnectionInfo m_ioSource = new IOConnectionInfo(); private bool m_bDatabaseOpened = false; private bool m_bModified = false; private PwUuid m_pwLastSelectedGroup = PwUuid.Zero; private PwUuid m_pwLastTopVisibleGroup = PwUuid.Zero; private bool m_bUseRecycleBin = true; private PwUuid m_pwRecycleBin = PwUuid.Zero; private DateTime m_dtRecycleBinChanged = PwDefs.DtDefaultNow; private PwUuid m_pwEntryTemplatesGroup = PwUuid.Zero; private DateTime m_dtEntryTemplatesChanged = PwDefs.DtDefaultNow; private int m_nHistoryMaxItems = DefaultHistoryMaxItems; private long m_lHistoryMaxSize = DefaultHistoryMaxSize; // In bytes private StringDictionaryEx m_vCustomData = new StringDictionaryEx(); private byte[] m_pbHashOfFileOnDisk = null; private byte[] m_pbHashOfLastIO = null; private bool m_bUseFileTransactions = false; private bool m_bUseFileLocks = false; private IStatusLogger m_slStatus = null; private static string m_strLocalizedAppName = string.Empty; // private const string StrBackupExtension = ".bak"; /// /// Get the root group that contains all groups and entries stored in the /// database. /// /// Root group. The return value is null, if no database /// has been opened. public PwGroup RootGroup { get { return m_pgRootGroup; } set { Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); m_pgRootGroup = value; } } /// /// IOConnection of the currently opened database file. /// Is never null. /// public IOConnectionInfo IOConnectionInfo { get { return m_ioSource; } } /// /// If this is true, a database is currently open. /// public bool IsOpen { get { return m_bDatabaseOpened; } } /// /// Modification flag. If true, the class has been modified and the /// user interface should prompt the user to save the changes before /// closing the database for example. /// public bool Modified { get { return m_bModified; } set { m_bModified = value; } } /// /// The user key used for database encryption. This key must be created /// and set before using any of the database load/save functions. /// public CompositeKey MasterKey { get { return m_pwUserKey; } set { Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); m_pwUserKey = value; } } /// /// Name of the database. /// public string Name { get { return m_strName; } set { Debug.Assert(value != null); if(value != null) m_strName = value; } } public DateTime NameChanged { get { return m_dtNameChanged; } set { m_dtNameChanged = value; } } /// /// Database description. /// public string Description { get { return m_strDesc; } set { Debug.Assert(value != null); if(value != null) m_strDesc = value; } } public DateTime DescriptionChanged { get { return m_dtDescChanged; } set { m_dtDescChanged = value; } } /// /// Default user name used for new entries. /// public string DefaultUserName { get { return m_strDefaultUserName; } set { Debug.Assert(value != null); if(value != null) m_strDefaultUserName = value; } } public DateTime DefaultUserNameChanged { get { return m_dtDefaultUserChanged; } set { m_dtDefaultUserChanged = value; } } /// /// Number of days until history entries are being deleted /// in a database maintenance operation. /// public uint MaintenanceHistoryDays { get { return m_uMntncHistoryDays; } set { m_uMntncHistoryDays = value; } } public Color Color { get { return m_clr; } set { m_clr = value; } } public DateTime MasterKeyChanged { get { return m_dtKeyLastChanged; } set { m_dtKeyLastChanged = value; } } public long MasterKeyChangeRec { get { return m_lKeyChangeRecDays; } set { m_lKeyChangeRecDays = value; } } public long MasterKeyChangeForce { get { return m_lKeyChangeForceDays; } set { m_lKeyChangeForceDays = value; } } /// /// The encryption algorithm used to encrypt the data part of the database. /// public PwUuid DataCipherUuid { get { return m_uuidDataCipher; } set { Debug.Assert(value != null); if(value != null) m_uuidDataCipher = value; } } /// /// Compression algorithm used to encrypt the data part of the database. /// public PwCompressionAlgorithm Compression { get { return m_caCompression; } set { m_caCompression = value; } } /// /// Number of key transformation rounds (in order to make dictionary /// attacks harder). /// public ulong KeyEncryptionRounds { get { return m_uKeyEncryptionRounds; } set { m_uKeyEncryptionRounds = value; } } /// /// Memory protection configuration (for default fields). /// public MemoryProtectionConfig MemoryProtection { get { return m_memProtConfig; } set { Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); m_memProtConfig = value; } } /// /// Get a list of all deleted objects. /// public PwObjectList DeletedObjects { get { return m_vDeletedObjects; } } /// /// Get all custom icons stored in this database. /// public List CustomIcons { get { return m_vCustomIcons; } } /// /// This is a dirty-flag for the UI. It is used to indicate when an /// icon list update is required. /// public bool UINeedsIconUpdate { get { return m_bUINeedsIconUpdate; } set { m_bUINeedsIconUpdate = value; } } public PwUuid LastSelectedGroup { get { return m_pwLastSelectedGroup; } set { Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); m_pwLastSelectedGroup = value; } } public PwUuid LastTopVisibleGroup { get { return m_pwLastTopVisibleGroup; } set { Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); m_pwLastTopVisibleGroup = value; } } public bool RecycleBinEnabled { get { return m_bUseRecycleBin; } set { m_bUseRecycleBin = value; } } public PwUuid RecycleBinUuid { get { return m_pwRecycleBin; } set { Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); m_pwRecycleBin = value; } } public DateTime RecycleBinChanged { get { return m_dtRecycleBinChanged; } set { m_dtRecycleBinChanged = value; } } /// /// UUID of the group containing template entries. May be /// PwUuid.Zero, if no entry templates group has been specified. /// public PwUuid EntryTemplatesGroup { get { return m_pwEntryTemplatesGroup; } set { Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); m_pwEntryTemplatesGroup = value; } } public DateTime EntryTemplatesGroupChanged { get { return m_dtEntryTemplatesChanged; } set { m_dtEntryTemplatesChanged = value; } } public int HistoryMaxItems { get { return m_nHistoryMaxItems; } set { m_nHistoryMaxItems = value; } } public long HistoryMaxSize { get { return m_lHistoryMaxSize; } set { m_lHistoryMaxSize = value; } } /// /// Custom data container that can be used by plugins to store /// own data in KeePass databases. /// public StringDictionaryEx CustomData { get { return m_vCustomData; } set { Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); m_vCustomData = value; } } /// /// Hash value of the primary file on disk (last read or last write). /// A call to SaveAs without making the saved file primary will /// not change this hash. May be null. /// public byte[] HashOfFileOnDisk { get { return m_pbHashOfFileOnDisk; } } public byte[] HashOfLastIO { get { return m_pbHashOfLastIO; } } public bool UseFileTransactions { get { return m_bUseFileTransactions; } set { m_bUseFileTransactions = value; } } public bool UseFileLocks { get { return m_bUseFileLocks; } set { m_bUseFileLocks = value; } } private string m_strDetachBins = null; /// /// Detach binaries when opening a file. If this isn't null, /// all binaries are saved to the specified path and are removed /// from the database. /// public string DetachBinaries { get { return m_strDetachBins; } set { m_strDetachBins = value; } } /// /// Localized application name. /// public static string LocalizedAppName { get { return m_strLocalizedAppName; } set { Debug.Assert(value != null); m_strLocalizedAppName = value; } } /// /// Constructs an empty password manager object. /// public PwDatabase() { if(m_bPrimaryCreated == false) m_bPrimaryCreated = true; Clear(); } private void Clear() { m_pgRootGroup = null; m_vDeletedObjects = new PwObjectList(); m_uuidDataCipher = StandardAesEngine.AesUuid; m_caCompression = PwCompressionAlgorithm.GZip; m_uKeyEncryptionRounds = PwDefs.DefaultKeyEncryptionRounds; m_pwUserKey = null; m_memProtConfig = new MemoryProtectionConfig(); m_vCustomIcons = new List(); m_bUINeedsIconUpdate = true; DateTime dtNow = DateTime.Now; m_strName = string.Empty; m_dtNameChanged = dtNow; m_strDesc = string.Empty; m_dtDescChanged = dtNow; m_strDefaultUserName = string.Empty; m_dtDefaultUserChanged = dtNow; m_uMntncHistoryDays = 365; m_clr = Color.Empty; m_dtKeyLastChanged = dtNow; m_lKeyChangeRecDays = -1; m_lKeyChangeForceDays = -1; m_ioSource = new IOConnectionInfo(); m_bDatabaseOpened = false; m_bModified = false; m_pwLastSelectedGroup = PwUuid.Zero; m_pwLastTopVisibleGroup = PwUuid.Zero; m_bUseRecycleBin = true; m_pwRecycleBin = PwUuid.Zero; m_dtRecycleBinChanged = dtNow; m_pwEntryTemplatesGroup = PwUuid.Zero; m_dtEntryTemplatesChanged = dtNow; m_nHistoryMaxItems = DefaultHistoryMaxItems; m_lHistoryMaxSize = DefaultHistoryMaxSize; m_vCustomData = new StringDictionaryEx(); m_pbHashOfFileOnDisk = null; m_pbHashOfLastIO = null; m_bUseFileTransactions = false; m_bUseFileLocks = false; } /// /// Initialize the class for managing a new database. Previously loaded /// data is deleted. /// /// IO connection of the new database. /// Key to open the database. public void New(IOConnectionInfo ioConnection, CompositeKey pwKey) { Debug.Assert(ioConnection != null); if(ioConnection == null) throw new ArgumentNullException("ioConnection"); Debug.Assert(pwKey != null); if(pwKey == null) throw new ArgumentNullException("pwKey"); Close(); m_ioSource = ioConnection; m_pwUserKey = pwKey; m_bDatabaseOpened = true; m_bModified = true; m_pgRootGroup = new PwGroup(true, true, UrlUtil.StripExtension(UrlUtil.GetFileName(ioConnection.Path)), PwIcon.FolderOpen); m_pgRootGroup.IsExpanded = true; } /// /// Open a database. The URL may point to any supported data source. /// /// IO connection to load the database from. /// Key used to open the specified database. /// Logger, which gets all status messages. public void Open(IOConnectionInfo ioSource, CompositeKey pwKey, IStatusLogger slLogger) { Debug.Assert(ioSource != null); if(ioSource == null) throw new ArgumentNullException("ioSource"); Debug.Assert(pwKey != null); if(pwKey == null) throw new ArgumentNullException("pwKey"); Close(); try { m_pgRootGroup = new PwGroup(true, true, UrlUtil.StripExtension( UrlUtil.GetFileName(ioSource.Path)), PwIcon.FolderOpen); m_pgRootGroup.IsExpanded = true; m_pwUserKey = pwKey; m_bModified = false; KdbxFile kdbx = new KdbxFile(this); kdbx.DetachBinaries = m_strDetachBins; Stream s = IOConnection.OpenRead(ioSource); kdbx.Load(s, KdbxFormat.Default, slLogger); s.Close(); m_pbHashOfLastIO = kdbx.HashOfFileOnDisk; m_pbHashOfFileOnDisk = kdbx.HashOfFileOnDisk; Debug.Assert(m_pbHashOfFileOnDisk != null); m_bDatabaseOpened = true; m_ioSource = ioSource; } catch(Exception) { Clear(); throw; } } /// /// Save the currently opened database. The file is written to the location /// it has been opened from. /// /// Logger that recieves status information. public void Save(IStatusLogger slLogger) { Debug.Assert(ValidateUuidUniqueness()); FileLock fl = null; if(m_bUseFileLocks) fl = new FileLock(m_ioSource); try { FileTransactionEx ft = new FileTransactionEx(m_ioSource, m_bUseFileTransactions); Stream s = ft.OpenWrite(); KdbxFile kdb = new KdbxFile(this); kdb.Save(s, null, KdbxFormat.Default, slLogger); ft.CommitWrite(); m_pbHashOfLastIO = kdb.HashOfFileOnDisk; m_pbHashOfFileOnDisk = kdb.HashOfFileOnDisk; Debug.Assert(m_pbHashOfFileOnDisk != null); } finally { if(fl != null) fl.Dispose(); } m_bModified = false; } /// /// Save the currently opened database to a different location. If /// is true, the specified /// location is made the default location for future saves /// using SaveDatabase. /// /// New location to serialize the database to. /// If true, the new location is made the /// standard location for the database. If false, a copy of the currently /// opened database is saved to the specified location, but it isn't /// made the default location (i.e. no lock files will be moved for /// example). /// Logger that recieves status information. public void SaveAs(IOConnectionInfo ioConnection, bool bIsPrimaryNow, IStatusLogger slLogger) { Debug.Assert(ioConnection != null); if(ioConnection == null) throw new ArgumentNullException("ioConnection"); IOConnectionInfo ioCurrent = m_ioSource; // Remember current m_ioSource = ioConnection; byte[] pbHashCopy = m_pbHashOfFileOnDisk; try { this.Save(slLogger); } catch(Exception) { m_ioSource = ioCurrent; // Restore m_pbHashOfFileOnDisk = pbHashCopy; m_pbHashOfLastIO = null; throw; } if(!bIsPrimaryNow) { m_ioSource = ioCurrent; // Restore m_pbHashOfFileOnDisk = pbHashCopy; } } /// /// Closes the currently opened database. No confirmation message is shown /// before closing. Unsaved changes will be lost. /// public void Close() { Clear(); } public void MergeIn(PwDatabase pwSource, PwMergeMethod mm) { MergeIn(pwSource, mm, null); } /// /// Synchronize the current database with another one. /// /// Input database to synchronize with. This input /// database is used to update the current one, but is not modified! You /// must copy the current object if you want a second instance of the /// synchronized database. The input database must not be seen as valid /// database any more after calling Synchronize. /// Merge method. /// Logger to report status messages to. /// May be null. public void MergeIn(PwDatabase pwSource, PwMergeMethod mm, IStatusLogger slStatus) { if(pwSource == null) throw new ArgumentNullException("pwSource"); PwGroup pgOrgStructure = m_pgRootGroup.CloneStructure(); PwGroup pgSrcStructure = pwSource.m_pgRootGroup.CloneStructure(); if(mm == PwMergeMethod.CreateNewUuids) pwSource.RootGroup.CreateNewItemUuids(true, true, true); GroupHandler gh = delegate(PwGroup pg) { if(pg == pwSource.m_pgRootGroup) return true; PwGroup pgLocal = m_pgRootGroup.FindGroup(pg.Uuid, true); if(pgLocal == null) { PwGroup pgSourceParent = pg.ParentGroup; PwGroup pgLocalContainer; if(pgSourceParent == pwSource.m_pgRootGroup) pgLocalContainer = m_pgRootGroup; else pgLocalContainer = m_pgRootGroup.FindGroup(pgSourceParent.Uuid, true); Debug.Assert(pgLocalContainer != null); if(pgLocalContainer == null) pgLocalContainer = m_pgRootGroup; PwGroup pgNew = new PwGroup(false, false); pgNew.Uuid = pg.Uuid; pgNew.AssignProperties(pg, false, true); pgLocalContainer.AddGroup(pgNew, true); } else // pgLocal != null { Debug.Assert(mm != PwMergeMethod.CreateNewUuids); if(mm == PwMergeMethod.OverwriteExisting) pgLocal.AssignProperties(pg, false, false); else if((mm == PwMergeMethod.OverwriteIfNewer) || (mm == PwMergeMethod.Synchronize)) { pgLocal.AssignProperties(pg, true, false); } // else if(mm == PwMergeMethod.KeepExisting) ... } return ((slStatus != null) ? slStatus.ContinueWork() : true); }; EntryHandler eh = delegate(PwEntry pe) { PwEntry peLocal = m_pgRootGroup.FindEntry(pe.Uuid, true); if(peLocal == null) { PwGroup pgSourceParent = pe.ParentGroup; PwGroup pgLocalContainer; if(pgSourceParent == pwSource.m_pgRootGroup) pgLocalContainer = m_pgRootGroup; else pgLocalContainer = m_pgRootGroup.FindGroup(pgSourceParent.Uuid, true); Debug.Assert(pgLocalContainer != null); if(pgLocalContainer == null) pgLocalContainer = m_pgRootGroup; PwEntry peNew = new PwEntry(false, false); peNew.Uuid = pe.Uuid; peNew.AssignProperties(pe, false, true, true); pgLocalContainer.AddEntry(peNew, true); } else // peLocal != null { Debug.Assert(mm != PwMergeMethod.CreateNewUuids); const PwCompareOptions cmpOpt = (PwCompareOptions.IgnoreParentGroup | PwCompareOptions.IgnoreLastAccess | PwCompareOptions.IgnoreHistory | PwCompareOptions.NullEmptyEquivStd); bool bEquals = peLocal.EqualsEntry(pe, cmpOpt, MemProtCmpMode.None); bool bOrgBackup = !bEquals; if(mm != PwMergeMethod.OverwriteExisting) bOrgBackup &= (pe.LastModificationTime > peLocal.LastModificationTime); bOrgBackup &= !pe.HasBackupOfData(peLocal, false, true); if(bOrgBackup) peLocal.CreateBackup(null); // Maintain at end bool bSrcBackup = !bEquals && (mm != PwMergeMethod.OverwriteExisting); bSrcBackup &= (peLocal.LastModificationTime > pe.LastModificationTime); bSrcBackup &= !peLocal.HasBackupOfData(pe, false, true); if(bSrcBackup) pe.CreateBackup(null); // Maintain at end if(mm == PwMergeMethod.OverwriteExisting) peLocal.AssignProperties(pe, false, false, false); else if((mm == PwMergeMethod.OverwriteIfNewer) || (mm == PwMergeMethod.Synchronize)) { peLocal.AssignProperties(pe, true, false, false); } // else if(mm == PwMergeMethod.KeepExisting) ... MergeEntryHistory(peLocal, pe, mm); } return ((slStatus != null) ? slStatus.ContinueWork() : true); }; if(!pwSource.RootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh)) throw new InvalidOperationException(); IStatusLogger slPrevStatus = m_slStatus; m_slStatus = slStatus; if(mm == PwMergeMethod.Synchronize) { ApplyDeletions(pwSource.m_vDeletedObjects, true); ApplyDeletions(m_vDeletedObjects, false); PwObjectPool ppOrgGroups = PwObjectPool.FromGroupRecursive( pgOrgStructure, false); PwObjectPool ppSrcGroups = PwObjectPool.FromGroupRecursive( pgSrcStructure, false); PwObjectPool ppOrgEntries = PwObjectPool.FromGroupRecursive( pgOrgStructure, true); PwObjectPool ppSrcEntries = PwObjectPool.FromGroupRecursive( pgSrcStructure, true); RelocateGroups(ppOrgGroups, ppSrcGroups); ReorderGroups(ppOrgGroups, ppSrcGroups); RelocateEntries(ppOrgEntries, ppSrcEntries); ReorderEntries(ppOrgEntries, ppSrcEntries); Debug.Assert(ValidateUuidUniqueness()); } // Must be called *after* merging groups, because group UUIDs // are required for recycle bin and entry template UUIDs MergeInDbProperties(pwSource, mm); MergeInCustomIcons(pwSource); MaintainBackups(); m_slStatus = slPrevStatus; } private void MergeInCustomIcons(PwDatabase pwSource) { foreach(PwCustomIcon pwci in pwSource.CustomIcons) { if(GetCustomIconIndex(pwci.Uuid) >= 0) continue; m_vCustomIcons.Add(pwci); // PwCustomIcon is immutable m_bUINeedsIconUpdate = true; } } /// /// Apply a list of deleted objects. /// /// List of deleted objects. private void ApplyDeletions(PwObjectList listDelObjects, bool bCopyDeletionInfoToLocal) { Debug.Assert(listDelObjects != null); if(listDelObjects == null) throw new ArgumentNullException("listDelObjects"); LinkedList listGroupsToDelete = new LinkedList(); LinkedList listEntriesToDelete = new LinkedList(); GroupHandler gh = delegate(PwGroup pg) { if(pg == m_pgRootGroup) return true; foreach(PwDeletedObject pdo in listDelObjects) { if(pg.Uuid.EqualsValue(pdo.Uuid)) if(pg.LastModificationTime < pdo.DeletionTime) listGroupsToDelete.AddLast(pg); } return ((m_slStatus != null) ? m_slStatus.ContinueWork() : true); }; EntryHandler eh = delegate(PwEntry pe) { foreach(PwDeletedObject pdo in listDelObjects) { if(pe.Uuid.EqualsValue(pdo.Uuid)) if(pe.LastModificationTime < pdo.DeletionTime) listEntriesToDelete.AddLast(pe); } return ((m_slStatus != null) ? m_slStatus.ContinueWork() : true); }; m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh); foreach(PwGroup pg in listGroupsToDelete) pg.ParentGroup.Groups.Remove(pg); foreach(PwEntry pe in listEntriesToDelete) pe.ParentGroup.Entries.Remove(pe); if(bCopyDeletionInfoToLocal) { foreach(PwDeletedObject pdoNew in listDelObjects) { bool bCopy = true; foreach(PwDeletedObject pdoLocal in m_vDeletedObjects) { if(pdoNew.Uuid.EqualsValue(pdoLocal.Uuid)) { bCopy = false; if(pdoNew.DeletionTime > pdoLocal.DeletionTime) pdoLocal.DeletionTime = pdoNew.DeletionTime; break; } } if(bCopy) m_vDeletedObjects.Add(pdoNew); } } } private void RelocateGroups(PwObjectPool ppOrgStructure, PwObjectPool ppSrcStructure) { PwObjectList vGroups = m_pgRootGroup.GetGroups(true); foreach(PwGroup pg in vGroups) { if((m_slStatus != null) && !m_slStatus.ContinueWork()) break; // PwGroup pgOrg = pgOrgStructure.FindGroup(pg.Uuid, true); IStructureItem ptOrg = ppOrgStructure.Get(pg.Uuid); if(ptOrg == null) continue; // PwGroup pgSrc = pgSrcStructure.FindGroup(pg.Uuid, true); IStructureItem ptSrc = ppSrcStructure.Get(pg.Uuid); if(ptSrc == null) continue; PwGroup pgOrgParent = ptOrg.ParentGroup; PwGroup pgSrcParent = ptSrc.ParentGroup; if(pgOrgParent.Uuid.EqualsValue(pgSrcParent.Uuid)) { pg.LocationChanged = ((ptSrc.LocationChanged > ptOrg.LocationChanged) ? ptSrc.LocationChanged : ptOrg.LocationChanged); continue; } if(ptSrc.LocationChanged > ptOrg.LocationChanged) { PwGroup pgLocal = m_pgRootGroup.FindGroup(pgSrcParent.Uuid, true); if(pgLocal == null) { Debug.Assert(false); continue; } if(pgLocal.IsContainedIn(pg)) continue; pg.ParentGroup.Groups.Remove(pg); pgLocal.AddGroup(pg, true); pg.LocationChanged = ptSrc.LocationChanged; } else { Debug.Assert(pg.ParentGroup.Uuid.EqualsValue(pgOrgParent.Uuid)); Debug.Assert(pg.LocationChanged == ptOrg.LocationChanged); } } Debug.Assert(m_pgRootGroup.GetGroups(true).UCount == vGroups.UCount); } private void RelocateEntries(PwObjectPool ppOrgStructure, PwObjectPool ppSrcStructure) { PwObjectList vEntries = m_pgRootGroup.GetEntries(true); foreach(PwEntry pe in vEntries) { if((m_slStatus != null) && !m_slStatus.ContinueWork()) break; // PwEntry peOrg = pgOrgStructure.FindEntry(pe.Uuid, true); IStructureItem ptOrg = ppOrgStructure.Get(pe.Uuid); if(ptOrg == null) continue; // PwEntry peSrc = pgSrcStructure.FindEntry(pe.Uuid, true); IStructureItem ptSrc = ppSrcStructure.Get(pe.Uuid); if(ptSrc == null) continue; PwGroup pgOrg = ptOrg.ParentGroup; PwGroup pgSrc = ptSrc.ParentGroup; if(pgOrg.Uuid.EqualsValue(pgSrc.Uuid)) { pe.LocationChanged = ((ptSrc.LocationChanged > ptOrg.LocationChanged) ? ptSrc.LocationChanged : ptOrg.LocationChanged); continue; } if(ptSrc.LocationChanged > ptOrg.LocationChanged) { PwGroup pgLocal = m_pgRootGroup.FindGroup(pgSrc.Uuid, true); if(pgLocal == null) { Debug.Assert(false); continue; } pe.ParentGroup.Entries.Remove(pe); pgLocal.AddEntry(pe, true); pe.LocationChanged = ptSrc.LocationChanged; } else { Debug.Assert(pe.ParentGroup.Uuid.EqualsValue(pgOrg.Uuid)); Debug.Assert(pe.LocationChanged == ptOrg.LocationChanged); } } Debug.Assert(m_pgRootGroup.GetEntries(true).UCount == vEntries.UCount); } private void ReorderGroups(PwObjectPool ppOrgStructure, PwObjectPool ppSrcStructure) { GroupHandler gh = delegate(PwGroup pg) { ReorderObjectList(pg.Groups, ppOrgStructure, ppSrcStructure, false); return true; }; ReorderObjectList(m_pgRootGroup.Groups, ppOrgStructure, ppSrcStructure, false); m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, gh, null); } private void ReorderEntries(PwObjectPool ppOrgStructure, PwObjectPool ppSrcStructure) { GroupHandler gh = delegate(PwGroup pg) { ReorderObjectList(pg.Entries, ppOrgStructure, ppSrcStructure, true); return true; }; ReorderObjectList(m_pgRootGroup.Entries, ppOrgStructure, ppSrcStructure, true); m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, gh, null); } private void ReorderObjectList(PwObjectList vItems, PwObjectPool ppOrgStructure, PwObjectPool ppSrcStructure, bool bEntries) where T : class, IStructureItem, IDeepCloneable { if(!ObjectListRequiresReorder(vItems, ppOrgStructure, ppSrcStructure, bEntries)) return; #if DEBUG PwObjectList vOrgListItems = vItems.CloneShallow(); #endif Queue> qToDo = new Queue>(); qToDo.Enqueue(new KeyValuePair(0, vItems.UCount - 1)); while(qToDo.Count > 0) { if((m_slStatus != null) && !m_slStatus.ContinueWork()) break; KeyValuePair kvp = qToDo.Dequeue(); if(kvp.Value <= kvp.Key) { Debug.Assert(false); continue; } Queue qRelBefore = new Queue(); Queue qRelAfter = new Queue(); uint uPivot = FindLocationChangedPivot(vItems, kvp, ppOrgStructure, ppSrcStructure, qRelBefore, qRelAfter, bEntries); T ptPivot = vItems.GetAt(uPivot); List vToSort = vItems.GetRange(kvp.Key, kvp.Value); Queue qBefore = new Queue(); Queue qAfter = new Queue(); bool bBefore = true; foreach(T pt in vToSort) { if(pt == ptPivot) { bBefore = false; continue; } bool bAdded = false; foreach(PwUuid puBefore in qRelBefore) { if(puBefore.EqualsValue(pt.Uuid)) { qBefore.Enqueue(pt); bAdded = true; break; } } if(bAdded) continue; foreach(PwUuid puAfter in qRelAfter) { if(puAfter.EqualsValue(pt.Uuid)) { qAfter.Enqueue(pt); bAdded = true; break; } } if(bAdded) continue; if(bBefore) qBefore.Enqueue(pt); else qAfter.Enqueue(pt); } Debug.Assert(bBefore == false); uint uPos = kvp.Key; while(qBefore.Count > 0) vItems.SetAt(uPos++, qBefore.Dequeue()); vItems.SetAt(uPos++, ptPivot); while(qAfter.Count > 0) vItems.SetAt(uPos++, qAfter.Dequeue()); Debug.Assert(uPos == (kvp.Value + 1)); int iNewPivot = vItems.IndexOf(ptPivot); if((iNewPivot < (int)kvp.Key) || (iNewPivot > (int)kvp.Value)) { Debug.Assert(false); continue; } if((iNewPivot - 1) > (int)kvp.Key) qToDo.Enqueue(new KeyValuePair(kvp.Key, (uint)(iNewPivot - 1))); if((iNewPivot + 1) < (int)kvp.Value) qToDo.Enqueue(new KeyValuePair((uint)(iNewPivot + 1), kvp.Value)); } #if DEBUG foreach(T ptItem in vOrgListItems) { Debug.Assert(vItems.IndexOf(ptItem) >= 0); } #endif } private static uint FindLocationChangedPivot(PwObjectList vItems, KeyValuePair kvpRange, PwObjectPool ppOrgStructure, PwObjectPool ppSrcStructure, Queue qBefore, Queue qAfter, bool bEntries) where T : class, IStructureItem, IDeepCloneable { uint uPosMax = kvpRange.Key; DateTime dtMax = DateTime.MinValue; List vNeighborSrc = null; for(uint u = kvpRange.Key; u <= kvpRange.Value; ++u) { T pt = vItems.GetAt(u); // IStructureItem ptOrg = pgOrgStructure.FindObject(pt.Uuid, true, bEntries); IStructureItem ptOrg = ppOrgStructure.Get(pt.Uuid); if((ptOrg != null) && (ptOrg.LocationChanged > dtMax)) { uPosMax = u; dtMax = ptOrg.LocationChanged; // No 'continue' vNeighborSrc = ptOrg.ParentGroup.GetObjects(false, bEntries); } // IStructureItem ptSrc = pgSrcStructure.FindObject(pt.Uuid, true, bEntries); IStructureItem ptSrc = ppSrcStructure.Get(pt.Uuid); if((ptSrc != null) && (ptSrc.LocationChanged > dtMax)) { uPosMax = u; dtMax = ptSrc.LocationChanged; // No 'continue' vNeighborSrc = ptSrc.ParentGroup.GetObjects(false, bEntries); } } GetNeighborItems(vNeighborSrc, vItems.GetAt(uPosMax).Uuid, qBefore, qAfter); return uPosMax; } private static void GetNeighborItems(List vItems, PwUuid pwPivot, Queue qBefore, Queue qAfter) { qBefore.Clear(); qAfter.Clear(); // Checks after clearing the queues if(vItems == null) { Debug.Assert(false); return; } // No throw bool bBefore = true; for(int i = 0; i < vItems.Count; ++i) { PwUuid pw = vItems[i].Uuid; if(pw.EqualsValue(pwPivot)) bBefore = false; else if(bBefore) qBefore.Enqueue(pw); else qAfter.Enqueue(pw); } Debug.Assert(bBefore == false); } /// /// Method to check whether a reordering is required. This fast test /// allows to skip the reordering routine, resulting in a large /// performance increase. /// private bool ObjectListRequiresReorder(PwObjectList vItems, PwObjectPool ppOrgStructure, PwObjectPool ppSrcStructure, bool bEntries) where T : class, IStructureItem, IDeepCloneable { Debug.Assert(ppOrgStructure.ContainsOnlyType(bEntries ? typeof(PwEntry) : typeof(PwGroup))); Debug.Assert(ppSrcStructure.ContainsOnlyType(bEntries ? typeof(PwEntry) : typeof(PwGroup))); if(vItems.UCount <= 1) return false; if((m_slStatus != null) && !m_slStatus.ContinueWork()) return false; T ptFirst = vItems.GetAt(0); // IStructureItem ptOrg = pgOrgStructure.FindObject(ptFirst.Uuid, true, bEntries); IStructureItem ptOrg = ppOrgStructure.Get(ptFirst.Uuid); if(ptOrg == null) return true; // IStructureItem ptSrc = pgSrcStructure.FindObject(ptFirst.Uuid, true, bEntries); IStructureItem ptSrc = ppSrcStructure.Get(ptFirst.Uuid); if(ptSrc == null) return true; if(ptFirst.ParentGroup == null) { Debug.Assert(false); return true; } PwGroup pgOrgParent = ptOrg.ParentGroup; if(pgOrgParent == null) return true; // Root might be in tree PwGroup pgSrcParent = ptSrc.ParentGroup; if(pgSrcParent == null) return true; // Root might be in tree if(!ptFirst.ParentGroup.Uuid.EqualsValue(pgOrgParent.Uuid)) return true; if(!pgOrgParent.Uuid.EqualsValue(pgSrcParent.Uuid)) return true; List lOrg = pgOrgParent.GetObjects(false, bEntries); List lSrc = pgSrcParent.GetObjects(false, bEntries); if(vItems.UCount != (uint)lOrg.Count) return true; if(lOrg.Count != lSrc.Count) return true; for(uint u = 0; u < vItems.UCount; ++u) { IStructureItem pt = vItems.GetAt(u); Debug.Assert(pt.ParentGroup == ptFirst.ParentGroup); if(!pt.Uuid.EqualsValue(lOrg[(int)u].Uuid)) return true; if(!pt.Uuid.EqualsValue(lSrc[(int)u].Uuid)) return true; if(pt.LocationChanged != lOrg[(int)u].LocationChanged) return true; if(pt.LocationChanged != lSrc[(int)u].LocationChanged) return true; } return false; } private void MergeInDbProperties(PwDatabase pwSource, PwMergeMethod mm) { if(pwSource == null) { Debug.Assert(false); return; } if((mm == PwMergeMethod.KeepExisting) || (mm == PwMergeMethod.None)) return; bool bForce = (mm == PwMergeMethod.OverwriteExisting); if(bForce || (pwSource.m_dtNameChanged > m_dtNameChanged)) { m_strName = pwSource.m_strName; m_dtNameChanged = pwSource.m_dtNameChanged; } if(bForce || (pwSource.m_dtDescChanged > m_dtDescChanged)) { m_strDesc = pwSource.m_strDesc; m_dtDescChanged = pwSource.m_dtDescChanged; } if(bForce || (pwSource.m_dtDefaultUserChanged > m_dtDefaultUserChanged)) { m_strDefaultUserName = pwSource.m_strDefaultUserName; m_dtDefaultUserChanged = pwSource.m_dtDefaultUserChanged; } if(bForce) m_clr = pwSource.m_clr; PwUuid pwPrefBin = m_pwRecycleBin, pwAltBin = pwSource.m_pwRecycleBin; if(bForce || (pwSource.m_dtRecycleBinChanged > m_dtRecycleBinChanged)) { pwPrefBin = pwSource.m_pwRecycleBin; pwAltBin = m_pwRecycleBin; m_bUseRecycleBin = pwSource.m_bUseRecycleBin; m_dtRecycleBinChanged = pwSource.m_dtRecycleBinChanged; } if(m_pgRootGroup.FindGroup(pwPrefBin, true) != null) m_pwRecycleBin = pwPrefBin; else if(m_pgRootGroup.FindGroup(pwAltBin, true) != null) m_pwRecycleBin = pwAltBin; else m_pwRecycleBin = PwUuid.Zero; // Debug.Assert(false); PwUuid pwPrefTmp = m_pwEntryTemplatesGroup, pwAltTmp = pwSource.m_pwEntryTemplatesGroup; if(bForce || (pwSource.m_dtEntryTemplatesChanged > m_dtEntryTemplatesChanged)) { pwPrefTmp = pwSource.m_pwEntryTemplatesGroup; pwAltTmp = m_pwEntryTemplatesGroup; m_dtEntryTemplatesChanged = pwSource.m_dtEntryTemplatesChanged; } if(m_pgRootGroup.FindGroup(pwPrefTmp, true) != null) m_pwEntryTemplatesGroup = pwPrefTmp; else if(m_pgRootGroup.FindGroup(pwAltTmp, true) != null) m_pwEntryTemplatesGroup = pwAltTmp; else m_pwEntryTemplatesGroup = PwUuid.Zero; // Debug.Assert(false); } private void MergeEntryHistory(PwEntry pe, PwEntry peSource, PwMergeMethod mm) { if(!pe.Uuid.EqualsValue(peSource.Uuid)) { Debug.Assert(false); return; } if(pe.History.UCount == peSource.History.UCount) { bool bEqual = true; for(uint uEnum = 0; uEnum < pe.History.UCount; ++uEnum) { if(pe.History.GetAt(uEnum).LastModificationTime != peSource.History.GetAt(uEnum).LastModificationTime) { bEqual = false; break; } } if(bEqual) return; } if((m_slStatus != null) && !m_slStatus.ContinueWork()) return; SortedList list = new SortedList(); foreach(PwEntry peOrg in pe.History) { list[peOrg.LastModificationTime] = peOrg; } foreach(PwEntry peSrc in peSource.History) { DateTime dt = peSrc.LastModificationTime; if(list.ContainsKey(dt)) { if(mm == PwMergeMethod.OverwriteExisting) list[dt] = peSrc.CloneDeep(); } else list[dt] = peSrc.CloneDeep(); } pe.History.Clear(); foreach(KeyValuePair kvpCur in list) { Debug.Assert(kvpCur.Value.Uuid.EqualsValue(pe.Uuid)); Debug.Assert(kvpCur.Value.History.UCount == 0); pe.History.Add(kvpCur.Value); } } public bool MaintainBackups() { if(m_pgRootGroup == null) { Debug.Assert(false); return false; } bool bDeleted = false; EntryHandler eh = delegate(PwEntry pe) { if(pe.MaintainBackups(this)) bDeleted = true; return true; }; m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, null, eh); return bDeleted; } /* /// /// Synchronize current database with another one. /// /// Source file. public void Synchronize(string strFile) { PwDatabase pwSource = new PwDatabase(); IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFile); pwSource.Open(ioc, m_pwUserKey, null); MergeIn(pwSource, PwMergeMethod.Synchronize); } */ /// /// Get the index of a custom icon. /// /// ID of the icon. /// Index of the icon. public int GetCustomIconIndex(PwUuid pwIconId) { int nIndex = 0; foreach(PwCustomIcon pwci in m_vCustomIcons) { if(pwci.Uuid.EqualsValue(pwIconId)) return nIndex; ++nIndex; } // Debug.Assert(false); // Do not assert return -1; } /// /// Get a custom icon. This function can return null, if /// no cached image of the icon is available. /// /// ID of the icon. /// Image data. public Android.Graphics.Bitmap GetCustomIcon(PwUuid pwIconId) { int nIndex = GetCustomIconIndex(pwIconId); if(nIndex >= 0) return m_vCustomIcons[nIndex].Image; else { Debug.Assert(false); return null; } } public bool DeleteCustomIcons(List vUuidsToDelete) { Debug.Assert(vUuidsToDelete != null); if(vUuidsToDelete == null) throw new ArgumentNullException("vUuidsToDelete"); if(vUuidsToDelete.Count <= 0) return true; GroupHandler gh = delegate(PwGroup pg) { PwUuid uuidThis = pg.CustomIconUuid; if(uuidThis.EqualsValue(PwUuid.Zero)) return true; foreach(PwUuid uuidDelete in vUuidsToDelete) { if(uuidThis.EqualsValue(uuidDelete)) { pg.CustomIconUuid = PwUuid.Zero; break; } } return true; }; EntryHandler eh = delegate(PwEntry pe) { RemoveCustomIconUuid(pe, vUuidsToDelete); return true; }; gh(m_pgRootGroup); if(!m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh)) { Debug.Assert(false); return false; } foreach(PwUuid pwUuid in vUuidsToDelete) { int nIndex = GetCustomIconIndex(pwUuid); if(nIndex >= 0) m_vCustomIcons.RemoveAt(nIndex); } return true; } private static void RemoveCustomIconUuid(PwEntry pe, List vToDelete) { PwUuid uuidThis = pe.CustomIconUuid; if(uuidThis.EqualsValue(PwUuid.Zero)) return; foreach(PwUuid uuidDelete in vToDelete) { if(uuidThis.EqualsValue(uuidDelete)) { pe.CustomIconUuid = PwUuid.Zero; break; } } foreach(PwEntry peHistory in pe.History) RemoveCustomIconUuid(peHistory, vToDelete); } private bool ValidateUuidUniqueness() { #if DEBUG List l = new List(); bool bAllUnique = true; GroupHandler gh = delegate(PwGroup pg) { foreach(PwUuid u in l) bAllUnique &= !pg.Uuid.EqualsValue(u); l.Add(pg.Uuid); return bAllUnique; }; EntryHandler eh = delegate(PwEntry pe) { foreach(PwUuid u in l) bAllUnique &= !pe.Uuid.EqualsValue(u); l.Add(pe.Uuid); return bAllUnique; }; m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh); return bAllUnique; #else return true; #endif } /* public void CreateBackupFile(IStatusLogger sl) { if(sl != null) sl.SetText(KLRes.CreatingBackupFile, LogStatusType.Info); IOConnectionInfo iocBk = m_ioSource.CloneDeep(); iocBk.Path += StrBackupExtension; bool bMadeUnhidden = UrlUtil.UnhideFile(iocBk.Path); bool bFastCopySuccess = false; if(m_ioSource.IsLocalFile() && (m_ioSource.UserName.Length == 0) && (m_ioSource.Password.Length == 0)) { try { string strFile = m_ioSource.Path + StrBackupExtension; File.Copy(m_ioSource.Path, strFile, true); bFastCopySuccess = true; } catch(Exception) { Debug.Assert(false); } } if(bFastCopySuccess == false) { using(Stream sIn = IOConnection.OpenRead(m_ioSource)) { using(Stream sOut = IOConnection.OpenWrite(iocBk)) { MemUtil.CopyStream(sIn, sOut); sOut.Close(); } sIn.Close(); } } if(bMadeUnhidden) UrlUtil.HideFile(iocBk.Path, true); // Hide again } */ /* private static void RemoveData(PwGroup pg) { EntryHandler eh = delegate(PwEntry pe) { pe.AutoType.Clear(); pe.Binaries.Clear(); pe.History.Clear(); pe.Strings.Clear(); return true; }; pg.TraverseTree(TraversalMethod.PreOrder, null, eh); } */ public uint DeleteDuplicateEntries(IStatusLogger sl) { uint uDeleted = 0; PwGroup pgRecycleBin = null; if(m_bUseRecycleBin) pgRecycleBin = m_pgRootGroup.FindGroup(m_pwRecycleBin, true); DateTime dtNow = DateTime.Now; PwObjectList l = m_pgRootGroup.GetEntries(true); int i = 0; while(true) { if(i >= ((int)l.UCount - 1)) break; if(sl != null) { long lCnt = (long)l.UCount, li = (long)i; long nArTotal = (lCnt * lCnt) / 2L; long nArCur = li * lCnt - ((li * li) / 2L); long nArPct = (nArCur * 100L) / nArTotal; if(nArPct < 0) nArPct = 0; if(nArPct > 100) nArPct = 100; if(!sl.SetProgress((uint)nArPct)) break; } PwEntry peA = l.GetAt((uint)i); for(uint j = (uint)i + 1; j < l.UCount; ++j) { PwEntry peB = l.GetAt(j); if(!DupEntriesEqual(peA, peB)) continue; bool bDeleteA = (peA.LastModificationTime <= peB.LastModificationTime); if(pgRecycleBin != null) { bool bAInBin = peA.IsContainedIn(pgRecycleBin); bool bBInBin = peB.IsContainedIn(pgRecycleBin); if(bAInBin && !bBInBin) bDeleteA = true; else if(bBInBin && !bAInBin) bDeleteA = false; } if(bDeleteA) { peA.ParentGroup.Entries.Remove(peA); m_vDeletedObjects.Add(new PwDeletedObject(peA.Uuid, dtNow)); l.RemoveAt((uint)i); --i; } else { peB.ParentGroup.Entries.Remove(peB); m_vDeletedObjects.Add(new PwDeletedObject(peB.Uuid, dtNow)); l.RemoveAt(j); } ++uDeleted; break; } ++i; } return uDeleted; } private static List m_lStdFields = null; private static bool DupEntriesEqual(PwEntry a, PwEntry b) { if(m_lStdFields == null) m_lStdFields = PwDefs.GetStandardFields(); foreach(string strStdKey in m_lStdFields) { string strA = a.Strings.ReadSafe(strStdKey); string strB = b.Strings.ReadSafe(strStdKey); if(!strA.Equals(strB)) return false; } foreach(KeyValuePair kvpA in a.Strings) { if(PwDefs.IsStandardField(kvpA.Key)) continue; ProtectedString psB = b.Strings.Get(kvpA.Key); if(psB == null) return false; // Ignore protection setting, compare values only if(!kvpA.Value.ReadString().Equals(psB.ReadString())) return false; } foreach(KeyValuePair kvpB in b.Strings) { if(PwDefs.IsStandardField(kvpB.Key)) continue; ProtectedString psA = a.Strings.Get(kvpB.Key); if(psA == null) return false; // Must be equal by logic Debug.Assert(kvpB.Value.ReadString().Equals(psA.ReadString())); } if(a.Binaries.UCount != b.Binaries.UCount) return false; foreach(KeyValuePair kvpBin in a.Binaries) { ProtectedBinary pbB = b.Binaries.Get(kvpBin.Key); if(pbB == null) return false; // Ignore protection setting, compare values only byte[] pbDataA = kvpBin.Value.ReadData(); byte[] pbDataB = pbB.ReadData(); bool bBinEq = MemUtil.ArraysEqual(pbDataA, pbDataB); MemUtil.ZeroByteArray(pbDataA); MemUtil.ZeroByteArray(pbDataB); if(!bBinEq) return false; } return true; } public uint DeleteEmptyGroups() { uint uDeleted = 0; PwObjectList l = m_pgRootGroup.GetGroups(true); int iStart = (int)l.UCount - 1; for(int i = iStart; i >= 0; --i) { PwGroup pg = l.GetAt((uint)i); if((pg.Groups.UCount > 0) || (pg.Entries.UCount > 0)) continue; pg.ParentGroup.Groups.Remove(pg); m_vDeletedObjects.Add(new PwDeletedObject(pg.Uuid, DateTime.Now)); ++uDeleted; } return uDeleted; } public uint DeleteUnusedCustomIcons() { List lToDelete = new List(); foreach(PwCustomIcon pwci in m_vCustomIcons) lToDelete.Add(pwci.Uuid); GroupHandler gh = delegate(PwGroup pg) { PwUuid pwUuid = pg.CustomIconUuid; if((pwUuid == null) || pwUuid.EqualsValue(PwUuid.Zero)) return true; for(int i = 0; i < lToDelete.Count; ++i) { if(lToDelete[i].EqualsValue(pwUuid)) { lToDelete.RemoveAt(i); break; } } return true; }; EntryHandler eh = delegate(PwEntry pe) { PwUuid pwUuid = pe.CustomIconUuid; if((pwUuid == null) || pwUuid.EqualsValue(PwUuid.Zero)) return true; for(int i = 0; i < lToDelete.Count; ++i) { if(lToDelete[i].EqualsValue(pwUuid)) { lToDelete.RemoveAt(i); break; } } return true; }; gh(m_pgRootGroup); m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh); uint uDeleted = 0; foreach(PwUuid pwDel in lToDelete) { int nIndex = GetCustomIconIndex(pwDel); if(nIndex < 0) { Debug.Assert(false); continue; } m_vCustomIcons.RemoveAt(nIndex); ++uDeleted; } if(uDeleted > 0) m_bUINeedsIconUpdate = true; return uDeleted; } } }