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