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.ComponentModel;
23 using System.Diagnostics;
24 using System.IO;
25 using System.Text;
26 
27 #if (!KeePassLibSD && !KeePassUAP)
28 using System.Security.AccessControl;
29 #endif
30 
31 using Microsoft.Win32;
32 
33 using KeePassLib.Cryptography;
34 using KeePassLib.Delegates;
35 using KeePassLib.Native;
36 using KeePassLib.Resources;
37 using KeePassLib.Utility;
38 
39 namespace KeePassLib.Serialization
40 {
41 	public sealed class FileTransactionEx : IDisposable
42 	{
43 		private bool m_bTransacted;
44 		private IOConnectionInfo m_iocBase; // Null means disposed
45 		private IOConnectionInfo m_iocTemp;
46 		private IOConnectionInfo m_iocTxfMidFallback = null; // Null <=> TxF not used
47 
48 		private bool m_bMadeUnhidden = false;
49 		private List<IOConnectionInfo> m_lToDelete = new List<IOConnectionInfo>();
50 
51 		internal const string StrTempSuffix = ".tmp";
52 		private static readonly string StrTxfTempPrefix = PwDefs.ShortProductName + "_TxF_";
53 		internal const string StrTxfTempSuffix = ".tmp";
54 
55 		private static Dictionary<string, bool> g_dEnabled =
56 			new Dictionary<string, bool>(StrUtil.CaseIgnoreComparer);
57 
58 		private static bool g_bExtraSafe = false;
59 		internal static bool ExtraSafe
60 		{
61 			get { return g_bExtraSafe; }
62 			set { g_bExtraSafe = value; }
63 		}
64 
FileTransactionEx(IOConnectionInfo iocBaseFile)65 		public FileTransactionEx(IOConnectionInfo iocBaseFile) :
66 			this(iocBaseFile, true)
67 		{
68 		}
69 
FileTransactionEx(IOConnectionInfo iocBaseFile, bool bTransacted)70 		public FileTransactionEx(IOConnectionInfo iocBaseFile, bool bTransacted)
71 		{
72 			if(iocBaseFile == null) throw new ArgumentNullException("iocBaseFile");
73 
74 			m_bTransacted = bTransacted;
75 
76 			m_iocBase = iocBaseFile.CloneDeep();
77 			if(m_iocBase.IsLocalFile())
78 				m_iocBase.Path = UrlUtil.GetShortestAbsolutePath(m_iocBase.Path);
79 
80 			string strPath = m_iocBase.Path;
81 
82 			if(m_iocBase.IsLocalFile())
83 			{
84 				try
85 				{
86 					if(File.Exists(strPath))
87 					{
88 						// Symbolic links are realized via reparse points;
89 						// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365503.aspx
90 						// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365680.aspx
91 						// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365006.aspx
92 						// Performing a file transaction on a symbolic link
93 						// would delete/replace the symbolic link instead of
94 						// writing to its target
95 						FileAttributes fa = File.GetAttributes(strPath);
96 						if((long)(fa & FileAttributes.ReparsePoint) != 0)
97 							m_bTransacted = false;
98 					}
99 					else
100 					{
101 						// If the base and the temporary file are in different
102 						// folders and the base file doesn't exist (i.e. we can't
103 						// backup the ACL), a transaction would cause the new file
104 						// to have the default ACL of the temporary folder instead
105 						// of the one of the base folder; therefore, we don't use
106 						// a transaction when the base file doesn't exist (this
107 						// also results in other applications monitoring the folder
108 						// to see one file creation only)
109 						m_bTransacted = false;
110 					}
111 				}
112 				catch(Exception) { Debug.Assert(false); }
113 			}
114 
115 #if !KeePassUAP
116 			// Prevent transactions for FTP URLs under .NET 4.0 in order to
117 			// avoid/workaround .NET bug 621450:
118 			// https://connect.microsoft.com/VisualStudio/feedback/details/621450/problem-renaming-file-on-ftp-server-using-ftpwebrequest-in-net-framework-4-0-vs2010-only
119 			if(strPath.StartsWith("ftp:", StrUtil.CaseIgnoreCmp) &&
120 				(Environment.Version.Major >= 4) && !NativeLib.IsUnix())
121 				m_bTransacted = false;
122 #endif
123 
124 			foreach(KeyValuePair<string, bool> kvp in g_dEnabled)
125 			{
126 				if(strPath.StartsWith(kvp.Key, StrUtil.CaseIgnoreCmp))
127 				{
128 					m_bTransacted = kvp.Value;
129 					break;
130 				}
131 			}
132 
133 			if(m_bTransacted)
134 			{
135 				m_iocTemp = m_iocBase.CloneDeep();
136 				m_iocTemp.Path += StrTempSuffix;
137 
138 				TxfPrepare(); // Adjusts m_iocTemp
139 			}
140 			else m_iocTemp = m_iocBase;
141 		}
142 
~FileTransactionEx()143 		~FileTransactionEx()
144 		{
145 			Dispose(false);
146 		}
147 
Dispose()148 		public void Dispose()
149 		{
150 			Dispose(true);
151 			GC.SuppressFinalize(this);
152 		}
153 
Dispose(bool bDisposing)154 		private void Dispose(bool bDisposing)
155 		{
156 			m_iocBase = null;
157 			if(!bDisposing) return;
158 
159 			try
160 			{
161 				foreach(IOConnectionInfo ioc in m_lToDelete)
162 				{
163 					if(IOConnection.FileExists(ioc, false))
164 						IOConnection.DeleteFile(ioc);
165 				}
166 
167 				m_lToDelete.Clear();
168 			}
169 			catch(Exception) { Debug.Assert(false); }
170 		}
171 
OpenWrite()172 		public Stream OpenWrite()
173 		{
174 			if(m_iocBase == null) { Debug.Assert(false); throw new ObjectDisposedException(null); }
175 
176 			if(!m_bTransacted) m_bMadeUnhidden |= UrlUtil.UnhideFile(m_iocTemp.Path);
177 
178 			return IOConnection.OpenWrite(m_iocTemp);
179 		}
180 
CommitWrite()181 		public void CommitWrite()
182 		{
183 			if(m_iocBase == null) { Debug.Assert(false); throw new ObjectDisposedException(null); }
184 
185 			if(!m_bTransacted)
186 			{
187 				if(m_bMadeUnhidden) UrlUtil.HideFile(m_iocTemp.Path, true);
188 			}
189 			else CommitWriteTransaction();
190 
191 			m_iocBase = null; // Dispose
192 		}
193 
CommitWriteTransaction()194 		private void CommitWriteTransaction()
195 		{
196 			if(g_bExtraSafe)
197 			{
198 				if(!IOConnection.FileExists(m_iocTemp))
199 					throw new FileNotFoundException(m_iocTemp.Path +
200 						MessageService.NewLine + KLRes.FileSaveFailed);
201 			}
202 
203 			bool bMadeUnhidden = UrlUtil.UnhideFile(m_iocBase.Path);
204 
205 #if !KeePassUAP
206 			// 'All' includes 'Audit' (SACL), which requires SeSecurityPrivilege,
207 			// which we usually don't have and therefore get an exception;
208 			// trying to set 'Owner' or 'Group' can result in an
209 			// UnauthorizedAccessException; thus we restore 'Access' (DACL) only
210 			const AccessControlSections acs = AccessControlSections.Access;
211 
212 			bool bEfsEncrypted = false;
213 			byte[] pbSec = null;
214 #endif
215 			DateTime? otCreation = null;
216 			SimpleStat sStat = null;
217 
218 			bool bBaseExists = IOConnection.FileExists(m_iocBase);
219 			if(bBaseExists && m_iocBase.IsLocalFile())
220 			{
221 				// FileAttributes faBase = FileAttributes.Normal;
222 				try
223 				{
224 #if !KeePassUAP
225 					FileAttributes faBase = File.GetAttributes(m_iocBase.Path);
226 					bEfsEncrypted = ((long)(faBase & FileAttributes.Encrypted) != 0);
227 					try { if(bEfsEncrypted) File.Decrypt(m_iocBase.Path); } // For TxF
228 					catch(Exception) { Debug.Assert(false); }
229 #endif
230 					otCreation = File.GetCreationTimeUtc(m_iocBase.Path);
231 					sStat = SimpleStat.Get(m_iocBase.Path);
232 #if !KeePassUAP
233 					// May throw with Mono
234 					FileSecurity sec = File.GetAccessControl(m_iocBase.Path, acs);
235 					if(sec != null) pbSec = sec.GetSecurityDescriptorBinaryForm();
236 #endif
237 				}
238 				catch(Exception) { Debug.Assert(NativeLib.IsUnix()); }
239 
240 				// if((long)(faBase & FileAttributes.ReadOnly) != 0)
241 				//	throw new UnauthorizedAccessException();
242 			}
243 
244 			if(!TxfMove())
245 			{
246 				if(bBaseExists) IOConnection.DeleteFile(m_iocBase);
247 				IOConnection.RenameFile(m_iocTemp, m_iocBase);
248 			}
249 			else { Debug.Assert(pbSec != null); } // TxF success => NTFS => has ACL
250 
251 			try
252 			{
253 				// If File.GetCreationTimeUtc fails, it may return a
254 				// date with year 1601, and Unix times start in 1970,
255 				// so testing for 1971 should ensure validity;
256 				// https://msdn.microsoft.com/en-us/library/system.io.file.getcreationtimeutc.aspx
257 				if(otCreation.HasValue && (otCreation.Value.Year >= 1971))
258 					File.SetCreationTimeUtc(m_iocBase.Path, otCreation.Value);
259 
260 				if(sStat != null) SimpleStat.Set(m_iocBase.Path, sStat);
261 
262 #if !KeePassUAP
263 				if(bEfsEncrypted)
264 				{
265 					try { File.Encrypt(m_iocBase.Path); }
266 					catch(Exception) { Debug.Assert(false); }
267 				}
268 
269 				// File.SetAccessControl(m_iocBase.Path, secPrev);
270 				// Directly calling File.SetAccessControl with the previous
271 				// FileSecurity object does not work; the binary form
272 				// indirection is required;
273 				// https://sourceforge.net/p/keepass/bugs/1738/
274 				// https://msdn.microsoft.com/en-us/library/system.io.file.setaccesscontrol.aspx
275 				if((pbSec != null) && (pbSec.Length != 0))
276 				{
277 					FileSecurity sec = new FileSecurity();
278 					sec.SetSecurityDescriptorBinaryForm(pbSec, acs);
279 
280 					File.SetAccessControl(m_iocBase.Path, sec);
281 				}
282 #endif
283 			}
284 			catch(Exception) { Debug.Assert(false); }
285 
286 			if(bMadeUnhidden) UrlUtil.HideFile(m_iocBase.Path, true);
287 		}
288 
289 		// For plugins
Configure(string strPrefix, bool? obTransacted)290 		public static void Configure(string strPrefix, bool? obTransacted)
291 		{
292 			if(string.IsNullOrEmpty(strPrefix)) { Debug.Assert(false); return; }
293 
294 			if(obTransacted.HasValue)
295 				g_dEnabled[strPrefix] = obTransacted.Value;
296 			else g_dEnabled.Remove(strPrefix);
297 		}
298 
TxfIsSupported(char chDriveLetter)299 		private static bool TxfIsSupported(char chDriveLetter)
300 		{
301 			if(chDriveLetter == '\0') return false;
302 
303 			try
304 			{
305 				string strRoot = (new string(chDriveLetter, 1)) + ":\\";
306 
307 				const int cch = NativeMethods.MAX_PATH + 1;
308 				StringBuilder sbName = new StringBuilder(cch + 1);
309 				uint uSerial = 0, cchMaxComp = 0, uFlags = 0;
310 				StringBuilder sbFileSystem = new StringBuilder(cch + 1);
311 
312 				if(!NativeMethods.GetVolumeInformation(strRoot, sbName, (uint)cch,
313 					ref uSerial, ref cchMaxComp, ref uFlags, sbFileSystem, (uint)cch))
314 				{
315 					Debug.Assert(false, (new Win32Exception()).Message);
316 					return false;
317 				}
318 
319 				return ((uFlags & NativeMethods.FILE_SUPPORTS_TRANSACTIONS) != 0);
320 			}
321 			catch(Exception) { Debug.Assert(false); }
322 
323 			return false;
324 		}
325 
TxfPrepare()326 		private void TxfPrepare()
327 		{
328 			try
329 			{
330 				if(NativeLib.IsUnix()) return;
331 				if(!m_iocBase.IsLocalFile()) return;
332 				if(TxfIsUnusable()) return;
333 
334 				string strID = StrUtil.AlphaNumericOnly(Convert.ToBase64String(
335 					CryptoRandom.Instance.GetRandomBytes(16)));
336 				string strTempDir = UrlUtil.GetTempPath();
337 				// See also ClearOld method
338 				string strTemp = UrlUtil.EnsureTerminatingSeparator(strTempDir,
339 					false) + StrTxfTempPrefix + strID + StrTxfTempSuffix;
340 
341 				char chB = UrlUtil.GetDriveLetter(m_iocBase.Path);
342 				char chT = UrlUtil.GetDriveLetter(strTemp);
343 				if(!TxfIsSupported(chB)) return;
344 				if((chT != chB) && !TxfIsSupported(chT)) return;
345 
346 				m_iocTxfMidFallback = m_iocTemp;
347 				m_iocTemp = IOConnectionInfo.FromPath(strTemp);
348 
349 				m_lToDelete.Add(m_iocTemp);
350 			}
351 			catch(Exception) { Debug.Assert(false); m_iocTxfMidFallback = null; }
352 		}
353 
TxfMove()354 		private bool TxfMove()
355 		{
356 			if(m_iocTxfMidFallback == null) return false;
357 
358 			if(TxfMoveWithTx()) return true;
359 
360 			// Move the temporary file onto the base file's drive first,
361 			// such that it cannot happen that both the base file and
362 			// the temporary file are deleted/corrupted
363 			const uint f = (NativeMethods.MOVEFILE_COPY_ALLOWED |
364 				NativeMethods.MOVEFILE_REPLACE_EXISTING);
365 			bool b = NativeMethods.MoveFileEx(m_iocTemp.Path, m_iocTxfMidFallback.Path, f);
366 			if(b) b = NativeMethods.MoveFileEx(m_iocTxfMidFallback.Path, m_iocBase.Path, f);
367 			if(!b) throw new Win32Exception();
368 
369 			Debug.Assert(!File.Exists(m_iocTemp.Path));
370 			Debug.Assert(!File.Exists(m_iocTxfMidFallback.Path));
371 			return true;
372 		}
373 
TxfMoveWithTx()374 		private bool TxfMoveWithTx()
375 		{
376 			IntPtr hTx = new IntPtr((int)NativeMethods.INVALID_HANDLE_VALUE);
377 			Debug.Assert(hTx.ToInt64() == NativeMethods.INVALID_HANDLE_VALUE);
378 			try
379 			{
380 				string strTx = PwDefs.ShortProductName + " TxF - " +
381 					StrUtil.AlphaNumericOnly(Convert.ToBase64String(
382 					CryptoRandom.Instance.GetRandomBytes(16)));
383 				const int mchTx = NativeMethods.MAX_TRANSACTION_DESCRIPTION_LENGTH;
384 				if(strTx.Length >= mchTx) strTx = strTx.Substring(0, mchTx - 1);
385 
386 				hTx = NativeMethods.CreateTransaction(IntPtr.Zero,
387 					IntPtr.Zero, 0, 0, 0, 0, strTx);
388 				if(hTx.ToInt64() == NativeMethods.INVALID_HANDLE_VALUE)
389 				{
390 					Debug.Assert(false, (new Win32Exception()).Message);
391 					return false;
392 				}
393 
394 				if(!NativeMethods.MoveFileTransacted(m_iocTemp.Path, m_iocBase.Path,
395 					IntPtr.Zero, IntPtr.Zero, (NativeMethods.MOVEFILE_COPY_ALLOWED |
396 					NativeMethods.MOVEFILE_REPLACE_EXISTING), hTx))
397 				{
398 					Debug.Assert(false, (new Win32Exception()).Message);
399 					return false;
400 				}
401 
402 				if(!NativeMethods.CommitTransaction(hTx))
403 				{
404 					Debug.Assert(false, (new Win32Exception()).Message);
405 					return false;
406 				}
407 
408 				Debug.Assert(!File.Exists(m_iocTemp.Path));
409 				return true;
410 			}
411 			catch(Exception) { Debug.Assert(false); }
412 			finally
413 			{
414 				if(hTx.ToInt64() != NativeMethods.INVALID_HANDLE_VALUE)
415 				{
416 					try { if(!NativeMethods.CloseHandle(hTx)) { Debug.Assert(false); } }
417 					catch(Exception) { Debug.Assert(false); }
418 				}
419 			}
420 
421 			return false;
422 		}
423 
TxfIsUnusable()424 		private bool TxfIsUnusable()
425 		{
426 			try
427 			{
428 				string strReleaseId = (Registry.GetValue(
429 					"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
430 					"ReleaseId", string.Empty) as string);
431 
432 				// Due to a bug in Microsoft's 'cldflt.sys' driver, a TxF transaction
433 				// results in a Blue Screen of Death on Windows 10 1903/1909;
434 				// https://www.windowslatest.com/2019/10/20/windows-10-update-issues-bsod-broken-apps-and-defender-atp/
435 				// https://sourceforge.net/p/keepass/discussion/329221/thread/924b94ea48/
436 				// This bug is fixed by the Windows update 4530684;
437 				// https://support.microsoft.com/en-us/help/4530684/windows-10-update-kb4530684
438 				// if(strReleaseId == "1903") return true;
439 				// if(strReleaseId == "1909") return true;
440 
441 				if(strReleaseId != "1809") return false;
442 
443 				// On Windows 10 1809, OneDrive crashes if the file is
444 				// in a OneDrive folder;
445 				// https://sourceforge.net/p/keepass/discussion/329220/thread/672ffecc65/
446 				// https://sourceforge.net/p/keepass/discussion/329221/thread/514786c23a/
447 
448 				string strFile = m_iocBase.Path;
449 
450 				GFunc<string, string, bool> fMatch = delegate(string strRoot, string strSfx)
451 				{
452 					if(string.IsNullOrEmpty(strRoot)) return false;
453 					string strPfx = UrlUtil.EnsureTerminatingSeparator(
454 						strRoot, false) + strSfx;
455 					return strFile.StartsWith(strPfx, StrUtil.CaseIgnoreCmp);
456 				};
457 				GFunc<string, string, bool> fMatchEnv = delegate(string strEnv, string strSfx)
458 				{
459 					return fMatch(Environment.GetEnvironmentVariable(strEnv), strSfx);
460 				};
461 
462 				string strKnown = NativeMethods.GetKnownFolderPath(
463 					NativeMethods.FOLDERID_SkyDrive);
464 				if(fMatch(strKnown, string.Empty)) return true;
465 
466 				if(fMatchEnv("USERPROFILE", "OneDrive\\")) return true;
467 				if(fMatchEnv("OneDrive", string.Empty)) return true;
468 				if(fMatchEnv("OneDriveCommercial", string.Empty)) return true;
469 				if(fMatchEnv("OneDriveConsumer", string.Empty)) return true;
470 
471 				using(RegistryKey kAccs = Registry.CurrentUser.OpenSubKey(
472 					"Software\\Microsoft\\OneDrive\\Accounts", false))
473 				{
474 					string[] vAccs = (((kAccs != null) ? kAccs.GetSubKeyNames() :
475 						null) ?? new string[0]);
476 
477 					foreach(string strAcc in vAccs)
478 					{
479 						if(string.IsNullOrEmpty(strAcc)) { Debug.Assert(false); continue; }
480 
481 						using(RegistryKey kTenants = kAccs.OpenSubKey(
482 							strAcc + "\\Tenants", false))
483 						{
484 							string[] vTenants = (((kTenants != null) ?
485 								kTenants.GetSubKeyNames() : null) ?? new string[0]);
486 
487 							foreach(string strT in vTenants)
488 							{
489 								if(string.IsNullOrEmpty(strT)) { Debug.Assert(false); continue; }
490 
491 								using(RegistryKey kT = kTenants.OpenSubKey(strT, false))
492 								{
493 									string[] vPaths = (((kT != null) ?
494 										kT.GetValueNames() : null) ?? new string[0]);
495 
496 									foreach(string strPath in vPaths)
497 									{
498 										if((strPath == null) || (strPath.Length < 4) ||
499 											(strPath[1] != ':'))
500 										{
501 											Debug.Assert(false);
502 											continue;
503 										}
504 
505 										if(fMatch(strPath, string.Empty)) return true;
506 									}
507 								}
508 							}
509 						}
510 					}
511 				}
512 			}
513 			catch(Exception) { Debug.Assert(false); }
514 
515 			return false;
516 		}
517 
ClearOld()518 		internal static void ClearOld()
519 		{
520 			try
521 			{
522 				// See also TxfPrepare method
523 				DirectoryInfo di = new DirectoryInfo(UrlUtil.GetTempPath());
524 				List<FileInfo> l = UrlUtil.GetFileInfos(di, StrTxfTempPrefix +
525 					"*" + StrTxfTempSuffix, SearchOption.TopDirectoryOnly);
526 
527 				foreach(FileInfo fi in l)
528 				{
529 					if(fi == null) { Debug.Assert(false); continue; }
530 					if(!fi.Name.StartsWith(StrTxfTempPrefix, StrUtil.CaseIgnoreCmp) ||
531 						!fi.Name.EndsWith(StrTxfTempSuffix, StrUtil.CaseIgnoreCmp))
532 						continue;
533 
534 					if((DateTime.UtcNow - fi.LastWriteTimeUtc).TotalDays > 1.0)
535 						fi.Delete();
536 				}
537 			}
538 			catch(Exception) { Debug.Assert(false); }
539 		}
540 	}
541 }
542