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.IO; 24 using System.Security.Cryptography; 25 using System.Text; 26 using System.Threading; 27 using System.Windows.Forms; 28 29 using KeePass.App; 30 using KeePass.Forms; 31 using KeePass.Plugins; 32 using KeePass.Resources; 33 using KeePass.UI; 34 35 using KeePassLib; 36 using KeePassLib.Interfaces; 37 using KeePassLib.Serialization; 38 using KeePassLib.Utility; 39 40 namespace KeePass.Util 41 { 42 public enum UpdateComponentStatus 43 { 44 Unknown = 0, 45 UpToDate, 46 NewVerAvailable, 47 PreRelease, 48 DownloadFailed 49 } 50 51 public sealed class UpdateComponentInfo 52 { 53 private readonly string m_strName; // Never null 54 public string Name 55 { 56 get { return m_strName; } 57 } 58 59 private readonly ulong m_uVerInstalled; 60 public ulong VerInstalled 61 { 62 get { return m_uVerInstalled; } 63 } 64 65 private ulong m_uVerAvailable = 0; 66 public ulong VerAvailable 67 { 68 get { return m_uVerAvailable; } 69 set { m_uVerAvailable = value; } 70 } 71 72 private UpdateComponentStatus m_status = UpdateComponentStatus.Unknown; 73 public UpdateComponentStatus Status 74 { 75 get { return m_status; } 76 set { m_status = value; } 77 } 78 79 private readonly string m_strUpdateUrl; // Never null 80 public string UpdateUrl 81 { 82 get { return m_strUpdateUrl; } 83 } 84 85 private readonly string m_strCat; // Never null 86 public string Category 87 { 88 get { return m_strCat; } 89 } 90 UpdateComponentInfo(string strName, ulong uVerInstalled, string strUpdateUrl, string strCategory)91 public UpdateComponentInfo(string strName, ulong uVerInstalled, 92 string strUpdateUrl, string strCategory) 93 { 94 if(strName == null) throw new ArgumentNullException("strName"); 95 if(strUpdateUrl == null) throw new ArgumentNullException("strUpdateUrl"); 96 if(strCategory == null) throw new ArgumentNullException("strCategory"); 97 98 m_strName = strName; 99 m_uVerInstalled = uVerInstalled; 100 m_strUpdateUrl = strUpdateUrl; 101 m_strCat = strCategory; 102 } 103 } 104 105 public static class UpdateCheckEx 106 { 107 private static Dictionary<string, string> g_dFileSigKeys = 108 new Dictionary<string, string>(); 109 110 private static readonly string CompMain = PwDefs.ShortProductName; 111 112 private sealed class UpdateCheckParams 113 { 114 public readonly bool ForceUI; 115 public readonly Form Parent; // May be null 116 UpdateCheckParams(bool bForceUI, Form fOptParent)117 public UpdateCheckParams(bool bForceUI, Form fOptParent) 118 { 119 this.ForceUI = bForceUI; 120 this.Parent = fOptParent; 121 } 122 } 123 Run(bool bForceUI, Form fOptParent)124 public static void Run(bool bForceUI, Form fOptParent) 125 { 126 DateTime dtNow = DateTime.UtcNow, dtLast; 127 string strLast = Program.Config.Application.LastUpdateCheck; 128 if(!bForceUI && (strLast.Length > 0) && TimeUtil.TryDeserializeUtc( 129 strLast, out dtLast)) 130 { 131 if(CompareDates(dtLast, dtNow) == 0) return; // Checked today already 132 } 133 Program.Config.Application.LastUpdateCheck = TimeUtil.SerializeUtc(dtNow); 134 135 UpdateCheckParams p = new UpdateCheckParams(bForceUI, fOptParent); 136 if(!bForceUI) // Async 137 { 138 // // Local, but thread will continue to run anyway 139 // Thread th = new Thread(new ParameterizedThreadStart( 140 // UpdateCheckEx.RunPriv)); 141 // th.Start(p); 142 143 try 144 { 145 ThreadPool.QueueUserWorkItem(new WaitCallback( 146 UpdateCheckEx.RunPriv), p); 147 } 148 catch(Exception) { Debug.Assert(false); } 149 } 150 else RunPriv(p); 151 } 152 CompareDates(DateTime a, DateTime b)153 private static int CompareDates(DateTime a, DateTime b) 154 { 155 Debug.Assert(a.Kind == b.Kind); 156 if(a.Year != b.Year) return ((a.Year < b.Year) ? -1 : 1); 157 if(a.Month != b.Month) return ((a.Month < b.Month) ? -1 : 1); 158 if(a.Day != b.Day) return ((a.Day < b.Day) ? -1 : 1); 159 return 0; 160 } 161 RunPriv(object o)162 private static void RunPriv(object o) 163 { 164 UpdateCheckParams p = (o as UpdateCheckParams); 165 if(p == null) { Debug.Assert(false); return; } 166 167 IStatusLogger sl = null; 168 try 169 { 170 if(p.ForceUI) 171 { 172 Form fStatusDialog; 173 sl = StatusUtil.CreateStatusDialog(p.Parent, out fStatusDialog, 174 KPRes.UpdateCheck, KPRes.CheckingForUpd + "...", true, true); 175 } 176 177 List<UpdateComponentInfo> lInst = GetInstalledComponents(); 178 List<string> lUrls = GetUrls(lInst); 179 Dictionary<string, List<UpdateComponentInfo>> dictAvail = 180 DownloadInfoFiles(lUrls, sl); 181 if(dictAvail == null) return; // User cancelled 182 183 MergeInfo(lInst, dictAvail); 184 185 bool bUpdAvail = false; 186 foreach(UpdateComponentInfo uc in lInst) 187 { 188 if(uc.Status == UpdateComponentStatus.NewVerAvailable) 189 { 190 bUpdAvail = true; 191 break; 192 } 193 } 194 195 if(sl != null) { sl.EndLogging(); sl = null; } 196 197 if(bUpdAvail || p.ForceUI) 198 ShowUpdateDialogAsync(lInst, p.ForceUI); 199 } 200 catch(Exception) { Debug.Assert(false); } 201 finally 202 { 203 try { if(sl != null) sl.EndLogging(); } 204 catch(Exception) { Debug.Assert(false); } 205 } 206 } 207 ShowUpdateDialogAsync(List<UpdateComponentInfo> lInst, bool bModal)208 private static void ShowUpdateDialogAsync(List<UpdateComponentInfo> lInst, 209 bool bModal) 210 { 211 try 212 { 213 MainForm mf = Program.MainForm; 214 if((mf != null) && mf.InvokeRequired) 215 mf.BeginInvoke(new UceShDlgDelegate(ShowUpdateDialogPriv), 216 lInst, bModal); 217 else ShowUpdateDialogPriv(lInst, bModal); 218 } 219 catch(Exception) { Debug.Assert(false); } 220 } 221 UceShDlgDelegate(List<UpdateComponentInfo> lInst, bool bModal)222 private delegate void UceShDlgDelegate(List<UpdateComponentInfo> lInst, 223 bool bModal); ShowUpdateDialogPriv(List<UpdateComponentInfo> lInst, bool bModal)224 private static void ShowUpdateDialogPriv(List<UpdateComponentInfo> lInst, 225 bool bModal) 226 { 227 try 228 { 229 // Do not show the update dialog while auto-typing; 230 // https://sourceforge.net/p/keepass/bugs/1265/ 231 if(SendInputEx.IsSending) return; 232 233 UpdateCheckForm dlg = new UpdateCheckForm(); 234 dlg.InitEx(lInst, bModal); 235 UIUtil.ShowDialogAndDestroy(dlg); 236 } 237 catch(Exception) { Debug.Assert(false); } 238 } 239 240 private sealed class UpdateDownloadInfo 241 { 242 public readonly string Url; // Never null 243 public readonly object SyncObj = new object(); 244 public bool Ready = false; 245 public List<UpdateComponentInfo> ComponentInfo = null; 246 UpdateDownloadInfo(string strUrl)247 public UpdateDownloadInfo(string strUrl) 248 { 249 if(strUrl == null) throw new ArgumentNullException("strUrl"); 250 251 this.Url = strUrl; 252 } 253 } 254 255 private static Dictionary<string, List<UpdateComponentInfo>> DownloadInfoFiles(List<string> lUrls, IStatusLogger sl)256 DownloadInfoFiles(List<string> lUrls, IStatusLogger sl) 257 { 258 List<UpdateDownloadInfo> lDl = new List<UpdateDownloadInfo>(); 259 foreach(string strUrl in lUrls) 260 { 261 if(string.IsNullOrEmpty(strUrl)) { Debug.Assert(false); continue; } 262 263 UpdateDownloadInfo dl = new UpdateDownloadInfo(strUrl); 264 lDl.Add(dl); 265 266 ThreadPool.QueueUserWorkItem(new WaitCallback( 267 UpdateCheckEx.DownloadInfoFile), dl); 268 } 269 270 while(true) 271 { 272 bool bReady = true; 273 foreach(UpdateDownloadInfo dl in lDl) 274 { 275 lock(dl.SyncObj) { bReady &= dl.Ready; } 276 } 277 278 if(bReady) break; 279 Thread.Sleep(40); 280 281 if(sl != null) 282 { 283 if(!sl.ContinueWork()) return null; 284 } 285 } 286 287 Dictionary<string, List<UpdateComponentInfo>> dict = 288 new Dictionary<string, List<UpdateComponentInfo>>(); 289 foreach(UpdateDownloadInfo dl in lDl) 290 { 291 dict[dl.Url.ToLower()] = dl.ComponentInfo; 292 } 293 return dict; 294 } 295 DownloadInfoFile(object o)296 private static void DownloadInfoFile(object o) 297 { 298 UpdateDownloadInfo dl = (o as UpdateDownloadInfo); 299 if(dl == null) { Debug.Assert(false); return; } 300 301 dl.ComponentInfo = LoadInfoFile(dl.Url); 302 lock(dl.SyncObj) { dl.Ready = true; } 303 } 304 GetUrls(List<UpdateComponentInfo> l)305 private static List<string> GetUrls(List<UpdateComponentInfo> l) 306 { 307 List<string> lUrls = new List<string>(); 308 foreach(UpdateComponentInfo uc in l) 309 { 310 string strUrl = uc.UpdateUrl; 311 if(string.IsNullOrEmpty(strUrl)) continue; 312 313 bool bFound = false; 314 for(int i = 0; i < lUrls.Count; ++i) 315 { 316 if(lUrls[i].Equals(strUrl, StrUtil.CaseIgnoreCmp)) 317 { 318 bFound = true; 319 break; 320 } 321 } 322 323 if(!bFound) lUrls.Add(strUrl); 324 } 325 326 return lUrls; 327 } 328 LoadInfoFile(string strUrl)329 private static List<UpdateComponentInfo> LoadInfoFile(string strUrl) 330 { 331 try 332 { 333 IOConnectionInfo ioc = IOConnectionInfo.FromPath(strUrl.Trim()); 334 335 byte[] pb; 336 using(Stream s = IOConnection.OpenRead(ioc)) 337 { 338 pb = MemUtil.Read(s); 339 } 340 341 if(ioc.Path.EndsWith(".gz", StrUtil.CaseIgnoreCmp)) 342 { 343 // Decompress in try-catch, because some web filters 344 // incorrectly pre-decompress the returned data 345 // https://sourceforge.net/projects/keepass/forums/forum/329221/topic/4915083 346 try 347 { 348 byte[] pbDec = MemUtil.Decompress(pb); 349 List<UpdateComponentInfo> l = LoadInfoFilePriv(pbDec, ioc); 350 if(l != null) return l; 351 } 352 catch(Exception) { } 353 } 354 355 return LoadInfoFilePriv(pb, ioc); 356 } 357 catch(Exception) { } 358 359 return null; 360 } 361 LoadInfoFilePriv(byte[] pbData, IOConnectionInfo iocSource)362 private static List<UpdateComponentInfo> LoadInfoFilePriv(byte[] pbData, 363 IOConnectionInfo iocSource) 364 { 365 if((pbData == null) || (pbData.Length == 0)) return null; 366 367 int iOffset = 0; 368 StrEncodingInfo sei = StrUtil.GetEncoding(StrEncodingType.Utf8); 369 byte[] pbBom = sei.StartSignature; 370 if((pbData.Length >= pbBom.Length) && MemUtil.ArraysEqual(pbBom, 371 MemUtil.Mid(pbData, 0, pbBom.Length))) 372 iOffset += pbBom.Length; 373 374 string strData = sei.Encoding.GetString(pbData, iOffset, pbData.Length - iOffset); 375 strData = StrUtil.NormalizeNewLines(strData, false); 376 string[] vLines = strData.Split('\n'); 377 378 string strSigKey; 379 g_dFileSigKeys.TryGetValue(iocSource.Path.ToLowerInvariant(), out strSigKey); 380 string strLdSig = null; 381 StringBuilder sbToVerify = ((strSigKey != null) ? new StringBuilder() : null); 382 383 List<UpdateComponentInfo> l = new List<UpdateComponentInfo>(); 384 bool bHeader = true, bFooterFound = false; 385 char chSep = ':'; // Modified by header 386 for(int i = 0; i < vLines.Length; ++i) 387 { 388 string str = vLines[i].Trim(); 389 if(str.Length == 0) continue; 390 391 if(bHeader) 392 { 393 chSep = str[0]; 394 bHeader = false; 395 396 string[] vHdr = str.Split(chSep); 397 if(vHdr.Length >= 2) strLdSig = vHdr[1]; 398 } 399 else if(str[0] == chSep) 400 { 401 bFooterFound = true; 402 break; 403 } 404 else // Component info 405 { 406 if(sbToVerify != null) 407 { 408 sbToVerify.Append(str); 409 sbToVerify.Append('\n'); 410 } 411 412 string[] vInfo = str.Split(chSep); 413 if(vInfo.Length >= 2) 414 { 415 UpdateComponentInfo c = new UpdateComponentInfo( 416 vInfo[0].Trim(), 0, iocSource.Path, string.Empty); 417 c.VerAvailable = StrUtil.ParseVersion(vInfo[1]); 418 419 AddComponent(l, c); 420 } 421 } 422 } 423 if(!bFooterFound) { Debug.Assert(false); return null; } 424 425 if(sbToVerify != null) 426 { 427 if(!VerifySignature(sbToVerify.ToString(), strLdSig, strSigKey)) 428 return null; 429 } 430 431 return l; 432 } 433 AddComponent(List<UpdateComponentInfo> l, UpdateComponentInfo c)434 private static void AddComponent(List<UpdateComponentInfo> l, 435 UpdateComponentInfo c) 436 { 437 if((l == null) || (c == null)) { Debug.Assert(false); return; } 438 439 for(int i = l.Count - 1; i >= 0; --i) 440 { 441 if(l[i].Name.Equals(c.Name, StrUtil.CaseIgnoreCmp)) 442 l.RemoveAt(i); 443 } 444 445 l.Add(c); 446 } 447 GetInstalledComponents()448 private static List<UpdateComponentInfo> GetInstalledComponents() 449 { 450 List<UpdateComponentInfo> l = new List<UpdateComponentInfo>(); 451 452 foreach(PluginInfo pi in Program.MainForm.PluginManager) 453 { 454 Plugin p = pi.Interface; 455 string strUrl = ((p != null) ? (p.UpdateUrl ?? string.Empty) : 456 string.Empty); 457 458 AddComponent(l, new UpdateComponentInfo(pi.Name.Trim(), 459 StrUtil.ParseVersion(pi.FileVersion), strUrl.Trim(), 460 KPRes.Plugins)); 461 } 462 463 // Add KeePass at the end to override any buggy plugin names 464 AddComponent(l, new UpdateComponentInfo(CompMain, PwDefs.FileVersion64, 465 PwDefs.VersionUrl, PwDefs.ShortProductName)); 466 467 l.Sort(UpdateCheckEx.CompareComponents); 468 return l; 469 } 470 CompareComponents(UpdateComponentInfo a, UpdateComponentInfo b)471 private static int CompareComponents(UpdateComponentInfo a, 472 UpdateComponentInfo b) 473 { 474 if(a.Name == b.Name) return 0; 475 if(a.Name == CompMain) return -1; 476 if(b.Name == CompMain) return 1; 477 478 return a.Name.CompareTo(b.Name); 479 } 480 MergeInfo(List<UpdateComponentInfo> lInst, Dictionary<string, List<UpdateComponentInfo>> dictAvail)481 private static void MergeInfo(List<UpdateComponentInfo> lInst, 482 Dictionary<string, List<UpdateComponentInfo>> dictAvail) 483 { 484 string strOvrId = PwDefs.VersionUrl.ToLower(); 485 List<UpdateComponentInfo> lOvr; 486 dictAvail.TryGetValue(strOvrId, out lOvr); 487 488 foreach(UpdateComponentInfo uc in lInst) 489 { 490 string strUrlId = uc.UpdateUrl.ToLower(); 491 List<UpdateComponentInfo> lAvail; 492 dictAvail.TryGetValue(strUrlId, out lAvail); 493 494 if(SetComponentAvail(uc, lOvr)) { } 495 else if(SetComponentAvail(uc, lAvail)) { } 496 else if((strUrlId.Length > 0) && (lAvail == null)) 497 uc.Status = UpdateComponentStatus.DownloadFailed; 498 else uc.Status = UpdateComponentStatus.Unknown; 499 } 500 } 501 SetComponentAvail(UpdateComponentInfo uc, List<UpdateComponentInfo> lAvail)502 private static bool SetComponentAvail(UpdateComponentInfo uc, 503 List<UpdateComponentInfo> lAvail) 504 { 505 if(uc == null) { Debug.Assert(false); return false; } 506 if(lAvail == null) return false; // No assert 507 508 if((uc.Name == CompMain) && WinUtil.IsAppX) 509 { 510 // The user's AppX may be old; do not claim it's up-to-date 511 // uc.VerAvailable = uc.VerInstalled; 512 // uc.Status = UpdateComponentStatus.UpToDate; 513 uc.Status = UpdateComponentStatus.Unknown; 514 return true; 515 } 516 517 foreach(UpdateComponentInfo ucAvail in lAvail) 518 { 519 if(ucAvail.Name.Equals(uc.Name, StrUtil.CaseIgnoreCmp)) 520 { 521 uc.VerAvailable = ucAvail.VerAvailable; 522 523 if(uc.VerInstalled == uc.VerAvailable) 524 uc.Status = UpdateComponentStatus.UpToDate; 525 else if(uc.VerInstalled < uc.VerAvailable) 526 uc.Status = UpdateComponentStatus.NewVerAvailable; 527 else uc.Status = UpdateComponentStatus.PreRelease; 528 529 return true; 530 } 531 } 532 533 return false; 534 } 535 VerifySignature(string strContent, string strSig, string strKey)536 private static bool VerifySignature(string strContent, string strSig, 537 string strKey) 538 { 539 if(string.IsNullOrEmpty(strSig)) { Debug.Assert(false); return false; } 540 541 try 542 { 543 byte[] pbMsg = StrUtil.Utf8.GetBytes(strContent); 544 byte[] pbSig = Convert.FromBase64String(strSig); 545 546 using(SHA512Managed sha = new SHA512Managed()) 547 { 548 using(RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) 549 { 550 // Watching this code in the debugger may result in a 551 // CryptographicException when disposing the object 552 rsa.PersistKeyInCsp = false; // Default key 553 rsa.FromXmlString(strKey); 554 rsa.PersistKeyInCsp = false; // Loaded key 555 556 if(!rsa.VerifyData(pbMsg, sha, pbSig)) 557 { 558 Debug.Assert(false); 559 return false; 560 } 561 562 rsa.PersistKeyInCsp = false; 563 } 564 } 565 } 566 catch(Exception) { Debug.Assert(false); return false; } 567 568 return true; 569 } 570 SetFileSigKey(string strUrl, string strKey)571 public static void SetFileSigKey(string strUrl, string strKey) 572 { 573 if(string.IsNullOrEmpty(strUrl)) { Debug.Assert(false); return; } 574 if(string.IsNullOrEmpty(strKey)) { Debug.Assert(false); return; } 575 576 g_dFileSigKeys[strUrl.ToLowerInvariant()] = strKey; 577 } 578 EnsureConfigured(Form fParent)579 public static void EnsureConfigured(Form fParent) 580 { 581 SetFileSigKey(PwDefs.VersionUrl, AppDefs.Rsa4096PublicKeyXml); 582 583 if(Program.Config.Application.Start.CheckForUpdateConfigured) return; 584 585 // If the user has manually enabled the automatic update check 586 // before, there's no need to ask him again 587 if(!Program.Config.Application.Start.CheckForUpdate && 588 !Program.IsDevelopmentSnapshot()) 589 { 590 string strHdr = KPRes.UpdateCheckInfo; 591 string strSub = KPRes.UpdateCheckInfoRes + MessageService.NewParagraph + 592 KPRes.UpdateCheckInfoPriv; 593 594 VistaTaskDialog dlg = new VistaTaskDialog(); 595 dlg.CommandLinks = true; 596 dlg.Content = strHdr; 597 dlg.MainInstruction = KPRes.UpdateCheckEnableQ; 598 dlg.WindowTitle = PwDefs.ShortProductName; 599 dlg.AddButton((int)DialogResult.Yes, KPRes.Enable + 600 " (" + KPRes.Recommended + ")", null); 601 dlg.AddButton((int)DialogResult.No, KPRes.Disable, null); 602 dlg.SetIcon(VtdCustomIcon.Question); 603 dlg.FooterText = strSub; 604 dlg.SetFooterIcon(VtdIcon.Information); 605 606 int iResult; 607 if(dlg.ShowDialog(fParent)) iResult = dlg.Result; 608 else 609 { 610 string strMain = strHdr + MessageService.NewParagraph + strSub; 611 iResult = (MessageService.AskYesNo(strMain + 612 MessageService.NewParagraph + KPRes.UpdateCheckEnableQ) ? 613 (int)DialogResult.Yes : (int)DialogResult.No); 614 } 615 616 Program.Config.Application.Start.CheckForUpdate = ((iResult == 617 (int)DialogResult.OK) || (iResult == (int)DialogResult.Yes)); 618 } 619 620 Program.Config.Application.Start.CheckForUpdateConfigured = true; 621 } 622 } 623 } 624