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.Drawing;
24 using System.Text;
25 using System.Threading;
26 using System.Windows.Forms;
27 
28 using KeePass.App.Configuration;
29 using KeePass.Native;
30 using KeePass.Resources;
31 using KeePass.Util;
32 
33 using KeePassLib;
34 using KeePassLib.Cryptography;
35 using KeePassLib.Utility;
36 
37 // using KpLibNativeMethods = KeePassLib.Native.NativeMethods;
38 
39 namespace KeePass.UI
40 {
UIFormConstructor(object objParam)41 	public delegate Form UIFormConstructor(object objParam);
UIFormResultBuilder(Form f)42 	public delegate object UIFormResultBuilder(Form f);
43 
44 	public sealed class ProtectedDialog
45 	{
46 		private UIFormConstructor m_fnConstruct;
47 		private UIFormResultBuilder m_fnResultBuilder;
48 
49 		private enum SecureThreadState
50 		{
51 			None = 0,
52 			ShowingDialog,
53 			Terminated
54 		}
55 
56 		private sealed class SecureThreadInfo
57 		{
58 			public List<Bitmap> BackgroundBitmaps = new List<Bitmap>();
59 			public IntPtr ThreadDesktop = IntPtr.Zero;
60 
61 			public object FormConstructParam = null;
62 
63 			public DialogResult DialogResult = DialogResult.None;
64 			public object ResultObject = null;
65 
66 			public SecureThreadState State = SecureThreadState.None;
67 		}
68 
ProtectedDialog(UIFormConstructor fnConstruct, UIFormResultBuilder fnResultBuilder)69 		public ProtectedDialog(UIFormConstructor fnConstruct,
70 			UIFormResultBuilder fnResultBuilder)
71 		{
72 			if(fnConstruct == null) { Debug.Assert(false); throw new ArgumentNullException("fnConstruct"); }
73 			if(fnResultBuilder == null) { Debug.Assert(false); throw new ArgumentNullException("fnResultBuilder"); }
74 
75 			m_fnConstruct = fnConstruct;
76 			m_fnResultBuilder = fnResultBuilder;
77 		}
78 
ShowDialog(out object objResult, object objConstructParam)79 		public DialogResult ShowDialog(out object objResult, object objConstructParam)
80 		{
81 			objResult = null;
82 
83 			ProcessMessagesEx();
84 
85 			// Creating a window on the new desktop spawns a CtfMon.exe child
86 			// process by default. On Windows Vista, this process is terminated
87 			// correctly when the desktop is closed. However, on Windows 7 it
88 			// isn't terminated (probably a bug); creating multiple desktops
89 			// accumulates CtfMon.exe child processes.
90 			ChildProcessesSnapshot cpsCtfMons = new ChildProcessesSnapshot(
91 				"CtfMon.exe");
92 
93 			ClipboardEventChainBlocker ccb = new ClipboardEventChainBlocker();
94 			byte[] pbClipHash = ClipboardUtil.ComputeHash();
95 
96 			SecureThreadInfo stp = new SecureThreadInfo();
97 			foreach(Screen sc in Screen.AllScreens)
98 			{
99 				Bitmap bmpBack = UIUtil.CreateScreenshot(sc);
100 				if(bmpBack != null) UIUtil.DimImage(bmpBack);
101 				stp.BackgroundBitmaps.Add(bmpBack);
102 			}
103 
104 			DialogResult dr = DialogResult.None;
105 			try
106 			{
107 				uint uOrgThreadId = NativeMethods.GetCurrentThreadId();
108 				IntPtr pOrgDesktop = NativeMethods.GetThreadDesktop(uOrgThreadId);
109 
110 				string strName = "D" + Convert.ToBase64String(
111 					CryptoRandom.Instance.GetRandomBytes(16));
112 				strName = StrUtil.AlphaNumericOnly(strName);
113 				if(strName.Length > 15) strName = strName.Substring(0, 15);
114 
115 				NativeMethods.DesktopFlags deskFlags =
116 					(NativeMethods.DesktopFlags.CreateMenu |
117 					NativeMethods.DesktopFlags.CreateWindow |
118 					NativeMethods.DesktopFlags.ReadObjects |
119 					NativeMethods.DesktopFlags.WriteObjects |
120 					NativeMethods.DesktopFlags.SwitchDesktop);
121 
122 				IntPtr pNewDesktop = NativeMethods.CreateDesktop(strName,
123 					null, IntPtr.Zero, 0, deskFlags, IntPtr.Zero);
124 				if(pNewDesktop == IntPtr.Zero)
125 					throw new InvalidOperationException();
126 
127 				bool bNameSupported = NativeMethods.DesktopNameContains(pNewDesktop,
128 					strName).GetValueOrDefault(false);
129 				Debug.Assert(bNameSupported);
130 
131 				stp.ThreadDesktop = pNewDesktop;
132 				stp.FormConstructParam = objConstructParam;
133 
134 				Thread th = new Thread(this.SecureDialogThread);
135 				th.CurrentCulture = Thread.CurrentThread.CurrentCulture;
136 				th.CurrentUICulture = Thread.CurrentThread.CurrentUICulture;
137 				th.Start(stp);
138 
139 				SecureThreadState st = SecureThreadState.None;
140 				while(st != SecureThreadState.Terminated)
141 				{
142 					th.Join(150);
143 
144 					lock(stp) { st = stp.State; }
145 
146 					if((st == SecureThreadState.ShowingDialog) && bNameSupported)
147 					{
148 						IntPtr hCurDesk = NativeMethods.OpenInputDesktop(0,
149 							false, NativeMethods.DesktopFlags.ReadObjects);
150 						if(hCurDesk == IntPtr.Zero) { Debug.Assert(false); continue; }
151 						if(hCurDesk == pNewDesktop)
152 						{
153 							if(!NativeMethods.CloseDesktop(hCurDesk)) { Debug.Assert(false); }
154 							continue;
155 						}
156 						bool? obOnSec = NativeMethods.DesktopNameContains(hCurDesk, strName);
157 						if(!NativeMethods.CloseDesktop(hCurDesk)) { Debug.Assert(false); }
158 
159 						lock(stp) { st = stp.State; } // Update; might have changed
160 
161 						if(obOnSec.HasValue && !obOnSec.Value &&
162 							(st == SecureThreadState.ShowingDialog))
163 							HandleUnexpectedDesktopSwitch(pOrgDesktop, pNewDesktop, stp);
164 					}
165 				}
166 
167 				if(!NativeMethods.SwitchDesktop(pOrgDesktop)) { Debug.Assert(false); }
168 				NativeMethods.SetThreadDesktop(pOrgDesktop);
169 
170 				th.Join(); // Ensure thread terminated before closing desktop
171 
172 				if(!NativeMethods.CloseDesktop(pNewDesktop)) { Debug.Assert(false); }
173 				NativeMethods.CloseDesktop(pOrgDesktop); // Optional
174 
175 				dr = stp.DialogResult;
176 				objResult = stp.ResultObject;
177 			}
178 			catch(Exception) { Debug.Assert(false); }
179 
180 			byte[] pbNewClipHash = ClipboardUtil.ComputeHash();
181 			if((pbClipHash != null) && (pbNewClipHash != null) &&
182 				!MemUtil.ArraysEqual(pbClipHash, pbNewClipHash))
183 				ClipboardUtil.Clear();
184 			ccb.Dispose();
185 
186 			foreach(Bitmap bmpBack in stp.BackgroundBitmaps)
187 			{
188 				if(bmpBack != null) bmpBack.Dispose();
189 			}
190 			stp.BackgroundBitmaps.Clear();
191 
192 			cpsCtfMons.TerminateNewChildsAsync(4100);
193 
194 			// If something failed, show the dialog on the normal desktop
195 			if(dr == DialogResult.None)
196 			{
197 				Form f = m_fnConstruct(objConstructParam);
198 				dr = f.ShowDialog();
199 				objResult = m_fnResultBuilder(f);
200 				UIUtil.DestroyForm(f);
201 			}
202 
203 			return dr;
204 		}
205 
ProcessMessagesEx()206 		private static void ProcessMessagesEx()
207 		{
208 			Application.DoEvents();
209 			Thread.Sleep(5);
210 			Application.DoEvents();
211 		}
212 
SecureDialogThread(object oParam)213 		private void SecureDialogThread(object oParam)
214 		{
215 			SecureThreadInfo stp = (oParam as SecureThreadInfo);
216 			if(stp == null) { Debug.Assert(false); return; }
217 
218 			List<BackgroundForm> lBackForms = new List<BackgroundForm>();
219 			BackgroundForm formBackPrimary = null;
220 			// bool bLangBar = false;
221 
222 			try
223 			{
224 				if(!NativeMethods.SetThreadDesktop(stp.ThreadDesktop))
225 				{
226 					Debug.Assert(false);
227 					return;
228 				}
229 
230 				ProcessMessagesEx();
231 
232 				// Test whether we're really on the secure desktop
233 				if(NativeMethods.GetThreadDesktop(NativeMethods.GetCurrentThreadId()) !=
234 					stp.ThreadDesktop)
235 				{
236 					Debug.Assert(false);
237 					return;
238 				}
239 
240 				// Disabling the IME was not required, because we terminate
241 				// CtfMon.exe child processes manually. However, since Sept. 2019,
242 				// there is an IME bug resulting in a black screen and/or an
243 				// IME/CTF process with high CPU load;
244 				// https://sourceforge.net/p/keepass/bugs/1881/
245 				// https://keepass.info/help/kb/sec_desk.html#ime
246 				try
247 				{
248 					ulong uif = Program.Config.UI.UIFlags;
249 					if((uif & (ulong)AceUIFlags.SecureDesktopIme) == 0)
250 						NativeMethods.ImmDisableIME(0); // Always false on 2000/XP
251 				}
252 				catch(Exception) { Debug.Assert(!WinUtil.IsAtLeastWindows2000); }
253 
254 				ProcessMessagesEx();
255 
256 				Screen[] vScreens = Screen.AllScreens;
257 				Screen scPrimary = Screen.PrimaryScreen;
258 				Debug.Assert(vScreens.Length == stp.BackgroundBitmaps.Count);
259 				int sMin = Math.Min(vScreens.Length, stp.BackgroundBitmaps.Count);
260 				for(int i = sMin - 1; i >= 0; --i)
261 				{
262 					Bitmap bmpBack = stp.BackgroundBitmaps[i];
263 					if(bmpBack == null) continue;
264 					Debug.Assert(bmpBack.Size == vScreens[i].Bounds.Size);
265 
266 					BackgroundForm formBack = new BackgroundForm(bmpBack,
267 						vScreens[i]);
268 
269 					lBackForms.Add(formBack);
270 					if(vScreens[i].Equals(scPrimary))
271 						formBackPrimary = formBack;
272 
273 					formBack.Show();
274 				}
275 				if(formBackPrimary == null)
276 				{
277 					Debug.Assert(false);
278 					if(lBackForms.Count > 0)
279 						formBackPrimary = lBackForms[lBackForms.Count - 1];
280 				}
281 
282 				ProcessMessagesEx();
283 
284 				if(!NativeMethods.SwitchDesktop(stp.ThreadDesktop)) { Debug.Assert(false); }
285 
286 				ProcessMessagesEx();
287 
288 				Form f = m_fnConstruct(stp.FormConstructParam);
289 				if(f == null) { Debug.Assert(false); return; }
290 
291 				if(Program.Config.UI.SecureDesktopPlaySound)
292 					UIUtil.PlayUacSound();
293 
294 				// bLangBar = ShowLangBar(true);
295 
296 				lock(stp) { stp.State = SecureThreadState.ShowingDialog; }
297 				stp.DialogResult = f.ShowDialog(formBackPrimary);
298 				stp.ResultObject = m_fnResultBuilder(f);
299 
300 				UIUtil.DestroyForm(f);
301 			}
302 			catch(Exception) { Debug.Assert(false); }
303 			finally
304 			{
305 				// if(bLangBar) ShowLangBar(false);
306 
307 				foreach(BackgroundForm formBack in lBackForms)
308 				{
309 					try
310 					{
311 						formBack.Close();
312 						UIUtil.DestroyForm(formBack);
313 					}
314 					catch(Exception) { Debug.Assert(false); }
315 				}
316 
317 				lock(stp) { stp.State = SecureThreadState.Terminated; }
318 			}
319 		}
320 
321 		/* private static bool ShowLangBar(bool bShow)
322 		{
323 			try
324 			{
325 				return KpLibNativeMethods.TfShowLangBar(bShow ?
326 					KpLibNativeMethods.TF_SFT_SHOWNORMAL :
327 					KpLibNativeMethods.TF_SFT_HIDDEN);
328 			}
329 			catch(Exception) { }
330 
331 			return false;
332 		} */
333 
HandleUnexpectedDesktopSwitch(IntPtr pOrgDesktop, IntPtr pNewDesktop, SecureThreadInfo stp)334 		private static void HandleUnexpectedDesktopSwitch(IntPtr pOrgDesktop,
335 			IntPtr pNewDesktop, SecureThreadInfo stp)
336 		{
337 			NativeMethods.SwitchDesktop(pOrgDesktop);
338 			NativeMethods.SetThreadDesktop(pOrgDesktop);
339 
340 			ProcessMessagesEx();
341 
342 			// Do not use MessageService.ShowWarning, because
343 			// it uses the top form's thread
344 			MessageService.ExternalIncrementMessageCount();
345 			NativeMethods.MessageBoxFlags mbf =
346 				(NativeMethods.MessageBoxFlags.MB_ICONWARNING |
347 				NativeMethods.MessageBoxFlags.MB_TASKMODAL |
348 				NativeMethods.MessageBoxFlags.MB_SETFOREGROUND |
349 				NativeMethods.MessageBoxFlags.MB_TOPMOST);
350 			if(StrUtil.RightToLeft)
351 				mbf |= (NativeMethods.MessageBoxFlags.MB_RTLREADING |
352 					NativeMethods.MessageBoxFlags.MB_RIGHT);
353 			NativeMethods.MessageBox(IntPtr.Zero, KPRes.SecDeskOtherSwitched +
354 				MessageService.NewParagraph + KPRes.SecDeskSwitchBack,
355 				PwDefs.ShortProductName, mbf);
356 			MessageService.ExternalDecrementMessageCount();
357 
358 			SecureThreadState st;
359 			lock(stp) { st = stp.State; }
360 			if(st != SecureThreadState.Terminated)
361 			{
362 				NativeMethods.SwitchDesktop(pNewDesktop);
363 				ProcessMessagesEx();
364 			}
365 		}
366 
367 		/* private static void BlockPrintScreen(Form f, bool bBlock)
368 		{
369 			if(f == null) { Debug.Assert(false); return; }
370 
371 			try
372 			{
373 				if(bBlock)
374 				{
375 					NativeMethods.RegisterHotKey(f.Handle, NativeMethods.IDHOT_SNAPDESKTOP,
376 						0, NativeMethods.VK_SNAPSHOT);
377 					NativeMethods.RegisterHotKey(f.Handle, NativeMethods.IDHOT_SNAPWINDOW,
378 						NativeMethods.MOD_ALT, NativeMethods.VK_SNAPSHOT);
379 				}
380 				else
381 				{
382 					NativeMethods.UnregisterHotKey(f.Handle, NativeMethods.IDHOT_SNAPWINDOW);
383 					NativeMethods.UnregisterHotKey(f.Handle, NativeMethods.IDHOT_SNAPDESKTOP);
384 				}
385 			}
386 			catch(Exception) { Debug.Assert(false); }
387 		} */
388 	}
389 }
390