/* KeePass Password Safe - The Open-Source Password Manager Copyright (C) 2003-2020 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 */ // #define KDBX_BENCHMARK using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Security; using System.Text; using System.Xml; #if !ModernKeePassLib && !KeePassUAP using System.Security.Cryptography; #endif #if !KeePassLibSD using System.IO.Compression; #else using KeePassLibSD; #endif using ModernKeePassLib.Collections; using ModernKeePassLib.Cryptography; using ModernKeePassLib.Cryptography.Cipher; using ModernKeePassLib.Cryptography.KeyDerivation; using ModernKeePassLib.Interfaces; using ModernKeePassLib.Keys; using ModernKeePassLib.Resources; using ModernKeePassLib.Security; using ModernKeePassLib.Utility; namespace ModernKeePassLib.Serialization { /// /// Serialization to KeePass KDBX files. /// public sealed partial class KdbxFile { /// /// Load a KDBX file. /// /// File to load. /// Format. /// Status logger (optional). #if ModernKeePassLib public void Load(byte[] fileContents, KdbxFormat fmt, IStatusLogger slLogger) { IOConnectionInfo ioc = IOConnectionInfo.FromByteArray(fileContents); #else public void Load(string strFilePath, KdbxFormat fmt, IStatusLogger slLogger) { IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFilePath); #endif Load(IOConnection.OpenRead(ioc), fmt, slLogger); } /// /// Load a KDBX file from a stream. /// /// Stream to read the data from. Must contain /// a KDBX stream. /// Format. /// Status logger (optional). public void Load(Stream sSource, KdbxFormat fmt, IStatusLogger slLogger) { Debug.Assert(sSource != null); if(sSource == null) throw new ArgumentNullException("sSource"); if(m_bUsedOnce) throw new InvalidOperationException("Do not reuse KdbxFile objects!"); m_bUsedOnce = true; #if KDBX_BENCHMARK Stopwatch swTime = Stopwatch.StartNew(); #endif m_format = fmt; m_slLogger = slLogger; m_pbsBinaries.Clear(); UTF8Encoding encNoBom = StrUtil.Utf8; byte[] pbCipherKey = null; byte[] pbHmacKey64 = null; List lStreams = new List(); lStreams.Add(sSource); HashingStreamEx sHashing = new HashingStreamEx(sSource, false, null); lStreams.Add(sHashing); try { Stream sXml; if(fmt == KdbxFormat.Default) { BinaryReaderEx br = new BinaryReaderEx(sHashing, encNoBom, KLRes.FileCorrupted); byte[] pbHeader = LoadHeader(br); m_pbHashOfHeader = CryptoUtil.HashSha256(pbHeader); int cbEncKey, cbEncIV; ICipherEngine iCipher = GetCipher(out cbEncKey, out cbEncIV); ComputeKeys(out pbCipherKey, cbEncKey, out pbHmacKey64); string strIncomplete = KLRes.FileHeaderCorrupted + " " + KLRes.FileIncomplete; Stream sPlain; if(m_uFileVersion < FileVersion32_4) { Stream sDecrypted = EncryptStream(sHashing, iCipher, pbCipherKey, cbEncIV, false); if((sDecrypted == null) || (sDecrypted == sHashing)) throw new SecurityException(KLRes.CryptoStreamFailed); lStreams.Add(sDecrypted); BinaryReaderEx brDecrypted = new BinaryReaderEx(sDecrypted, encNoBom, strIncomplete); byte[] pbStoredStartBytes = brDecrypted.ReadBytes(32); if((m_pbStreamStartBytes == null) || (m_pbStreamStartBytes.Length != 32)) throw new EndOfStreamException(strIncomplete); if(!MemUtil.ArraysEqual(pbStoredStartBytes, m_pbStreamStartBytes)) throw new InvalidCompositeKeyException(); sPlain = new HashedBlockStream(sDecrypted, false, 0, !m_bRepairMode); } else // KDBX >= 4 { byte[] pbStoredHash = MemUtil.Read(sHashing, 32); if((pbStoredHash == null) || (pbStoredHash.Length != 32)) throw new EndOfStreamException(strIncomplete); if(!MemUtil.ArraysEqual(m_pbHashOfHeader, pbStoredHash)) throw new InvalidDataException(KLRes.FileHeaderCorrupted); byte[] pbHeaderHmac = ComputeHeaderHmac(pbHeader, pbHmacKey64); byte[] pbStoredHmac = MemUtil.Read(sHashing, 32); if((pbStoredHmac == null) || (pbStoredHmac.Length != 32)) throw new EndOfStreamException(strIncomplete); if(!MemUtil.ArraysEqual(pbHeaderHmac, pbStoredHmac)) throw new InvalidCompositeKeyException(); HmacBlockStream sBlocks = new HmacBlockStream(sHashing, false, !m_bRepairMode, pbHmacKey64); lStreams.Add(sBlocks); sPlain = EncryptStream(sBlocks, iCipher, pbCipherKey, cbEncIV, false); if((sPlain == null) || (sPlain == sBlocks)) throw new SecurityException(KLRes.CryptoStreamFailed); } lStreams.Add(sPlain); if(m_pwDatabase.Compression == PwCompressionAlgorithm.GZip) { sXml = new GZipStream(sPlain, CompressionMode.Decompress); lStreams.Add(sXml); } else sXml = sPlain; if(m_uFileVersion >= FileVersion32_4) LoadInnerHeader(sXml); // Binary header before XML } else if(fmt == KdbxFormat.PlainXml) sXml = sHashing; else { Debug.Assert(false); throw new ArgumentOutOfRangeException("fmt"); } if(fmt == KdbxFormat.Default) { if(m_pbInnerRandomStreamKey == null) { Debug.Assert(false); throw new SecurityException("Invalid inner random stream key!"); } m_randomStream = new CryptoRandomStream(m_craInnerRandomStream, m_pbInnerRandomStreamKey); } #if KeePassDebug_WriteXml #warning XML output is enabled! /* using(FileStream fsOut = new FileStream("Raw.xml", FileMode.Create, FileAccess.Write, FileShare.None)) { while(true) { int b = sXml.ReadByte(); if(b == -1) throw new EndOfStreamException(); fsOut.WriteByte((byte)b); } } */ #endif ReadXmlStreamed(sXml, sHashing); // ReadXmlDom(sXml); } #if !ModernKeePassLib catch(CryptographicException) // Thrown on invalid padding { throw new CryptographicException(KLRes.FileCorrupted); } #endif finally { if(pbCipherKey != null) MemUtil.ZeroByteArray(pbCipherKey); if(pbHmacKey64 != null) MemUtil.ZeroByteArray(pbHmacKey64); CommonCleanUpRead(lStreams, sHashing); } #if KDBX_BENCHMARK swTime.Stop(); MessageService.ShowInfo("Loading KDBX took " + swTime.ElapsedMilliseconds.ToString() + " ms."); #endif } private void CommonCleanUpRead(List lStreams, HashingStreamEx sHashing) { CloseStreams(lStreams); Debug.Assert(lStreams.Contains(sHashing)); // sHashing must be closed m_pbHashOfFileOnDisk = sHashing.Hash; Debug.Assert(m_pbHashOfFileOnDisk != null); CleanUpInnerRandomStream(); // Reset memory protection settings (to always use reasonable // defaults) m_pwDatabase.MemoryProtection = new MemoryProtectionConfig(); // Remove old backups (this call is required here in order to apply // the default history maintenance settings for people upgrading from // KeePass <= 2.14 to >= 2.15; also it ensures history integrity in // case a different application has created the KDBX file and ignored // the history maintenance settings) m_pwDatabase.MaintainBackups(); // Don't mark database as modified // Expand the root group, such that in case the user accidently // collapses the root group he can simply reopen the database PwGroup pgRoot = m_pwDatabase.RootGroup; if(pgRoot != null) pgRoot.IsExpanded = true; else { Debug.Assert(false); } m_pbHashOfHeader = null; } private byte[] LoadHeader(BinaryReaderEx br) { string strPrevExcpText = br.ReadExceptionText; br.ReadExceptionText = KLRes.FileHeaderCorrupted + " " + KLRes.FileIncompleteExpc; MemoryStream msHeader = new MemoryStream(); Debug.Assert(br.CopyDataTo == null); br.CopyDataTo = msHeader; byte[] pbSig1 = br.ReadBytes(4); uint uSig1 = MemUtil.BytesToUInt32(pbSig1); byte[] pbSig2 = br.ReadBytes(4); uint uSig2 = MemUtil.BytesToUInt32(pbSig2); if((uSig1 == FileSignatureOld1) && (uSig2 == FileSignatureOld2)) throw new OldFormatException(PwDefs.ShortProductName + @" 1.x", OldFormatException.OldFormatType.KeePass1x); if((uSig1 == FileSignature1) && (uSig2 == FileSignature2)) { } else if((uSig1 == FileSignaturePreRelease1) && (uSig2 == FileSignaturePreRelease2)) { } else throw new FormatException(KLRes.FileSigInvalid); byte[] pb = br.ReadBytes(4); uint uVersion = MemUtil.BytesToUInt32(pb); if((uVersion & FileVersionCriticalMask) > (FileVersion32 & FileVersionCriticalMask)) throw new FormatException(KLRes.FileVersionUnsupported + MessageService.NewParagraph + KLRes.FileNewVerReq); m_uFileVersion = uVersion; while(true) { if(!ReadHeaderField(br)) break; } br.CopyDataTo = null; byte[] pbHeader = msHeader.ToArray(); msHeader.Dispose(); br.ReadExceptionText = strPrevExcpText; return pbHeader; } private bool ReadHeaderField(BinaryReaderEx brSource) { Debug.Assert(brSource != null); if(brSource == null) throw new ArgumentNullException("brSource"); byte btFieldID = brSource.ReadByte(); int cbSize; Debug.Assert(m_uFileVersion > 0); if(m_uFileVersion < FileVersion32_4) cbSize = (int)MemUtil.BytesToUInt16(brSource.ReadBytes(2)); else cbSize = MemUtil.BytesToInt32(brSource.ReadBytes(4)); if(cbSize < 0) throw new FormatException(KLRes.FileCorrupted); byte[] pbData = MemUtil.EmptyByteArray; if(cbSize > 0) pbData = brSource.ReadBytes(cbSize); bool bResult = true; KdbxHeaderFieldID kdbID = (KdbxHeaderFieldID)btFieldID; switch(kdbID) { case KdbxHeaderFieldID.EndOfHeader: bResult = false; // Returning false indicates end of header break; case KdbxHeaderFieldID.CipherID: SetCipher(pbData); break; case KdbxHeaderFieldID.CompressionFlags: SetCompressionFlags(pbData); break; case KdbxHeaderFieldID.MasterSeed: m_pbMasterSeed = pbData; CryptoRandom.Instance.AddEntropy(pbData); break; // Obsolete; for backward compatibility only case KdbxHeaderFieldID.TransformSeed: Debug.Assert(m_uFileVersion < FileVersion32_4); AesKdf kdfS = new AesKdf(); if(!m_pwDatabase.KdfParameters.KdfUuid.Equals(kdfS.Uuid)) m_pwDatabase.KdfParameters = kdfS.GetDefaultParameters(); // m_pbTransformSeed = pbData; m_pwDatabase.KdfParameters.SetByteArray(AesKdf.ParamSeed, pbData); CryptoRandom.Instance.AddEntropy(pbData); break; // Obsolete; for backward compatibility only case KdbxHeaderFieldID.TransformRounds: Debug.Assert(m_uFileVersion < FileVersion32_4); AesKdf kdfR = new AesKdf(); if(!m_pwDatabase.KdfParameters.KdfUuid.Equals(kdfR.Uuid)) m_pwDatabase.KdfParameters = kdfR.GetDefaultParameters(); // m_pwDatabase.KeyEncryptionRounds = MemUtil.BytesToUInt64(pbData); m_pwDatabase.KdfParameters.SetUInt64(AesKdf.ParamRounds, MemUtil.BytesToUInt64(pbData)); break; case KdbxHeaderFieldID.EncryptionIV: m_pbEncryptionIV = pbData; break; case KdbxHeaderFieldID.InnerRandomStreamKey: Debug.Assert(m_uFileVersion < FileVersion32_4); Debug.Assert(m_pbInnerRandomStreamKey == null); m_pbInnerRandomStreamKey = pbData; CryptoRandom.Instance.AddEntropy(pbData); break; case KdbxHeaderFieldID.StreamStartBytes: Debug.Assert(m_uFileVersion < FileVersion32_4); m_pbStreamStartBytes = pbData; break; case KdbxHeaderFieldID.InnerRandomStreamID: Debug.Assert(m_uFileVersion < FileVersion32_4); SetInnerRandomStreamID(pbData); break; case KdbxHeaderFieldID.KdfParameters: m_pwDatabase.KdfParameters = KdfParameters.DeserializeExt(pbData); break; case KdbxHeaderFieldID.PublicCustomData: Debug.Assert(m_pwDatabase.PublicCustomData.Count == 0); m_pwDatabase.PublicCustomData = VariantDictionary.Deserialize(pbData); break; default: Debug.Assert(false); if(m_slLogger != null) m_slLogger.SetText(KLRes.UnknownHeaderId + ": " + kdbID.ToString() + "!", LogStatusType.Warning); break; } return bResult; } private void LoadInnerHeader(Stream s) { BinaryReaderEx br = new BinaryReaderEx(s, StrUtil.Utf8, KLRes.FileCorrupted + " " + KLRes.FileIncompleteExpc); while(true) { if(!ReadInnerHeaderField(br)) break; } } private bool ReadInnerHeaderField(BinaryReaderEx br) { Debug.Assert(br != null); if(br == null) throw new ArgumentNullException("br"); byte btFieldID = br.ReadByte(); int cbSize = MemUtil.BytesToInt32(br.ReadBytes(4)); if(cbSize < 0) throw new FormatException(KLRes.FileCorrupted); byte[] pbData = MemUtil.EmptyByteArray; if(cbSize > 0) pbData = br.ReadBytes(cbSize); bool bResult = true; KdbxInnerHeaderFieldID kdbID = (KdbxInnerHeaderFieldID)btFieldID; switch(kdbID) { case KdbxInnerHeaderFieldID.EndOfHeader: bResult = false; // Returning false indicates end of header break; case KdbxInnerHeaderFieldID.InnerRandomStreamID: SetInnerRandomStreamID(pbData); break; case KdbxInnerHeaderFieldID.InnerRandomStreamKey: Debug.Assert(m_pbInnerRandomStreamKey == null); m_pbInnerRandomStreamKey = pbData; CryptoRandom.Instance.AddEntropy(pbData); break; case KdbxInnerHeaderFieldID.Binary: if(pbData.Length < 1) throw new FormatException(); KdbxBinaryFlags f = (KdbxBinaryFlags)pbData[0]; bool bProt = ((f & KdbxBinaryFlags.Protected) != KdbxBinaryFlags.None); ProtectedBinary pb = new ProtectedBinary(bProt, pbData, 1, pbData.Length - 1); m_pbsBinaries.Add(pb); if(bProt) MemUtil.ZeroByteArray(pbData); break; default: Debug.Assert(false); break; } return bResult; } private void SetCipher(byte[] pbID) { if((pbID == null) || (pbID.Length != (int)PwUuid.UuidSize)) throw new FormatException(KLRes.FileUnknownCipher); m_pwDatabase.DataCipherUuid = new PwUuid(pbID); } private void SetCompressionFlags(byte[] pbFlags) { int nID = (int)MemUtil.BytesToUInt32(pbFlags); if((nID < 0) || (nID >= (int)PwCompressionAlgorithm.Count)) throw new FormatException(KLRes.FileUnknownCompression); m_pwDatabase.Compression = (PwCompressionAlgorithm)nID; } private void SetInnerRandomStreamID(byte[] pbID) { uint uID = MemUtil.BytesToUInt32(pbID); if(uID >= (uint)CrsAlgorithm.Count) throw new FormatException(KLRes.FileUnknownCipher); m_craInnerRandomStream = (CrsAlgorithm)uID; } [Obsolete] public static List ReadEntries(Stream msData) { return ReadEntries(msData, null, false); } [Obsolete] public static List ReadEntries(PwDatabase pdContext, Stream msData) { return ReadEntries(msData, pdContext, true); } /// /// Read entries from a stream. /// /// Input stream to read the entries from. /// Context database (e.g. for storing icons). /// If true, custom icons required by /// the loaded entries are copied to the context database. /// Loaded entries. public static List ReadEntries(Stream msData, PwDatabase pdContext, bool bCopyIcons) { List lEntries = new List(); if(msData == null) { Debug.Assert(false); return lEntries; } // pdContext may be null /* KdbxFile f = new KdbxFile(pwDatabase); f.m_format = KdbxFormat.PlainXml; XmlDocument doc = XmlUtilEx.CreateXmlDocument(); doc.Load(msData); XmlElement el = doc.DocumentElement; if(el.Name != ElemRoot) throw new FormatException(); List vEntries = new List(); foreach(XmlNode xmlChild in el.ChildNodes) { if(xmlChild.Name == ElemEntry) { PwEntry pe = f.ReadEntry(xmlChild); pe.Uuid = new PwUuid(true); foreach(PwEntry peHistory in pe.History) peHistory.Uuid = pe.Uuid; vEntries.Add(pe); } else { Debug.Assert(false); } } return vEntries; */ PwDatabase pd = new PwDatabase(); pd.New(new IOConnectionInfo(), new CompositeKey()); KdbxFile f = new KdbxFile(pd); f.Load(msData, KdbxFormat.PlainXml, null); foreach(PwEntry pe in pd.RootGroup.Entries) { pe.SetUuid(new PwUuid(true), true); lEntries.Add(pe); if(bCopyIcons && (pdContext != null)) { PwUuid pu = pe.CustomIconUuid; if(!pu.Equals(PwUuid.Zero)) { int iSrc = pd.GetCustomIconIndex(pu); int iDst = pdContext.GetCustomIconIndex(pu); if(iSrc < 0) { Debug.Assert(false); } else if(iDst < 0) { pdContext.CustomIcons.Add(pd.CustomIcons[iSrc]); pdContext.Modified = true; pdContext.UINeedsIconUpdate = true; } } } } return lEntries; } } }