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