1 /* 2 KeePass Password Safe - The Open-Source Password Manager 3 Copyright (C) 2003-2021 Dominik Reichl <dominik.reichl@t-online.de> 4 5 This program is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published by 7 the Free Software Foundation; either version 2 of the License, or 8 (at your option) any later version. 9 10 This program is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with this program; if not, write to the Free Software 17 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 */ 19 20 // #define KDBX_BENCHMARK 21 22 using System; 23 using System.Collections.Generic; 24 using System.Diagnostics; 25 using System.IO; 26 using System.Security; 27 using System.Text; 28 using System.Xml; 29 30 #if !KeePassUAP 31 using System.Security.Cryptography; 32 #endif 33 34 #if !KeePassLibSD 35 using System.IO.Compression; 36 #else 37 using KeePassLibSD; 38 #endif 39 40 using KeePassLib.Collections; 41 using KeePassLib.Cryptography; 42 using KeePassLib.Cryptography.Cipher; 43 using KeePassLib.Cryptography.KeyDerivation; 44 using KeePassLib.Interfaces; 45 using KeePassLib.Keys; 46 using KeePassLib.Resources; 47 using KeePassLib.Security; 48 using KeePassLib.Utility; 49 50 namespace KeePassLib.Serialization 51 { 52 /// <summary> 53 /// Serialization to KeePass KDBX files. 54 /// </summary> 55 public sealed partial class KdbxFile 56 { 57 /// <summary> 58 /// Load a KDBX file. 59 /// </summary> 60 /// <param name="strFilePath">File to load.</param> 61 /// <param name="fmt">Format.</param> 62 /// <param name="slLogger">Status logger (optional).</param> Load(string strFilePath, KdbxFormat fmt, IStatusLogger slLogger)63 public void Load(string strFilePath, KdbxFormat fmt, IStatusLogger slLogger) 64 { 65 IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFilePath); 66 Load(IOConnection.OpenRead(ioc), fmt, slLogger); 67 } 68 69 /// <summary> 70 /// Load a KDBX file from a stream. 71 /// </summary> 72 /// <param name="sSource">Stream to read the data from. Must contain 73 /// a KDBX stream.</param> 74 /// <param name="fmt">Format.</param> 75 /// <param name="slLogger">Status logger (optional).</param> Load(Stream sSource, KdbxFormat fmt, IStatusLogger slLogger)76 public void Load(Stream sSource, KdbxFormat fmt, IStatusLogger slLogger) 77 { 78 Debug.Assert(sSource != null); 79 if(sSource == null) throw new ArgumentNullException("sSource"); 80 81 if(m_bUsedOnce) 82 throw new InvalidOperationException("Do not reuse KdbxFile objects!"); 83 m_bUsedOnce = true; 84 85 #if KDBX_BENCHMARK 86 Stopwatch swTime = Stopwatch.StartNew(); 87 #endif 88 89 m_format = fmt; 90 m_slLogger = slLogger; 91 92 // Other applications might not perform a deduplication 93 m_pbsBinaries = new ProtectedBinarySet(false); 94 95 UTF8Encoding encNoBom = StrUtil.Utf8; 96 byte[] pbCipherKey = null; 97 byte[] pbHmacKey64 = null; 98 99 List<Stream> lStreams = new List<Stream>(); 100 lStreams.Add(sSource); 101 102 HashingStreamEx sHashing = new HashingStreamEx(sSource, false, null); 103 lStreams.Add(sHashing); 104 105 try 106 { 107 Stream sXml; 108 if(fmt == KdbxFormat.Default) 109 { 110 BinaryReaderEx br = new BinaryReaderEx(sHashing, 111 encNoBom, KLRes.FileCorrupted); 112 byte[] pbHeader = LoadHeader(br); 113 m_pbHashOfHeader = CryptoUtil.HashSha256(pbHeader); 114 115 int cbEncKey, cbEncIV; 116 ICipherEngine iCipher = GetCipher(out cbEncKey, out cbEncIV); 117 118 ComputeKeys(out pbCipherKey, cbEncKey, out pbHmacKey64); 119 120 string strIncomplete = KLRes.FileHeaderCorrupted + " " + 121 KLRes.FileIncomplete; 122 123 Stream sPlain; 124 if(m_uFileVersion < FileVersion32_4) 125 { 126 Stream sDecrypted = EncryptStream(sHashing, iCipher, 127 pbCipherKey, cbEncIV, false); 128 if((sDecrypted == null) || (sDecrypted == sHashing)) 129 throw new SecurityException(KLRes.CryptoStreamFailed); 130 lStreams.Add(sDecrypted); 131 132 BinaryReaderEx brDecrypted = new BinaryReaderEx(sDecrypted, 133 encNoBom, strIncomplete); 134 byte[] pbStoredStartBytes = brDecrypted.ReadBytes(32); 135 136 if((m_pbStreamStartBytes == null) || (m_pbStreamStartBytes.Length != 32)) 137 throw new EndOfStreamException(strIncomplete); 138 if(!MemUtil.ArraysEqual(pbStoredStartBytes, m_pbStreamStartBytes)) 139 throw new InvalidCompositeKeyException(); 140 141 sPlain = new HashedBlockStream(sDecrypted, false, 0, !m_bRepairMode); 142 } 143 else // KDBX >= 4 144 { 145 byte[] pbStoredHash = MemUtil.Read(sHashing, 32); 146 if((pbStoredHash == null) || (pbStoredHash.Length != 32)) 147 throw new EndOfStreamException(strIncomplete); 148 if(!MemUtil.ArraysEqual(m_pbHashOfHeader, pbStoredHash)) 149 throw new InvalidDataException(KLRes.FileHeaderCorrupted); 150 151 byte[] pbHeaderHmac = ComputeHeaderHmac(pbHeader, pbHmacKey64); 152 byte[] pbStoredHmac = MemUtil.Read(sHashing, 32); 153 if((pbStoredHmac == null) || (pbStoredHmac.Length != 32)) 154 throw new EndOfStreamException(strIncomplete); 155 if(!MemUtil.ArraysEqual(pbHeaderHmac, pbStoredHmac)) 156 throw new InvalidCompositeKeyException(); 157 158 HmacBlockStream sBlocks = new HmacBlockStream(sHashing, 159 false, !m_bRepairMode, pbHmacKey64); 160 lStreams.Add(sBlocks); 161 162 sPlain = EncryptStream(sBlocks, iCipher, pbCipherKey, 163 cbEncIV, false); 164 if((sPlain == null) || (sPlain == sBlocks)) 165 throw new SecurityException(KLRes.CryptoStreamFailed); 166 } 167 lStreams.Add(sPlain); 168 169 if(m_pwDatabase.Compression == PwCompressionAlgorithm.GZip) 170 { 171 sXml = new GZipStream(sPlain, CompressionMode.Decompress); 172 lStreams.Add(sXml); 173 } 174 else sXml = sPlain; 175 176 if(m_uFileVersion >= FileVersion32_4) 177 LoadInnerHeader(sXml); // Binary header before XML 178 } 179 else if(fmt == KdbxFormat.PlainXml) 180 sXml = sHashing; 181 else { Debug.Assert(false); throw new ArgumentOutOfRangeException("fmt"); } 182 183 if(fmt == KdbxFormat.Default) 184 { 185 if(m_pbInnerRandomStreamKey == null) 186 { 187 Debug.Assert(false); 188 throw new SecurityException("Invalid inner random stream key!"); 189 } 190 191 m_randomStream = new CryptoRandomStream(m_craInnerRandomStream, 192 m_pbInnerRandomStreamKey); 193 } 194 195 #if KeePassDebug_WriteXml 196 #warning XML output is enabled! 197 /* using(FileStream fsOut = new FileStream("Raw.xml", FileMode.Create, 198 FileAccess.Write, FileShare.None)) 199 { 200 while(true) 201 { 202 int b = sXml.ReadByte(); 203 if(b == -1) throw new EndOfStreamException(); 204 fsOut.WriteByte((byte)b); 205 } 206 } */ 207 #endif 208 209 ReadXmlStreamed(sXml, sHashing); 210 // ReadXmlDom(sXml); 211 } 212 catch(CryptographicException) // Thrown on invalid padding 213 { 214 throw new CryptographicException(KLRes.FileCorrupted); 215 } 216 finally 217 { 218 if(pbCipherKey != null) MemUtil.ZeroByteArray(pbCipherKey); 219 if(pbHmacKey64 != null) MemUtil.ZeroByteArray(pbHmacKey64); 220 221 CommonCleanUpRead(lStreams, sHashing); 222 } 223 224 #if KDBX_BENCHMARK 225 swTime.Stop(); 226 MessageService.ShowInfo("Loading KDBX took " + 227 swTime.ElapsedMilliseconds.ToString() + " ms."); 228 #endif 229 } 230 CommonCleanUpRead(List<Stream> lStreams, HashingStreamEx sHashing)231 private void CommonCleanUpRead(List<Stream> lStreams, HashingStreamEx sHashing) 232 { 233 CloseStreams(lStreams); 234 235 Debug.Assert(lStreams.Contains(sHashing)); // sHashing must be closed 236 m_pbHashOfFileOnDisk = sHashing.Hash; 237 Debug.Assert(m_pbHashOfFileOnDisk != null); 238 239 CleanUpInnerRandomStream(); 240 241 // Reset memory protection settings (to always use reasonable 242 // defaults) 243 m_pwDatabase.MemoryProtection = new MemoryProtectionConfig(); 244 245 // Remove old backups (this call is required here in order to apply 246 // the default history maintenance settings for people upgrading from 247 // KeePass <= 2.14 to >= 2.15; also it ensures history integrity in 248 // case a different application has created the KDBX file and ignored 249 // the history maintenance settings) 250 m_pwDatabase.MaintainBackups(); // Don't mark database as modified 251 252 // Expand the root group, such that in case the user accidently 253 // collapses the root group he can simply reopen the database 254 PwGroup pgRoot = m_pwDatabase.RootGroup; 255 if(pgRoot != null) pgRoot.IsExpanded = true; 256 else { Debug.Assert(false); } 257 258 m_pbHashOfHeader = null; 259 } 260 LoadHeader(BinaryReaderEx br)261 private byte[] LoadHeader(BinaryReaderEx br) 262 { 263 string strPrevExcpText = br.ReadExceptionText; 264 br.ReadExceptionText = KLRes.FileHeaderCorrupted + " " + 265 KLRes.FileIncompleteExpc; 266 267 MemoryStream msHeader = new MemoryStream(); 268 Debug.Assert(br.CopyDataTo == null); 269 br.CopyDataTo = msHeader; 270 271 byte[] pbSig1 = br.ReadBytes(4); 272 uint uSig1 = MemUtil.BytesToUInt32(pbSig1); 273 byte[] pbSig2 = br.ReadBytes(4); 274 uint uSig2 = MemUtil.BytesToUInt32(pbSig2); 275 276 if((uSig1 == FileSignatureOld1) && (uSig2 == FileSignatureOld2)) 277 throw new OldFormatException(PwDefs.ShortProductName + @" 1.x", 278 OldFormatException.OldFormatType.KeePass1x); 279 280 if((uSig1 == FileSignature1) && (uSig2 == FileSignature2)) { } 281 else if((uSig1 == FileSignaturePreRelease1) && (uSig2 == 282 FileSignaturePreRelease2)) { } 283 else throw new FormatException(KLRes.FileSigInvalid); 284 285 byte[] pb = br.ReadBytes(4); 286 uint uVer = MemUtil.BytesToUInt32(pb); 287 uint uVerMajor = uVer & FileVersionCriticalMask; 288 uint uVerMinor = uVer & ~FileVersionCriticalMask; 289 const uint uVerMaxMajor = FileVersion32 & FileVersionCriticalMask; 290 const uint uVerMaxMinor = FileVersion32 & ~FileVersionCriticalMask; 291 if(uVerMajor > uVerMaxMajor) 292 throw new FormatException(KLRes.FileVersionUnsupported + 293 MessageService.NewParagraph + KLRes.FileNewVerReq); 294 if((uVerMajor == uVerMaxMajor) && (uVerMinor > uVerMaxMinor) && 295 (g_fConfirmOpenUnkVer != null)) 296 { 297 if(!g_fConfirmOpenUnkVer()) 298 throw new OperationCanceledException(); 299 } 300 m_uFileVersion = uVer; 301 302 while(true) 303 { 304 if(!ReadHeaderField(br)) break; 305 } 306 307 br.CopyDataTo = null; 308 byte[] pbHeader = msHeader.ToArray(); 309 msHeader.Close(); 310 311 br.ReadExceptionText = strPrevExcpText; 312 return pbHeader; 313 } 314 ReadHeaderField(BinaryReaderEx brSource)315 private bool ReadHeaderField(BinaryReaderEx brSource) 316 { 317 Debug.Assert(brSource != null); 318 if(brSource == null) throw new ArgumentNullException("brSource"); 319 320 byte btFieldID = brSource.ReadByte(); 321 322 int cbSize; 323 Debug.Assert(m_uFileVersion > 0); 324 if(m_uFileVersion < FileVersion32_4) 325 cbSize = (int)MemUtil.BytesToUInt16(brSource.ReadBytes(2)); 326 else cbSize = MemUtil.BytesToInt32(brSource.ReadBytes(4)); 327 if(cbSize < 0) throw new FormatException(KLRes.FileCorrupted); 328 329 byte[] pbData = MemUtil.EmptyByteArray; 330 if(cbSize > 0) pbData = brSource.ReadBytes(cbSize); 331 332 bool bResult = true; 333 KdbxHeaderFieldID kdbID = (KdbxHeaderFieldID)btFieldID; 334 switch(kdbID) 335 { 336 case KdbxHeaderFieldID.EndOfHeader: 337 bResult = false; // Returning false indicates end of header 338 break; 339 340 case KdbxHeaderFieldID.CipherID: 341 SetCipher(pbData); 342 break; 343 344 case KdbxHeaderFieldID.CompressionFlags: 345 SetCompressionFlags(pbData); 346 break; 347 348 case KdbxHeaderFieldID.MasterSeed: 349 m_pbMasterSeed = pbData; 350 CryptoRandom.Instance.AddEntropy(pbData); 351 break; 352 353 // Obsolete; for backward compatibility only 354 case KdbxHeaderFieldID.TransformSeed: 355 Debug.Assert(m_uFileVersion < FileVersion32_4); 356 357 AesKdf kdfS = new AesKdf(); 358 if(!m_pwDatabase.KdfParameters.KdfUuid.Equals(kdfS.Uuid)) 359 m_pwDatabase.KdfParameters = kdfS.GetDefaultParameters(); 360 361 // m_pbTransformSeed = pbData; 362 m_pwDatabase.KdfParameters.SetByteArray(AesKdf.ParamSeed, pbData); 363 364 CryptoRandom.Instance.AddEntropy(pbData); 365 break; 366 367 // Obsolete; for backward compatibility only 368 case KdbxHeaderFieldID.TransformRounds: 369 Debug.Assert(m_uFileVersion < FileVersion32_4); 370 371 AesKdf kdfR = new AesKdf(); 372 if(!m_pwDatabase.KdfParameters.KdfUuid.Equals(kdfR.Uuid)) 373 m_pwDatabase.KdfParameters = kdfR.GetDefaultParameters(); 374 375 // m_pwDatabase.KeyEncryptionRounds = MemUtil.BytesToUInt64(pbData); 376 m_pwDatabase.KdfParameters.SetUInt64(AesKdf.ParamRounds, 377 MemUtil.BytesToUInt64(pbData)); 378 break; 379 380 case KdbxHeaderFieldID.EncryptionIV: 381 m_pbEncryptionIV = pbData; 382 break; 383 384 case KdbxHeaderFieldID.InnerRandomStreamKey: 385 Debug.Assert(m_uFileVersion < FileVersion32_4); 386 Debug.Assert(m_pbInnerRandomStreamKey == null); 387 m_pbInnerRandomStreamKey = pbData; 388 CryptoRandom.Instance.AddEntropy(pbData); 389 break; 390 391 case KdbxHeaderFieldID.StreamStartBytes: 392 Debug.Assert(m_uFileVersion < FileVersion32_4); 393 m_pbStreamStartBytes = pbData; 394 break; 395 396 case KdbxHeaderFieldID.InnerRandomStreamID: 397 Debug.Assert(m_uFileVersion < FileVersion32_4); 398 SetInnerRandomStreamID(pbData); 399 break; 400 401 case KdbxHeaderFieldID.KdfParameters: 402 m_pwDatabase.KdfParameters = KdfParameters.DeserializeExt(pbData); 403 break; 404 405 case KdbxHeaderFieldID.PublicCustomData: 406 Debug.Assert(m_pwDatabase.PublicCustomData.Count == 0); 407 m_pwDatabase.PublicCustomData = VariantDictionary.Deserialize(pbData); 408 break; 409 410 default: 411 Debug.Assert(false); 412 if(m_slLogger != null) 413 m_slLogger.SetText(KLRes.UnknownHeaderId + ": " + 414 kdbID.ToString() + "!", LogStatusType.Warning); 415 break; 416 } 417 418 return bResult; 419 } 420 LoadInnerHeader(Stream s)421 private void LoadInnerHeader(Stream s) 422 { 423 BinaryReaderEx br = new BinaryReaderEx(s, StrUtil.Utf8, 424 KLRes.FileCorrupted + " " + KLRes.FileIncompleteExpc); 425 426 while(true) 427 { 428 if(!ReadInnerHeaderField(br)) break; 429 } 430 } 431 ReadInnerHeaderField(BinaryReaderEx br)432 private bool ReadInnerHeaderField(BinaryReaderEx br) 433 { 434 Debug.Assert(br != null); 435 if(br == null) throw new ArgumentNullException("br"); 436 437 byte btFieldID = br.ReadByte(); 438 439 int cbSize = MemUtil.BytesToInt32(br.ReadBytes(4)); 440 if(cbSize < 0) throw new FormatException(KLRes.FileCorrupted); 441 442 byte[] pbData = MemUtil.EmptyByteArray; 443 if(cbSize > 0) pbData = br.ReadBytes(cbSize); 444 445 bool bResult = true; 446 KdbxInnerHeaderFieldID kdbID = (KdbxInnerHeaderFieldID)btFieldID; 447 switch(kdbID) 448 { 449 case KdbxInnerHeaderFieldID.EndOfHeader: 450 bResult = false; // Returning false indicates end of header 451 break; 452 453 case KdbxInnerHeaderFieldID.InnerRandomStreamID: 454 SetInnerRandomStreamID(pbData); 455 break; 456 457 case KdbxInnerHeaderFieldID.InnerRandomStreamKey: 458 Debug.Assert(m_pbInnerRandomStreamKey == null); 459 m_pbInnerRandomStreamKey = pbData; 460 CryptoRandom.Instance.AddEntropy(pbData); 461 break; 462 463 case KdbxInnerHeaderFieldID.Binary: 464 if(pbData.Length < 1) throw new FormatException(); 465 KdbxBinaryFlags f = (KdbxBinaryFlags)pbData[0]; 466 bool bProt = ((f & KdbxBinaryFlags.Protected) != KdbxBinaryFlags.None); 467 468 ProtectedBinary pb = new ProtectedBinary(bProt, pbData, 469 1, pbData.Length - 1); 470 Debug.Assert(m_pbsBinaries.Find(pb) < 0); // No deduplication? 471 m_pbsBinaries.Add(pb); 472 473 if(bProt) MemUtil.ZeroByteArray(pbData); 474 break; 475 476 default: 477 Debug.Assert(false); 478 break; 479 } 480 481 return bResult; 482 } 483 SetCipher(byte[] pbID)484 private void SetCipher(byte[] pbID) 485 { 486 if((pbID == null) || (pbID.Length != (int)PwUuid.UuidSize)) 487 throw new FormatException(KLRes.FileUnknownCipher); 488 489 m_pwDatabase.DataCipherUuid = new PwUuid(pbID); 490 } 491 SetCompressionFlags(byte[] pbFlags)492 private void SetCompressionFlags(byte[] pbFlags) 493 { 494 int nID = (int)MemUtil.BytesToUInt32(pbFlags); 495 if((nID < 0) || (nID >= (int)PwCompressionAlgorithm.Count)) 496 throw new FormatException(KLRes.FileUnknownCompression); 497 498 m_pwDatabase.Compression = (PwCompressionAlgorithm)nID; 499 } 500 SetInnerRandomStreamID(byte[] pbID)501 private void SetInnerRandomStreamID(byte[] pbID) 502 { 503 uint uID = MemUtil.BytesToUInt32(pbID); 504 if(uID >= (uint)CrsAlgorithm.Count) 505 throw new FormatException(KLRes.FileUnknownCipher); 506 507 m_craInnerRandomStream = (CrsAlgorithm)uID; 508 } 509 ReadGroup(Stream msData, PwDatabase pdContext, bool bCopyIcons, bool bNewUuids, bool bSetCreatedNow)510 internal static PwGroup ReadGroup(Stream msData, PwDatabase pdContext, 511 bool bCopyIcons, bool bNewUuids, bool bSetCreatedNow) 512 { 513 PwDatabase pd = new PwDatabase(); 514 pd.New(new IOConnectionInfo(), new CompositeKey()); 515 516 KdbxFile f = new KdbxFile(pd); 517 f.Load(msData, KdbxFormat.PlainXml, null); 518 519 if(bCopyIcons) 520 PwDatabase.CopyCustomIcons(pd, pdContext, pd.RootGroup, true); 521 522 if(bNewUuids) 523 { 524 pd.RootGroup.Uuid = new PwUuid(true); 525 pd.RootGroup.CreateNewItemUuids(true, true, true); 526 } 527 528 if(bSetCreatedNow) pd.RootGroup.SetCreatedNow(true); 529 530 return pd.RootGroup; 531 } 532 533 [Obsolete] ReadEntries(Stream msData)534 public static List<PwEntry> ReadEntries(Stream msData) 535 { 536 return ReadEntries(msData, null, false); 537 } 538 539 [Obsolete] ReadEntries(PwDatabase pdContext, Stream msData)540 public static List<PwEntry> ReadEntries(PwDatabase pdContext, Stream msData) 541 { 542 return ReadEntries(msData, pdContext, true); 543 } 544 ReadEntries(Stream msData, PwDatabase pdContext, bool bCopyIcons)545 public static List<PwEntry> ReadEntries(Stream msData, PwDatabase pdContext, 546 bool bCopyIcons) 547 { 548 if(msData == null) { Debug.Assert(false); return new List<PwEntry>(); } 549 550 PwGroup pg = ReadGroup(msData, pdContext, bCopyIcons, true, true); 551 return pg.GetEntries(true).CloneShallowToList(); 552 } 553 } 554 } 555