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