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 using System; 21 using System.Collections.Generic; 22 using System.Diagnostics; 23 using System.Globalization; 24 using System.IO; 25 using System.Security; 26 using System.Text; 27 using System.Xml; 28 29 #if !KeePassUAP 30 using System.Security.Cryptography; 31 #endif 32 33 using KeePassLib.Collections; 34 using KeePassLib.Cryptography; 35 using KeePassLib.Cryptography.Cipher; 36 using KeePassLib.Cryptography.KeyDerivation; 37 using KeePassLib.Delegates; 38 using KeePassLib.Interfaces; 39 using KeePassLib.Resources; 40 using KeePassLib.Security; 41 using KeePassLib.Utility; 42 43 namespace KeePassLib.Serialization 44 { 45 /// <summary> 46 /// The <c>KdbxFile</c> class supports saving the data to various 47 /// formats. 48 /// </summary> 49 public enum KdbxFormat 50 { 51 /// <summary> 52 /// The default, encrypted file format. 53 /// </summary> 54 Default = 0, 55 56 /// <summary> 57 /// Use this flag when exporting data to a plain-text XML file. 58 /// </summary> 59 PlainXml 60 } 61 62 /// <summary> 63 /// Serialization to KeePass KDBX files. 64 /// </summary> 65 public sealed partial class KdbxFile 66 { 67 /// <summary> 68 /// File identifier, first 32-bit value. 69 /// </summary> 70 internal const uint FileSignature1 = 0x9AA2D903; 71 72 /// <summary> 73 /// File identifier, second 32-bit value. 74 /// </summary> 75 internal const uint FileSignature2 = 0xB54BFB67; 76 77 /// <summary> 78 /// Maximum supported version of database files. 79 /// KeePass 2.07 has version 1.01, 2.08 has 1.02, 2.09 has 2.00, 80 /// 2.10 has 2.02, 2.11 has 2.04, 2.15 has 3.00, 2.20 has 3.01. 81 /// The first 2 bytes are critical (i.e. loading will fail, if the 82 /// file version is too high), the last 2 bytes are informational. 83 /// </summary> 84 internal const uint FileVersion32 = 0x00040001; 85 86 private const uint FileVersion32_4_1 = 0x00040001; // 4.1 87 private const uint FileVersion32_4 = 0x00040000; // 4.0 88 internal const uint FileVersion32_3_1 = 0x00030001; // 3.1 89 90 private const uint FileVersionCriticalMask = 0xFFFF0000; 91 92 // KeePass 1.x signature 93 internal const uint FileSignatureOld1 = 0x9AA2D903; 94 internal const uint FileSignatureOld2 = 0xB54BFB65; 95 // KeePass 2.x pre-release (alpha and beta) signature 96 internal const uint FileSignaturePreRelease1 = 0x9AA2D903; 97 internal const uint FileSignaturePreRelease2 = 0xB54BFB66; 98 99 private const string ElemDocNode = "KeePassFile"; 100 private const string ElemMeta = "Meta"; 101 private const string ElemRoot = "Root"; 102 private const string ElemGroup = "Group"; 103 internal const string ElemEntry = "Entry"; 104 105 private const string ElemGenerator = "Generator"; 106 private const string ElemHeaderHash = "HeaderHash"; 107 private const string ElemSettingsChanged = "SettingsChanged"; 108 private const string ElemDbName = "DatabaseName"; 109 private const string ElemDbNameChanged = "DatabaseNameChanged"; 110 private const string ElemDbDesc = "DatabaseDescription"; 111 private const string ElemDbDescChanged = "DatabaseDescriptionChanged"; 112 private const string ElemDbDefaultUser = "DefaultUserName"; 113 private const string ElemDbDefaultUserChanged = "DefaultUserNameChanged"; 114 private const string ElemDbMntncHistoryDays = "MaintenanceHistoryDays"; 115 private const string ElemDbColor = "Color"; 116 private const string ElemDbKeyChanged = "MasterKeyChanged"; 117 private const string ElemDbKeyChangeRec = "MasterKeyChangeRec"; 118 private const string ElemDbKeyChangeForce = "MasterKeyChangeForce"; 119 private const string ElemDbKeyChangeForceOnce = "MasterKeyChangeForceOnce"; 120 private const string ElemRecycleBinEnabled = "RecycleBinEnabled"; 121 private const string ElemRecycleBinUuid = "RecycleBinUUID"; 122 private const string ElemRecycleBinChanged = "RecycleBinChanged"; 123 private const string ElemEntryTemplatesGroup = "EntryTemplatesGroup"; 124 private const string ElemEntryTemplatesGroupChanged = "EntryTemplatesGroupChanged"; 125 private const string ElemHistoryMaxItems = "HistoryMaxItems"; 126 private const string ElemHistoryMaxSize = "HistoryMaxSize"; 127 private const string ElemLastSelectedGroup = "LastSelectedGroup"; 128 private const string ElemLastTopVisibleGroup = "LastTopVisibleGroup"; 129 130 private const string ElemMemoryProt = "MemoryProtection"; 131 private const string ElemProtTitle = "ProtectTitle"; 132 private const string ElemProtUserName = "ProtectUserName"; 133 private const string ElemProtPassword = "ProtectPassword"; 134 private const string ElemProtUrl = "ProtectURL"; 135 private const string ElemProtNotes = "ProtectNotes"; 136 // private const string ElemProtAutoHide = "AutoEnableVisualHiding"; 137 138 private const string ElemCustomIcons = "CustomIcons"; 139 private const string ElemCustomIconItem = "Icon"; 140 private const string ElemCustomIconItemID = "UUID"; 141 private const string ElemCustomIconItemData = "Data"; 142 143 private const string ElemAutoType = "AutoType"; 144 private const string ElemHistory = "History"; 145 146 private const string ElemName = "Name"; 147 private const string ElemNotes = "Notes"; 148 internal const string ElemUuid = "UUID"; 149 private const string ElemIcon = "IconID"; 150 private const string ElemCustomIconID = "CustomIconUUID"; 151 private const string ElemFgColor = "ForegroundColor"; 152 private const string ElemBgColor = "BackgroundColor"; 153 private const string ElemOverrideUrl = "OverrideURL"; 154 private const string ElemQualityCheck = "QualityCheck"; 155 private const string ElemTimes = "Times"; 156 private const string ElemTags = "Tags"; 157 158 private const string ElemCreationTime = "CreationTime"; 159 private const string ElemLastModTime = "LastModificationTime"; 160 private const string ElemLastAccessTime = "LastAccessTime"; 161 private const string ElemExpiryTime = "ExpiryTime"; 162 private const string ElemExpires = "Expires"; 163 private const string ElemUsageCount = "UsageCount"; 164 private const string ElemLocationChanged = "LocationChanged"; 165 166 private const string ElemPreviousParentGroup = "PreviousParentGroup"; 167 168 private const string ElemGroupDefaultAutoTypeSeq = "DefaultAutoTypeSequence"; 169 private const string ElemEnableAutoType = "EnableAutoType"; 170 private const string ElemEnableSearching = "EnableSearching"; 171 172 private const string ElemString = "String"; 173 private const string ElemBinary = "Binary"; 174 private const string ElemKey = "Key"; 175 private const string ElemValue = "Value"; 176 177 private const string ElemAutoTypeEnabled = "Enabled"; 178 private const string ElemAutoTypeObfuscation = "DataTransferObfuscation"; 179 private const string ElemAutoTypeDefaultSeq = "DefaultSequence"; 180 private const string ElemAutoTypeItem = "Association"; 181 private const string ElemWindow = "Window"; 182 private const string ElemKeystrokeSequence = "KeystrokeSequence"; 183 184 private const string ElemBinaries = "Binaries"; 185 186 private const string AttrId = "ID"; 187 private const string AttrRef = "Ref"; 188 private const string AttrProtected = "Protected"; 189 private const string AttrProtectedInMemPlainXml = "ProtectInMemory"; 190 private const string AttrCompressed = "Compressed"; 191 192 private const string ElemIsExpanded = "IsExpanded"; 193 private const string ElemLastTopVisibleEntry = "LastTopVisibleEntry"; 194 195 private const string ElemDeletedObjects = "DeletedObjects"; 196 private const string ElemDeletedObject = "DeletedObject"; 197 private const string ElemDeletionTime = "DeletionTime"; 198 199 private const string ValFalse = "False"; 200 private const string ValTrue = "True"; 201 202 private const string ElemCustomData = "CustomData"; 203 private const string ElemStringDictExItem = "Item"; 204 205 private PwDatabase m_pwDatabase; // Not null, see constructor 206 private bool m_bUsedOnce = false; 207 208 private XmlWriter m_xmlWriter = null; 209 private CryptoRandomStream m_randomStream = null; 210 private KdbxFormat m_format = KdbxFormat.Default; 211 private IStatusLogger m_slLogger = null; 212 213 private uint m_uFileVersion = 0; 214 private byte[] m_pbMasterSeed = null; 215 // private byte[] m_pbTransformSeed = null; 216 private byte[] m_pbEncryptionIV = null; 217 private byte[] m_pbStreamStartBytes = null; 218 219 // ArcFourVariant only for backward compatibility; KeePass defaults 220 // to a more secure algorithm when *writing* databases 221 private CrsAlgorithm m_craInnerRandomStream = CrsAlgorithm.ArcFourVariant; 222 private byte[] m_pbInnerRandomStreamKey = null; 223 224 private ProtectedBinarySet m_pbsBinaries = null; 225 226 private byte[] m_pbHashOfHeader = null; 227 private byte[] m_pbHashOfFileOnDisk = null; 228 229 private readonly DateTime m_dtNow = DateTime.UtcNow; // Cache current time 230 231 private const uint NeutralLanguageOffset = 0x100000; // 2^20, see 32-bit Unicode specs 232 private const uint NeutralLanguageIDSec = 0x7DC5C; // See 32-bit Unicode specs 233 private const uint NeutralLanguageID = NeutralLanguageOffset + NeutralLanguageIDSec; 234 private static bool g_bLocalizedNames = false; 235 236 private enum KdbxHeaderFieldID : byte 237 { 238 EndOfHeader = 0, 239 Comment = 1, 240 CipherID = 2, 241 CompressionFlags = 3, 242 MasterSeed = 4, 243 TransformSeed = 5, // KDBX 3.1, for backward compatibility only 244 TransformRounds = 6, // KDBX 3.1, for backward compatibility only 245 EncryptionIV = 7, 246 InnerRandomStreamKey = 8, // KDBX 3.1, for backward compatibility only 247 StreamStartBytes = 9, // KDBX 3.1, for backward compatibility only 248 InnerRandomStreamID = 10, // KDBX 3.1, for backward compatibility only 249 KdfParameters = 11, // KDBX 4, superseding Transform* 250 PublicCustomData = 12 // KDBX 4 251 } 252 253 // Inner header in KDBX >= 4 files 254 private enum KdbxInnerHeaderFieldID : byte 255 { 256 EndOfHeader = 0, 257 InnerRandomStreamID = 1, // Supersedes KdbxHeaderFieldID.InnerRandomStreamID 258 InnerRandomStreamKey = 2, // Supersedes KdbxHeaderFieldID.InnerRandomStreamKey 259 Binary = 3 260 } 261 262 [Flags] 263 private enum KdbxBinaryFlags : byte 264 { 265 None = 0, 266 Protected = 1 267 } 268 269 private static GFunc<bool> g_fConfirmOpenUnkVer = null; 270 internal static GFunc<bool> ConfirmOpenUnknownVersion 271 { 272 get { return g_fConfirmOpenUnkVer; } 273 set { g_fConfirmOpenUnkVer = value; } 274 } 275 276 public byte[] HashOfFileOnDisk 277 { 278 get { return m_pbHashOfFileOnDisk; } 279 } 280 281 private bool m_bRepairMode = false; 282 public bool RepairMode 283 { 284 get { return m_bRepairMode; } 285 set { m_bRepairMode = value; } 286 } 287 288 private uint m_uForceVersion = 0; 289 internal uint ForceVersion 290 { 291 get { return m_uForceVersion; } 292 set { m_uForceVersion = value; } 293 } 294 295 private string m_strDetachBins = null; 296 /// <summary> 297 /// Detach binaries when opening a file. If this isn't <c>null</c>, 298 /// all binaries are saved to the specified path and are removed 299 /// from the database. 300 /// </summary> 301 public string DetachBinaries 302 { 303 get { return m_strDetachBins; } 304 set { m_strDetachBins = value; } 305 } 306 307 /// <summary> 308 /// Default constructor. 309 /// </summary> 310 /// <param name="pwDataStore">The <c>PwDatabase</c> instance that the 311 /// class will load file data into or use to create a KDBX file.</param> KdbxFile(PwDatabase pwDataStore)312 public KdbxFile(PwDatabase pwDataStore) 313 { 314 Debug.Assert(pwDataStore != null); 315 if(pwDataStore == null) throw new ArgumentNullException("pwDataStore"); 316 317 m_pwDatabase = pwDataStore; 318 } 319 320 /// <summary> 321 /// Call this once to determine the current localization settings. 322 /// </summary> DetermineLanguageId()323 public static void DetermineLanguageId() 324 { 325 // Test if localized names should be used. If localized names are used, 326 // the g_bLocalizedNames value must be set to true. By default, localized 327 // names should be used (otherwise characters could be corrupted 328 // because of different code pages). 329 unchecked 330 { 331 uint uTest = 0; 332 foreach(char ch in PwDatabase.LocalizedAppName) 333 uTest = uTest * 5 + ch; 334 335 g_bLocalizedNames = (uTest != NeutralLanguageID); 336 } 337 } 338 GetMinKdbxVersion()339 private uint GetMinKdbxVersion() 340 { 341 if(m_uForceVersion != 0) return m_uForceVersion; 342 343 // See also KeePassKdb2x3.Export (KDBX 3.1 export module) 344 345 uint uMin = 0; 346 347 GroupHandler gh = delegate(PwGroup pg) 348 { 349 if(pg == null) { Debug.Assert(false); return true; } 350 351 if(pg.Tags.Count != 0) 352 uMin = Math.Max(uMin, FileVersion32_4_1); 353 if(pg.CustomData.Count != 0) 354 uMin = Math.Max(uMin, FileVersion32_4); 355 356 return true; 357 }; 358 359 EntryHandler eh = delegate(PwEntry pe) 360 { 361 if(pe == null) { Debug.Assert(false); return true; } 362 363 if(!pe.QualityCheck) 364 uMin = Math.Max(uMin, FileVersion32_4_1); 365 if(pe.CustomData.Count != 0) 366 uMin = Math.Max(uMin, FileVersion32_4); 367 368 return true; 369 }; 370 371 gh(m_pwDatabase.RootGroup); 372 m_pwDatabase.RootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh); 373 374 if(uMin >= FileVersion32_4_1) return uMin; // All below is <= 4.1 375 376 foreach(PwCustomIcon ci in m_pwDatabase.CustomIcons) 377 { 378 if((ci.Name.Length != 0) || ci.LastModificationTime.HasValue) 379 return FileVersion32_4_1; 380 } 381 382 foreach(KeyValuePair<string, string> kvp in m_pwDatabase.CustomData) 383 { 384 DateTime? odt = m_pwDatabase.CustomData.GetLastModificationTime(kvp.Key); 385 if(odt.HasValue) return FileVersion32_4_1; 386 } 387 388 if(uMin >= FileVersion32_4) return uMin; // All below is <= 4 389 390 if(m_pwDatabase.DataCipherUuid.Equals(ChaCha20Engine.ChaCha20Uuid)) 391 return FileVersion32_4; 392 393 AesKdf kdfAes = new AesKdf(); 394 if(!m_pwDatabase.KdfParameters.KdfUuid.Equals(kdfAes.Uuid)) 395 return FileVersion32_4; 396 397 if(m_pwDatabase.PublicCustomData.Count != 0) 398 return FileVersion32_4; 399 400 return FileVersion32_3_1; // KDBX 3.1 is sufficient 401 } 402 ComputeKeys(out byte[] pbCipherKey, int cbCipherKey, out byte[] pbHmacKey64)403 private void ComputeKeys(out byte[] pbCipherKey, int cbCipherKey, 404 out byte[] pbHmacKey64) 405 { 406 byte[] pbCmp = new byte[32 + 32 + 1]; 407 try 408 { 409 Debug.Assert(m_pbMasterSeed != null); 410 if(m_pbMasterSeed == null) 411 throw new ArgumentNullException("m_pbMasterSeed"); 412 Debug.Assert(m_pbMasterSeed.Length == 32); 413 if(m_pbMasterSeed.Length != 32) 414 throw new FormatException(KLRes.MasterSeedLengthInvalid); 415 Array.Copy(m_pbMasterSeed, 0, pbCmp, 0, 32); 416 417 Debug.Assert(m_pwDatabase != null); 418 Debug.Assert(m_pwDatabase.MasterKey != null); 419 ProtectedBinary pbinUser = m_pwDatabase.MasterKey.GenerateKey32Ex( 420 m_pwDatabase.KdfParameters, m_slLogger); 421 Debug.Assert(pbinUser != null); 422 if(pbinUser == null) 423 throw new SecurityException(KLRes.InvalidCompositeKey); 424 byte[] pUserKey32 = pbinUser.ReadData(); 425 if((pUserKey32 == null) || (pUserKey32.Length != 32)) 426 throw new SecurityException(KLRes.InvalidCompositeKey); 427 Array.Copy(pUserKey32, 0, pbCmp, 32, 32); 428 MemUtil.ZeroByteArray(pUserKey32); 429 430 pbCipherKey = CryptoUtil.ResizeKey(pbCmp, 0, 64, cbCipherKey); 431 432 pbCmp[64] = 1; 433 using(SHA512Managed h = new SHA512Managed()) 434 { 435 pbHmacKey64 = h.ComputeHash(pbCmp); 436 } 437 } 438 finally { MemUtil.ZeroByteArray(pbCmp); } 439 } 440 GetCipher(out int cbEncKey, out int cbEncIV)441 private ICipherEngine GetCipher(out int cbEncKey, out int cbEncIV) 442 { 443 PwUuid pu = m_pwDatabase.DataCipherUuid; 444 ICipherEngine iCipher = CipherPool.GlobalPool.GetCipher(pu); 445 if(iCipher == null) // CryptographicExceptions are translated to "file corrupted" 446 throw new Exception(KLRes.FileUnknownCipher + 447 MessageService.NewParagraph + KLRes.FileNewVerOrPlgReq + 448 MessageService.NewParagraph + "UUID: " + pu.ToHexString() + "."); 449 450 ICipherEngine2 iCipher2 = (iCipher as ICipherEngine2); 451 if(iCipher2 != null) 452 { 453 cbEncKey = iCipher2.KeyLength; 454 if(cbEncKey < 0) throw new InvalidOperationException("EncKey.Length"); 455 456 cbEncIV = iCipher2.IVLength; 457 if(cbEncIV < 0) throw new InvalidOperationException("EncIV.Length"); 458 } 459 else 460 { 461 cbEncKey = 32; 462 cbEncIV = 16; 463 } 464 465 return iCipher; 466 } 467 EncryptStream(Stream s, ICipherEngine iCipher, byte[] pbKey, int cbIV, bool bEncrypt)468 private Stream EncryptStream(Stream s, ICipherEngine iCipher, 469 byte[] pbKey, int cbIV, bool bEncrypt) 470 { 471 byte[] pbIV = (m_pbEncryptionIV ?? MemUtil.EmptyByteArray); 472 if(pbIV.Length != cbIV) 473 { 474 Debug.Assert(false); 475 throw new Exception(KLRes.FileCorrupted); 476 } 477 478 if(bEncrypt) 479 return iCipher.EncryptStream(s, pbKey, pbIV); 480 return iCipher.DecryptStream(s, pbKey, pbIV); 481 } 482 ComputeHeaderHmac(byte[] pbHeader, byte[] pbKey)483 private byte[] ComputeHeaderHmac(byte[] pbHeader, byte[] pbKey) 484 { 485 byte[] pbHeaderHmac; 486 byte[] pbBlockKey = HmacBlockStream.GetHmacKey64( 487 pbKey, ulong.MaxValue); 488 using(HMACSHA256 h = new HMACSHA256(pbBlockKey)) 489 { 490 pbHeaderHmac = h.ComputeHash(pbHeader); 491 } 492 MemUtil.ZeroByteArray(pbBlockKey); 493 494 return pbHeaderHmac; 495 } 496 CloseStreams(List<Stream> lStreams)497 private void CloseStreams(List<Stream> lStreams) 498 { 499 if(lStreams == null) { Debug.Assert(false); return; } 500 501 // Typically, closing a stream also closes its base 502 // stream; however, there may be streams that do not 503 // do this (e.g. some cipher plugin), thus for safety 504 // we close all streams manually, from the innermost 505 // to the outermost 506 507 for(int i = lStreams.Count - 1; i >= 0; --i) 508 { 509 // Check for duplicates 510 Debug.Assert((lStreams.IndexOf(lStreams[i]) == i) && 511 (lStreams.LastIndexOf(lStreams[i]) == i)); 512 513 try { lStreams[i].Close(); } 514 catch(Exception) { Debug.Assert(false); } 515 } 516 517 // Do not clear the list 518 } 519 CleanUpInnerRandomStream()520 private void CleanUpInnerRandomStream() 521 { 522 if(m_randomStream != null) m_randomStream.Dispose(); 523 524 if(m_pbInnerRandomStreamKey != null) 525 MemUtil.ZeroByteArray(m_pbInnerRandomStreamKey); 526 } 527 SaveBinary(string strName, ProtectedBinary pb, string strSaveDir)528 private static void SaveBinary(string strName, ProtectedBinary pb, 529 string strSaveDir) 530 { 531 if(pb == null) { Debug.Assert(false); return; } 532 533 strName = UrlUtil.GetSafeFileName(strName); 534 535 string strPath; 536 int iTry = 1; 537 do 538 { 539 strPath = UrlUtil.EnsureTerminatingSeparator(strSaveDir, false); 540 541 string strDesc = UrlUtil.StripExtension(strName); 542 string strExt = UrlUtil.GetExtension(strName); 543 544 strPath += strDesc; 545 if(iTry > 1) 546 strPath += " (" + iTry.ToString(NumberFormatInfo.InvariantInfo) + 547 ")"; 548 549 if(!string.IsNullOrEmpty(strExt)) strPath += "." + strExt; 550 551 ++iTry; 552 } 553 while(File.Exists(strPath)); 554 555 byte[] pbData = pb.ReadData(); 556 try { File.WriteAllBytes(strPath, pbData); } 557 finally { if(pb.IsProtected) MemUtil.ZeroByteArray(pbData); } 558 } 559 } 560 } 561