/* KeePass Password Safe - The Open-Source Password Manager Copyright (C) 2003-2017 Dominik Reichl 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.Globalization; using System.IO; using System.Security; using System.Text; using System.Xml; #if !KeePassUAP using System.Security.Cryptography; #endif using KeePassLib.Collections; using KeePassLib.Cryptography; using KeePassLib.Cryptography.Cipher; using KeePassLib.Cryptography.KeyDerivation; using KeePassLib.Delegates; using KeePassLib.Interfaces; using KeePassLib.Resources; using KeePassLib.Security; using KeePassLib.Utility; namespace KeePassLib.Serialization { /// /// The KdbxFile class supports saving the data to various /// formats. /// public enum KdbxFormat { /// /// The default, encrypted file format. /// Default = 0, /// /// Use this flag when exporting data to a plain-text XML file. /// PlainXml } /// /// Serialization to KeePass KDBX files. /// public sealed partial class KdbxFile { /// /// File identifier, first 32-bit value. /// internal const uint FileSignature1 = 0x9AA2D903; /// /// File identifier, second 32-bit value. /// internal const uint FileSignature2 = 0xB54BFB67; /// /// File version of files saved by the current KdbxFile class. /// KeePass 2.07 has version 1.01, 2.08 has 1.02, 2.09 has 2.00, /// 2.10 has 2.02, 2.11 has 2.04, 2.15 has 3.00, 2.20 has 3.01. /// The first 2 bytes are critical (i.e. loading will fail, if the /// file version is too high), the last 2 bytes are informational. /// private const uint FileVersion32 = 0x00040000; internal const uint FileVersion32_4 = 0x00040000; // First of 4.x series internal const uint FileVersion32_3 = 0x00030001; // Old format 3.1 private const uint FileVersionCriticalMask = 0xFFFF0000; // KeePass 1.x signature internal const uint FileSignatureOld1 = 0x9AA2D903; internal const uint FileSignatureOld2 = 0xB54BFB65; // KeePass 2.x pre-release (alpha and beta) signature internal const uint FileSignaturePreRelease1 = 0x9AA2D903; internal const uint FileSignaturePreRelease2 = 0xB54BFB66; private const string ElemDocNode = "KeePassFile"; private const string ElemMeta = "Meta"; private const string ElemRoot = "Root"; private const string ElemGroup = "Group"; private const string ElemEntry = "Entry"; private const string ElemGenerator = "Generator"; private const string ElemHeaderHash = "HeaderHash"; private const string ElemSettingsChanged = "SettingsChanged"; private const string ElemDbName = "DatabaseName"; private const string ElemDbNameChanged = "DatabaseNameChanged"; private const string ElemDbDesc = "DatabaseDescription"; private const string ElemDbDescChanged = "DatabaseDescriptionChanged"; private const string ElemDbDefaultUser = "DefaultUserName"; private const string ElemDbDefaultUserChanged = "DefaultUserNameChanged"; private const string ElemDbMntncHistoryDays = "MaintenanceHistoryDays"; private const string ElemDbColor = "Color"; private const string ElemDbKeyChanged = "MasterKeyChanged"; private const string ElemDbKeyChangeRec = "MasterKeyChangeRec"; private const string ElemDbKeyChangeForce = "MasterKeyChangeForce"; private const string ElemDbKeyChangeForceOnce = "MasterKeyChangeForceOnce"; private const string ElemRecycleBinEnabled = "RecycleBinEnabled"; private const string ElemRecycleBinUuid = "RecycleBinUUID"; private const string ElemRecycleBinChanged = "RecycleBinChanged"; private const string ElemEntryTemplatesGroup = "EntryTemplatesGroup"; private const string ElemEntryTemplatesGroupChanged = "EntryTemplatesGroupChanged"; private const string ElemHistoryMaxItems = "HistoryMaxItems"; private const string ElemHistoryMaxSize = "HistoryMaxSize"; private const string ElemLastSelectedGroup = "LastSelectedGroup"; private const string ElemLastTopVisibleGroup = "LastTopVisibleGroup"; private const string ElemMemoryProt = "MemoryProtection"; private const string ElemProtTitle = "ProtectTitle"; private const string ElemProtUserName = "ProtectUserName"; private const string ElemProtPassword = "ProtectPassword"; private const string ElemProtUrl = "ProtectURL"; private const string ElemProtNotes = "ProtectNotes"; // private const string ElemProtAutoHide = "AutoEnableVisualHiding"; private const string ElemCustomIcons = "CustomIcons"; private const string ElemCustomIconItem = "Icon"; private const string ElemCustomIconItemID = "UUID"; private const string ElemCustomIconItemData = "Data"; private const string ElemAutoType = "AutoType"; private const string ElemHistory = "History"; private const string ElemName = "Name"; private const string ElemNotes = "Notes"; private const string ElemUuid = "UUID"; private const string ElemIcon = "IconID"; private const string ElemCustomIconID = "CustomIconUUID"; private const string ElemFgColor = "ForegroundColor"; private const string ElemBgColor = "BackgroundColor"; private const string ElemOverrideUrl = "OverrideURL"; private const string ElemTimes = "Times"; private const string ElemTags = "Tags"; private const string ElemCreationTime = "CreationTime"; private const string ElemLastModTime = "LastModificationTime"; private const string ElemLastAccessTime = "LastAccessTime"; private const string ElemExpiryTime = "ExpiryTime"; private const string ElemExpires = "Expires"; private const string ElemUsageCount = "UsageCount"; private const string ElemLocationChanged = "LocationChanged"; private const string ElemGroupDefaultAutoTypeSeq = "DefaultAutoTypeSequence"; private const string ElemEnableAutoType = "EnableAutoType"; private const string ElemEnableSearching = "EnableSearching"; private const string ElemString = "String"; private const string ElemBinary = "Binary"; private const string ElemKey = "Key"; private const string ElemValue = "Value"; private const string ElemAutoTypeEnabled = "Enabled"; private const string ElemAutoTypeObfuscation = "DataTransferObfuscation"; private const string ElemAutoTypeDefaultSeq = "DefaultSequence"; private const string ElemAutoTypeItem = "Association"; private const string ElemWindow = "Window"; private const string ElemKeystrokeSequence = "KeystrokeSequence"; private const string ElemBinaries = "Binaries"; private const string AttrId = "ID"; private const string AttrRef = "Ref"; private const string AttrProtected = "Protected"; private const string AttrProtectedInMemPlainXml = "ProtectInMemory"; private const string AttrCompressed = "Compressed"; private const string ElemIsExpanded = "IsExpanded"; private const string ElemLastTopVisibleEntry = "LastTopVisibleEntry"; private const string ElemDeletedObjects = "DeletedObjects"; private const string ElemDeletedObject = "DeletedObject"; private const string ElemDeletionTime = "DeletionTime"; private const string ValFalse = "False"; private const string ValTrue = "True"; private const string ElemCustomData = "CustomData"; private const string ElemStringDictExItem = "Item"; private PwDatabase m_pwDatabase; // Not null, see constructor private bool m_bUsedOnce = false; private XmlWriter m_xmlWriter = null; private CryptoRandomStream m_randomStream = null; private KdbxFormat m_format = KdbxFormat.Default; private IStatusLogger m_slLogger = null; private uint m_uFileVersion = 0; private byte[] m_pbMasterSeed = null; // private byte[] m_pbTransformSeed = null; private byte[] m_pbEncryptionIV = null; private byte[] m_pbStreamStartBytes = null; // ArcFourVariant only for backward compatibility; KeePass defaults // to a more secure algorithm when *writing* databases private CrsAlgorithm m_craInnerRandomStream = CrsAlgorithm.ArcFourVariant; private byte[] m_pbInnerRandomStreamKey = null; private ProtectedBinarySet m_pbsBinaries = new ProtectedBinarySet(); private byte[] m_pbHashOfHeader = null; private byte[] m_pbHashOfFileOnDisk = null; private readonly DateTime m_dtNow = DateTime.UtcNow; // Cache current time private const uint NeutralLanguageOffset = 0x100000; // 2^20, see 32-bit Unicode specs private const uint NeutralLanguageIDSec = 0x7DC5C; // See 32-bit Unicode specs private const uint NeutralLanguageID = NeutralLanguageOffset + NeutralLanguageIDSec; private static bool m_bLocalizedNames = false; private enum KdbxHeaderFieldID : byte { EndOfHeader = 0, Comment = 1, CipherID = 2, CompressionFlags = 3, MasterSeed = 4, TransformSeed = 5, // KDBX 3.1, for backward compatibility only TransformRounds = 6, // KDBX 3.1, for backward compatibility only EncryptionIV = 7, InnerRandomStreamKey = 8, // KDBX 3.1, for backward compatibility only StreamStartBytes = 9, // KDBX 3.1, for backward compatibility only InnerRandomStreamID = 10, // KDBX 3.1, for backward compatibility only KdfParameters = 11, // KDBX 4, superseding Transform* PublicCustomData = 12 // KDBX 4 } // Inner header in KDBX >= 4 files private enum KdbxInnerHeaderFieldID : byte { EndOfHeader = 0, InnerRandomStreamID = 1, // Supersedes KdbxHeaderFieldID.InnerRandomStreamID InnerRandomStreamKey = 2, // Supersedes KdbxHeaderFieldID.InnerRandomStreamKey Binary = 3 } [Flags] private enum KdbxBinaryFlags : byte { None = 0, Protected = 1 } public byte[] HashOfFileOnDisk { get { return m_pbHashOfFileOnDisk; } } private bool m_bRepairMode = false; public bool RepairMode { get { return m_bRepairMode; } set { m_bRepairMode = value; } } private uint m_uForceVersion = 0; internal uint ForceVersion { get { return m_uForceVersion; } set { m_uForceVersion = 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; } } /// /// Default constructor. /// /// The PwDatabase instance that the /// class will load file data into or use to create a KDBX file. public KdbxFile(PwDatabase pwDataStore) { Debug.Assert(pwDataStore != null); if(pwDataStore == null) throw new ArgumentNullException("pwDataStore"); m_pwDatabase = pwDataStore; } /// /// Call this once to determine the current localization settings. /// public static void DetermineLanguageId() { // Test if localized names should be used. If localized names are used, // the m_bLocalizedNames value must be set to true. By default, localized // names should be used! (Otherwise characters could be corrupted // because of different code pages). unchecked { uint uTest = 0; foreach(char ch in PwDatabase.LocalizedAppName) uTest = uTest * 5 + ch; m_bLocalizedNames = (uTest != NeutralLanguageID); } } private uint GetMinKdbxVersion() { if(m_uForceVersion != 0) return m_uForceVersion; // See also KeePassKdb2x3.Export (KDBX 3.1 export module) AesKdf kdfAes = new AesKdf(); if(!kdfAes.Uuid.Equals(m_pwDatabase.KdfParameters.KdfUuid)) return FileVersion32; if(m_pwDatabase.PublicCustomData.Count > 0) return FileVersion32; bool bCustomData = false; GroupHandler gh = delegate(PwGroup pg) { if(pg == null) { Debug.Assert(false); return true; } if(pg.CustomData.Count > 0) { bCustomData = true; return false; } return true; }; EntryHandler eh = delegate(PwEntry pe) { if(pe == null) { Debug.Assert(false); return true; } if(pe.CustomData.Count > 0) { bCustomData = true; return false; } return true; }; gh(m_pwDatabase.RootGroup); m_pwDatabase.RootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh); if(bCustomData) return FileVersion32; return FileVersion32_3; // KDBX 3.1 is sufficient } private void ComputeKeys(out byte[] pbCipherKey, int cbCipherKey, out byte[] pbHmacKey64) { byte[] pbCmp = new byte[32 + 32 + 1]; try { Debug.Assert(m_pbMasterSeed != null); if(m_pbMasterSeed == null) throw new ArgumentNullException("m_pbMasterSeed"); Debug.Assert(m_pbMasterSeed.Length == 32); if(m_pbMasterSeed.Length != 32) throw new FormatException(KLRes.MasterSeedLengthInvalid); Array.Copy(m_pbMasterSeed, 0, pbCmp, 0, 32); Debug.Assert(m_pwDatabase != null); Debug.Assert(m_pwDatabase.MasterKey != null); ProtectedBinary pbinUser = m_pwDatabase.MasterKey.GenerateKey32( m_pwDatabase.KdfParameters); Debug.Assert(pbinUser != null); if(pbinUser == null) throw new SecurityException(KLRes.InvalidCompositeKey); byte[] pUserKey32 = pbinUser.ReadData(); if((pUserKey32 == null) || (pUserKey32.Length != 32)) throw new SecurityException(KLRes.InvalidCompositeKey); Array.Copy(pUserKey32, 0, pbCmp, 32, 32); MemUtil.ZeroByteArray(pUserKey32); pbCipherKey = CryptoUtil.ResizeKey(pbCmp, 0, 64, cbCipherKey); pbCmp[64] = 1; using(SHA512Managed h = new SHA512Managed()) { pbHmacKey64 = h.ComputeHash(pbCmp); } } finally { MemUtil.ZeroByteArray(pbCmp); } } private ICipherEngine GetCipher(out int cbEncKey, out int cbEncIV) { PwUuid pu = m_pwDatabase.DataCipherUuid; ICipherEngine iCipher = CipherPool.GlobalPool.GetCipher(pu); if(iCipher == null) // CryptographicExceptions are translated to "file corrupted" throw new Exception(KLRes.FileUnknownCipher + MessageService.NewParagraph + KLRes.FileNewVerOrPlgReq + MessageService.NewParagraph + "UUID: " + pu.ToHexString() + "."); ICipherEngine2 iCipher2 = (iCipher as ICipherEngine2); if(iCipher2 != null) { cbEncKey = iCipher2.KeyLength; if(cbEncKey < 0) throw new InvalidOperationException("EncKey.Length"); cbEncIV = iCipher2.IVLength; if(cbEncIV < 0) throw new InvalidOperationException("EncIV.Length"); } else { cbEncKey = 32; cbEncIV = 16; } return iCipher; } private Stream EncryptStream(Stream s, ICipherEngine iCipher, byte[] pbKey, int cbIV, bool bEncrypt) { byte[] pbIV = (m_pbEncryptionIV ?? MemUtil.EmptyByteArray); if(pbIV.Length != cbIV) { Debug.Assert(false); throw new Exception(KLRes.FileCorrupted); } if(bEncrypt) return iCipher.EncryptStream(s, pbKey, pbIV); return iCipher.DecryptStream(s, pbKey, pbIV); } private byte[] ComputeHeaderHmac(byte[] pbHeader, byte[] pbKey) { byte[] pbHeaderHmac; byte[] pbBlockKey = HmacBlockStream.GetHmacKey64( pbKey, ulong.MaxValue); using(HMACSHA256 h = new HMACSHA256(pbBlockKey)) { pbHeaderHmac = h.ComputeHash(pbHeader); } MemUtil.ZeroByteArray(pbBlockKey); return pbHeaderHmac; } private void CloseStreams(List lStreams) { if(lStreams == null) { Debug.Assert(false); return; } // Typically, closing a stream also closes its base // stream; however, there may be streams that do not // do this (e.g. some cipher plugin), thus for safety // we close all streams manually, from the innermost // to the outermost for(int i = lStreams.Count - 1; i >= 0; --i) { // Check for duplicates Debug.Assert((lStreams.IndexOf(lStreams[i]) == i) && (lStreams.LastIndexOf(lStreams[i]) == i)); try { lStreams[i].Close(); } catch(Exception) { Debug.Assert(false); } } // Do not clear the list } private void CleanUpInnerRandomStream() { if(m_randomStream != null) m_randomStream.Dispose(); if(m_pbInnerRandomStreamKey != null) MemUtil.ZeroByteArray(m_pbInnerRandomStreamKey); } private static void SaveBinary(string strName, ProtectedBinary pb, string strSaveDir) { if(pb == null) { Debug.Assert(false); return; } if(string.IsNullOrEmpty(strName)) strName = "File.bin"; string strPath; int iTry = 1; do { strPath = UrlUtil.EnsureTerminatingSeparator(strSaveDir, false); string strExt = UrlUtil.GetExtension(strName); string strDesc = UrlUtil.StripExtension(strName); strPath += strDesc; if(iTry > 1) strPath += " (" + iTry.ToString(NumberFormatInfo.InvariantInfo) + ")"; if(!string.IsNullOrEmpty(strExt)) strPath += "." + strExt; ++iTry; } while(File.Exists(strPath)); #if !KeePassLibSD byte[] pbData = pb.ReadData(); File.WriteAllBytes(strPath, pbData); MemUtil.ZeroByteArray(pbData); #else FileStream fs = new FileStream(strPath, FileMode.Create, FileAccess.Write, FileShare.None); byte[] pbData = pb.ReadData(); fs.Write(pbData, 0, pbData.Length); fs.Close(); #endif } } }