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