1 using System;
2 using System.Collections.Generic;
3 using Jayrock.JsonRpc;
4 using KeePassRPC.DataExchangeModel;
5 using System.Windows.Forms;
6 using KeePass.Forms;
7 using KeePassLib;
8 using System.Collections;
9 using System.Drawing;
10 using KeePass.Resources;
11 using KeePassLib.Serialization;
12 using System.IO;
13 using KeePassLib.Security;
14 using KeePass.Plugins;
15 using KeePassLib.Cryptography.PasswordGenerator;
16 using System.Diagnostics;
17 using System.Reflection;
18 using KeePass.UI;
19 
20 namespace KeePassRPC
21 {
22     /// <summary>
23     /// Provides an externally accessible API for common KeePass operations
24     /// </summary>
25     public class KeePassRPCService : JsonRpcService
26     {
27         #region Class variables, constructor and destructor
28 
29         KeePassRPCExt KeePassRPCPlugin;
30         Version PluginVersion;
31         IPluginHost host;
32 
33         private string[] _standardIconsBase64;
34         private bool _restartWarningShown = false;
35 
KeePassRPCService(IPluginHost host, string[] standardIconsBase64, KeePassRPCExt plugin)36         public KeePassRPCService(IPluginHost host, string[] standardIconsBase64, KeePassRPCExt plugin)
37         {
38             KeePassRPCPlugin = plugin;
39             PluginVersion = KeePassRPCExt.PluginVersion;
40             this.host = host;
41             _standardIconsBase64 = standardIconsBase64;
42         }
43         #endregion
44 
45         #region KeePass GUI routines
46 
47         /// <summary>
48         /// Halts thread until a DB is open in the KeePass application
49         /// </summary>
50         /// <remarks>This simple thread sync may not work if more than one RPC client gets involved.</remarks>
ensureDBisOpen()51         private bool ensureDBisOpen()
52         {
53 
54             if (!host.Database.IsOpen)
55             {
56                 //ensureDBisOpenEWH.Reset(); // ensures we will wait even if DB has been opened previously.
57                 // maybe tiny opportunity for deadlock if user opens DB exactly between DB.IsOpen and this statement?
58                 // TODO2: consider moving above statement to top of method - shouldn't do any harm and could rule out rare deadlock?
59                 host.MainWindow.BeginInvoke((MethodInvoker)delegate { promptUserToOpenDB(null); });
60                 //ensureDBisOpenEWH.WaitOne(15000, false); // wait until DB has been opened
61 
62                 if (!host.Database.IsOpen)
63                     return false;
64             }
65             return true;
66         }
67 
promptUserToOpenDB(IOConnectionInfo ioci)68         void promptUserToOpenDB(IOConnectionInfo ioci)
69         {
70             //TODO: find out z-index of firefox and push keepass just behind it rather than right to the back
71             //TODO: focus open DB dialog box if it's there
72 
73             IntPtr ffWindow = Native.GetForegroundWindow();
74             bool minimised = KeePass.Program.MainForm.WindowState == FormWindowState.Minimized;
75             bool trayed = KeePass.Program.MainForm.IsTrayed();
76 
77             if (ioci == null)
78                 ioci = KeePass.Program.Config.Application.LastUsedFile;
79 
80             Native.AttachToActiveAndBringToForeground(KeePass.Program.MainForm.Handle);
81             KeePass.Program.MainForm.Activate();
82 
83             // refresh the UI in case user cancelled the dialog box and/or KeePass native calls have left us in a bit of a weird state
84             host.MainWindow.UpdateUI(true, null, true, null, true, null, false);
85 
86             // Set the program state back to what is was unless the user has
87             // configured "lock on minimise" in which case we always set it to Normal
88             if (!KeePass.Program.Config.Security.WorkspaceLocking.LockOnWindowMinimize)
89             {
90                 minimised = false;
91                 trayed = false;
92             }
93 
94             KeePass.Program.MainForm.WindowState = minimised ? FormWindowState.Minimized : FormWindowState.Normal;
95 
96             if (trayed)
97             {
98                 KeePass.Program.MainForm.Visible = false;
99                 KeePass.Program.MainForm.UpdateTrayIcon();
100             }
101 
102             // Make Firefox active again
103             Native.EnsureForegroundWindow(ffWindow);
104         }
105 
showOpenDB(IOConnectionInfo ioci)106         bool showOpenDB(IOConnectionInfo ioci)
107         {
108             // KeePass does this on "show window" keypress. Not sure what it does but most likely does no harm to check here too
109             if (KeePass.Program.MainForm.UIIsInteractionBlocked()) { return false; }
110 
111             // Make sure the login dialog (or options and other windows) are not already visible. Same behaviour as KP.
112             if (KeePass.UI.GlobalWindowManager.WindowCount != 0) return false;
113 
114             // Prompt user to open database
115             KeePass.Program.MainForm.OpenDatabase(ioci, null, false);
116             return true;
117         }
118 
dlgUpdateUINoSave()119         private delegate void dlgUpdateUINoSave();
120 
updateUINoSave()121         void updateUINoSave()
122         {
123             host.MainWindow.UpdateUI(false, null, true, null, true, null, true);
124         }
125 
dlgSaveDB(PwDatabase databaseToSave)126         private delegate void dlgSaveDB(PwDatabase databaseToSave);
127 
saveDB(PwDatabase databaseToSave)128         void saveDB(PwDatabase databaseToSave)
129         {
130             // store current active tab/db
131             PwDocument currentActiveDoc = host.MainWindow.DocumentManager.ActiveDocument;
132 
133             // change active tab
134             PwDocument doc = host.MainWindow.DocumentManager.FindDocument(databaseToSave);
135             host.MainWindow.DocumentManager.ActiveDocument = doc;
136 
137             if (host.CustomConfig.GetBool("KeePassRPC.KeeFox.autoCommit", true))
138             {
139                 // save active database & update UI appearance
140                 if (host.MainWindow.UIFileSave(false))
141                     host.MainWindow.UpdateUI(false, null, true, null, true, null, false);
142             }
143             else
144             {
145                 // update ui with "changed" flag
146                 host.MainWindow.UpdateUI(false, null, true, null, true, null, true);
147             }
148             // change tab back
149             host.MainWindow.DocumentManager.ActiveDocument = currentActiveDoc;
150         }
151 
openGroupEditorWindow(PwGroup pg, PwDatabase db)152         void openGroupEditorWindow(PwGroup pg, PwDatabase db)
153         {
154             using (GroupForm gf = new GroupForm())
155             {
156                 gf.InitEx(pg, false, host.MainWindow.ClientIcons, host.Database);
157 
158                 gf.BringToFront();
159                 gf.ShowInTaskbar = true;
160 
161                 host.MainWindow.Focus();
162                 gf.TopMost = true;
163                 gf.Focus();
164                 gf.Activate();
165                 if (gf.ShowDialog() == DialogResult.OK)
166                     saveDB(db);
167             }
168         }
169 
dlgOpenGroupEditorWindow(PwGroup pg, PwDatabase db)170         private delegate void dlgOpenGroupEditorWindow(PwGroup pg, PwDatabase db);
171 
172         /// <summary>
173         /// Launches the group editor.
174         /// </summary>
175         /// <param name="uuid">The UUID of the group to edit.</param>
176         [JsonRpcMethod]
LaunchGroupEditor(string uuid, string dbFileName)177         public void LaunchGroupEditor(string uuid, string dbFileName)
178         {
179             // Make sure there is an active database
180             if (!ensureDBisOpen()) return;
181 
182             // find the database
183             PwDatabase db = SelectDatabase(dbFileName);
184 
185             if (uuid != null && uuid.Length > 0)
186             {
187                 PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(uuid));
188 
189                 PwGroup matchedGroup = GetRootPwGroup(db).FindGroup(pwuuid, true);
190 
191                 if (matchedGroup == null)
192                     throw new Exception("Could not find requested entry.");
193 
194                 host.MainWindow.BeginInvoke(new dlgOpenGroupEditorWindow(openGroupEditorWindow), matchedGroup, db);
195             }
196 
197         }
198 
OpenLoginEditorWindow(PwEntry pe, PwDatabase db)199         void OpenLoginEditorWindow(PwEntry pe, PwDatabase db)
200         {
201             using (PwEntryForm ef = new PwEntryForm())
202             {
203                 ef.InitEx(pe, PwEditMode.EditExistingEntry, host.Database, host.MainWindow.ClientIcons, false, false);
204 
205                 ef.BringToFront();
206                 ef.ShowInTaskbar = true;
207 
208                 host.MainWindow.Focus();
209                 ef.TopMost = true;
210                 ef.Focus();
211                 ef.Activate();
212 
213                 if (ef.ShowDialog() == DialogResult.OK)
214                     saveDB(db);
215             }
216         }
217 
dlgOpenLoginEditorWindow(PwEntry pg, PwDatabase db)218         private delegate void dlgOpenLoginEditorWindow(PwEntry pg, PwDatabase db);
219 
220         /// <summary>
221         /// Launches the login editor.
222         /// </summary>
223         /// <param name="uuid">The UUID of the entry to edit.</param>
224         [JsonRpcMethod]
LaunchLoginEditor(string uuid, string dbFileName)225         public void LaunchLoginEditor(string uuid, string dbFileName)
226         {
227             // Make sure there is an active database
228             if (!ensureDBisOpen()) return;
229 
230             // find the database
231             PwDatabase db = SelectDatabase(dbFileName);
232 
233             if (uuid != null && uuid.Length > 0)
234             {
235                 PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(uuid));
236 
237                 PwEntry matchedLogin = GetRootPwGroup(db).FindEntry(pwuuid, true);
238 
239                 if (matchedLogin == null)
240                     throw new Exception("Could not find requested entry.");
241 
242                 host.MainWindow.BeginInvoke(new dlgOpenLoginEditorWindow(OpenLoginEditorWindow), matchedLogin, db);
243             }
244 
245         }
246 
247         // A similar function is defined in KeePass MainForm_functions.cs but it's internal
CompleteConnectionInfoUsingMru(IOConnectionInfo ioc)248         IOConnectionInfo CompleteConnectionInfoUsingMru(IOConnectionInfo ioc)
249         {
250             if (string.IsNullOrEmpty(ioc.UserName) && string.IsNullOrEmpty(ioc.Password))
251             {
252                 for (uint u = 0; u < host.MainWindow.FileMruList.ItemCount; ++u)
253                 {
254                     IOConnectionInfo iocMru = (host.MainWindow.FileMruList.GetItem(u).Value as IOConnectionInfo);
255                     if (iocMru == null) { continue; }
256 
257                     if (iocMru.Path.Equals(ioc.Path, KeePassLib.Utility.StrUtil.CaseIgnoreCmp))
258                     {
259                         ioc = iocMru.CloneDeep();
260                         break;
261                     }
262                 }
263             }
264 
265             return ioc;
266         }
267 
268         #endregion
269 
270         #region Utility functions to convert between KeePassRPC object schema and KeePass schema
271 
GetEntryFromPwEntry(PwEntry pwe, int matchAccuracy, bool fullDetails, PwDatabase db)272         private LightEntry GetEntryFromPwEntry(PwEntry pwe, int matchAccuracy, bool fullDetails, PwDatabase db)
273         {
274             return GetEntryFromPwEntry(pwe, matchAccuracy, fullDetails, db, false);
275         }
276 
GetEntryFromPwEntry(PwEntry pwe, int matchAccuracy, bool fullDetails, PwDatabase db, bool abortIfHidden)277         private LightEntry GetEntryFromPwEntry(PwEntry pwe, int matchAccuracy, bool fullDetails, PwDatabase db, bool abortIfHidden)
278         {
279             EntryConfig conf = pwe.GetKPRPCConfig(db.GetKPRPCConfig().DefaultMatchAccuracy);
280             if (conf == null)
281                 return null;
282             return GetEntryFromPwEntry(pwe, conf, matchAccuracy, fullDetails, db, abortIfHidden);
283         }
284 
GetEntryFromPwEntry(PwEntry pwe, EntryConfig conf, int matchAccuracy, bool fullDetails, PwDatabase db, bool abortIfHidden)285         private LightEntry GetEntryFromPwEntry(PwEntry pwe, EntryConfig conf, int matchAccuracy, bool fullDetails, PwDatabase db, bool abortIfHidden)
286         {
287             ArrayList formFieldList = new ArrayList();
288             ArrayList URLs = new ArrayList();
289             bool usernameFound = false;
290             bool passwordFound = false;
291             bool alwaysAutoFill = false;
292             bool neverAutoFill = false;
293             bool alwaysAutoSubmit = false;
294             bool neverAutoSubmit = false;
295             int priority = 0;
296             string usernameName = "";
297             string usernameValue = "";
298 
299             if (!string.IsNullOrEmpty(pwe.Strings.ReadSafe("URL")))
300             {
301                 URLs.Add(pwe.Strings.ReadSafe("URL"));
302             }
303 
304             if (abortIfHidden && conf.Hide)
305                 return null;
306 
307             bool dbDefaultPlaceholderHandlingEnabled = db.GetKPRPCConfig().DefaultPlaceholderHandling == PlaceholderHandling.Enabled;
308 
309             if (conf.FormFieldList != null)
310             {
311                 foreach (FormField ff in conf.FormFieldList)
312                 {
313                     if (!fullDetails && ff.Type != FormFieldType.FFTusername)
314                         continue;
315 
316                     bool enablePlaceholders = false;
317                     string displayName = ff.Name;
318                     string ffValue = ff.Value;
319 
320                     if (ff.PlaceholderHandling == PlaceholderHandling.Enabled ||
321                         (ff.PlaceholderHandling == PlaceholderHandling.Default && dbDefaultPlaceholderHandlingEnabled))
322                     {
323                         enablePlaceholders = true;
324                     }
325 
326                     if (ff.Type == FormFieldType.FFTpassword && ff.Value == "{PASSWORD}")
327                     {
328                         ffValue = KeePassRPCPlugin.GetPwEntryString(pwe, "Password", db);
329                         if (!string.IsNullOrEmpty(ffValue))
330                         {
331                             displayName = "KeePass password";
332                             passwordFound = true;
333                         }
334                     }
335                     else if (ff.Type == FormFieldType.FFTusername && ff.Value == "{USERNAME}")
336                     {
337                         ffValue = KeePassRPCPlugin.GetPwEntryString(pwe, "UserName", db);
338                         if (!string.IsNullOrEmpty(ffValue))
339                         {
340                             displayName = "KeePass username";
341                             usernameFound = true;
342                         }
343                     }
344 
345                     string derefValue = enablePlaceholders ? KeePassRPCPlugin.GetPwEntryStringFromDereferencableValue(pwe, ffValue, db) : ffValue;
346 
347                     if (fullDetails)
348                     {
349                         formFieldList.Add(new FormField(ff.Name, displayName, derefValue, ff.Type, ff.Id, ff.Page, ff.PlaceholderHandling));
350                     } else
351                     {
352                         usernameName = "username";
353                         usernameValue = derefValue;
354                     }
355                 }
356             }
357 
358             if (conf.AltURLs != null)
359                 URLs.AddRange(conf.AltURLs);
360 
361             // If we didn't find an explicit password field, we assume any value
362             // in the KeePass "password" box is what we are looking for
363             if (fullDetails && !passwordFound)
364             {
365                 string ffValue = KeePassRPCPlugin.GetPwEntryString(pwe, "Password", db);
366                 string derefValue = dbDefaultPlaceholderHandlingEnabled ? KeePassRPCPlugin.GetPwEntryStringFromDereferencableValue(pwe, ffValue, db) : ffValue;
367                 if (!string.IsNullOrEmpty(ffValue))
368                 {
369                     formFieldList.Add(new FormField("",
370                         "KeePass password", derefValue, FormFieldType.FFTpassword, "", 1, PlaceholderHandling.Default));
371                 }
372             }
373 
374             // If we didn't find an explicit username field, we assume any value
375             // in the KeePass "username" box is what we are looking for
376             if (!usernameFound)
377             {
378                 string ffValue = KeePassRPCPlugin.GetPwEntryString(pwe, "UserName", db);
379                 string derefValue = dbDefaultPlaceholderHandlingEnabled ? KeePassRPCPlugin.GetPwEntryStringFromDereferencableValue(pwe, ffValue, db) : ffValue;
380                 if (!string.IsNullOrEmpty(ffValue))
381                 {
382                     if (fullDetails)
383                     {
384                         formFieldList.Add(new FormField("",
385                             "KeePass username", derefValue, FormFieldType.FFTusername, "", 1, PlaceholderHandling.Default));
386                     }
387                     else
388                     {
389                         usernameName = "username";
390                         usernameValue = ffValue;
391                     }
392                 }
393             }
394 
395             string imageData = iconToBase64(pwe.CustomIconUuid, pwe.IconId);
396             //Debug.WriteLine("GetEntryFromPwEntry icon converted: " + sw.Elapsed);
397 
398             if (fullDetails)
399             {
400                 alwaysAutoFill = conf.AlwaysAutoFill;
401                 alwaysAutoSubmit = conf.AlwaysAutoSubmit;
402                 neverAutoFill = conf.NeverAutoFill;
403                 neverAutoSubmit = conf.NeverAutoSubmit;
404                 priority = conf.Priority;
405 
406             }
407 
408             //sw.Stop();
409             //Debug.WriteLine("GetEntryFromPwEntry execution time: " + sw.Elapsed);
410             //Debug.Unindent();
411 
412             if (fullDetails)
413             {
414                 string realm = "";
415                 if (!string.IsNullOrEmpty(conf.HTTPRealm))
416                     realm = conf.HTTPRealm;
417 
418                 FormField[] temp = (FormField[])formFieldList.ToArray(typeof(FormField));
419                 Entry kpe = new Entry(
420                 (string[])URLs.ToArray(typeof(string)), realm,
421                 pwe.Strings.ReadSafe(PwDefs.TitleField), temp,
422                 KeePassLib.Utility.MemUtil.ByteArrayToHexString(pwe.Uuid.UuidBytes),
423                 alwaysAutoFill, neverAutoFill, alwaysAutoSubmit, neverAutoSubmit, priority,
424                 GetGroupFromPwGroup(pwe.ParentGroup), imageData,
425                 GetDatabaseFromPwDatabase(db, false, true),matchAccuracy);
426                 return kpe;
427             }
428             else
429             {
430                 return new LightEntry((string[])URLs.ToArray(typeof(string)),
431                     pwe.Strings.ReadSafe(PwDefs.TitleField),
432                     KeePassLib.Utility.MemUtil.ByteArrayToHexString(pwe.Uuid.UuidBytes),
433                     imageData, usernameName, usernameValue);
434             }
435         }
436 
GetGroupFromPwGroup(PwGroup pwg)437         private Group GetGroupFromPwGroup(PwGroup pwg)
438         {
439             //Debug.Indent();
440             //Stopwatch sw = Stopwatch.StartNew();
441 
442             string imageData = iconToBase64(pwg.CustomIconUuid, pwg.IconId);
443 
444             Group kpg = new Group(pwg.Name, KeePassLib.Utility.MemUtil.ByteArrayToHexString(pwg.Uuid.UuidBytes), imageData, pwg.GetFullPath("/", false));
445 
446             //sw.Stop();
447             //Debug.WriteLine("GetGroupFromPwGroup execution time: " + sw.Elapsed);
448             //Debug.Unindent();
449             return kpg;
450         }
451 
GetDatabaseFromPwDatabase(PwDatabase pwd, bool fullDetail, bool noDetail)452         private Database GetDatabaseFromPwDatabase(PwDatabase pwd, bool fullDetail, bool noDetail)
453         {
454             //Debug.Indent();
455             // Stopwatch sw = Stopwatch.StartNew();
456             if (fullDetail && noDetail)
457                 throw new ArgumentException("Don't be silly");
458 
459             PwGroup pwg = GetRootPwGroup(pwd);
460             Group rt = GetGroupFromPwGroup(pwg);
461             if (fullDetail)
462                 rt.ChildEntries = (Entry[])GetChildEntries(pwd, pwg, fullDetail, true);
463             else if (!noDetail)
464                 rt.ChildLightEntries = GetChildEntries(pwd, pwg, fullDetail, true);
465 
466             if (!noDetail)
467                 rt.ChildGroups = GetChildGroups(pwd, pwg, true, fullDetail);
468 
469             Database kpd = new Database(pwd.Name, pwd.IOConnectionInfo.Path, rt, (pwd == host.Database) ? true : false,
470                 DataExchangeModel.IconCache<string>.GetIconEncoding(pwd.IOConnectionInfo.Path) ?? "");
471             //  sw.Stop();
472             //  Debug.WriteLine("GetDatabaseFromPwDatabase execution time: " + sw.Elapsed);
473             //  Debug.Unindent();
474             return kpd;
475         }
476 
setPwEntryFromEntry(PwEntry pwe, Entry login)477         private void setPwEntryFromEntry(PwEntry pwe, Entry login)
478         {
479             bool firstPasswordFound = false;
480             EntryConfig conf = new EntryConfig(host.Database.GetKPRPCConfig().DefaultMatchAccuracy);
481             List<FormField> ffl = new List<FormField>();
482 
483             // Go through each form field, mostly just making a copy but with occasional tweaks such as default username and password selection
484             // by convention, we'll always have the first text field as the username when both reading and writing from the EntryConfig
485             foreach (FormField kpff in login.FormFieldList)
486             {
487                 if (kpff.Type == FormFieldType.FFTpassword && !firstPasswordFound)
488                 {
489                     ffl.Add(new FormField(kpff.Name, "KeePass password", "{PASSWORD}", kpff.Type, kpff.Id, kpff.Page, PlaceholderHandling.Default));
490                     pwe.Strings.Set("Password", new ProtectedString(host.Database.MemoryProtection.ProtectPassword, kpff.Value));
491                     firstPasswordFound = true;
492                 }
493                 else if (kpff.Type == FormFieldType.FFTusername)
494                 {
495                     ffl.Add(new FormField(kpff.Name, "KeePass username", "{USERNAME}", kpff.Type, kpff.Id, kpff.Page, PlaceholderHandling.Default));
496                     pwe.Strings.Set("UserName", new ProtectedString(host.Database.MemoryProtection.ProtectUserName, kpff.Value));
497                 }
498                 else
499                 {
500                     ffl.Add(new FormField(kpff.Name, kpff.Name, kpff.Value, kpff.Type, kpff.Id, kpff.Page, PlaceholderHandling.Default));
501                 }
502             }
503             conf.FormFieldList = ffl.ToArray();
504 
505             List<string> altURLs = new List<string>();
506 
507             for (int i = 0; i < login.URLs.Length; i++)
508             {
509                 string url = login.URLs[i];
510                 if (i == 0)
511                 {
512                     // We can't use the framework Uri.Port property here because
513                     // we are interested in whether it is explicit or not - the
514                     // Port property returns the default port for a protocol if
515                     // one is not explicitly included in the URL
516                     URLSummary urlsum = URLSummary.FromURL(url);
517 
518                     // Require more strict default matching for entries that come
519                     // with a port configured (user can override in the rare case
520                     // that they want the loose domain-level matching)
521                     if (!string.IsNullOrEmpty(urlsum.Port))
522                         conf.SetMatchAccuracyMethod(MatchAccuracyMethod.Hostname);
523 
524                     pwe.Strings.Set("URL", new ProtectedString(host.Database.MemoryProtection.ProtectUrl, url ?? ""));
525                 }
526                 else
527                     altURLs.Add(url);
528             }
529             conf.AltURLs = altURLs.ToArray();
530             conf.HTTPRealm = login.HTTPRealm;
531             conf.Version = 1;
532 
533             // Set some of the string fields
534             pwe.Strings.Set(PwDefs.TitleField, new ProtectedString(host.Database.MemoryProtection.ProtectTitle, login.Title ?? ""));
535 
536             // update the icon for this entry (in most cases we'll
537             // just detect that it is the same standard icon as before)
538             PwUuid customIconUUID = PwUuid.Zero;
539             PwIcon iconId = PwIcon.Key;
540             if (login.IconImageData != null
541                 && login.IconImageData.Length > 0
542                 && base64ToIcon(login.IconImageData, ref customIconUUID, ref iconId))
543             {
544                 if (customIconUUID == PwUuid.Zero)
545                     pwe.IconId = iconId;
546                 else
547                     pwe.CustomIconUuid = customIconUUID;
548             }
549 
550             pwe.SetKPRPCConfig(conf);
551         }
552 
dbIconToBase64(PwDatabase db)553         private string dbIconToBase64(PwDatabase db)
554         {
555             string cachedBase64 = DataExchangeModel.IconCache<string>.GetIconEncoding(db.IOConnectionInfo.Path);
556             if (string.IsNullOrEmpty(cachedBase64))
557             {
558                 // Don't think this should ever happen but we'll return a null icon if we have to
559                 return "";
560             }
561             else
562             {
563                 return cachedBase64;
564             }
565         }
566 
567         /// <summary>
568         /// extract the current icon information for this entry
569         /// </summary>
570         /// <param name="customIconUUID"></param>
571         /// <param name="iconId"></param>
572         /// <returns></returns>
iconToBase64(PwUuid customIconUUID, PwIcon iconId)573         private string iconToBase64(PwUuid customIconUUID, PwIcon iconId)
574         {
575             Image icon = null;
576             PwUuid uuid = null;
577 
578             string imageData = "";
579             if (customIconUUID != PwUuid.Zero)
580             {
581                 string cachedBase64 = DataExchangeModel.IconCache<PwUuid>.GetIconEncoding(customIconUUID);
582                 if (string.IsNullOrEmpty(cachedBase64))
583                 {
584                     object[] delParams = { customIconUUID };
585                     object invokeResult = host.MainWindow.Invoke(
586                         new KeePassRPCExt.GetCustomIconDelegate(
587                             KeePassRPCPlugin.GetCustomIcon), delParams);
588                     if (invokeResult != null)
589                     {
590                         icon = (Image)invokeResult;
591                     }
592                     if (icon != null)
593                     {
594                         uuid = customIconUUID;
595                     }
596                 }
597                 else
598                 {
599                     return cachedBase64;
600                 }
601             }
602 
603             // this happens if we didn't want to or couldn't find a custom icon
604             if (icon == null)
605             {
606                 int iconIdInt = (int)iconId;
607                 uuid = new PwUuid(new byte[]{
608                     (byte)(iconIdInt & 0xFF), (byte)(iconIdInt & 0xFF),
609                     (byte)(iconIdInt & 0xFF), (byte)(iconIdInt & 0xFF),
610                     (byte)(iconIdInt >> 8 & 0xFF), (byte)(iconIdInt >> 8 & 0xFF),
611                     (byte)(iconIdInt >> 8 & 0xFF), (byte)(iconIdInt >> 8 & 0xFF),
612                     (byte)(iconIdInt >> 16 & 0xFF), (byte)(iconIdInt >> 16 & 0xFF),
613                     (byte)(iconIdInt >> 16 & 0xFF), (byte)(iconIdInt >> 16 & 0xFF),
614                     (byte)(iconIdInt >> 24 & 0xFF), (byte)(iconIdInt >> 24 & 0xFF),
615                     (byte)(iconIdInt >> 24 & 0xFF), (byte)(iconIdInt >> 24 & 0xFF)
616                 });
617 
618                 string cachedBase64 = DataExchangeModel.IconCache<PwUuid>.GetIconEncoding(uuid);
619                 if (string.IsNullOrEmpty(cachedBase64))
620                 {
621                     object[] delParams = { (int)iconId };
622                     object invokeResult = host.MainWindow.Invoke(
623                         new KeePassRPCExt.GetIconDelegate(
624                             KeePassRPCPlugin.GetIcon), delParams);
625                     if (invokeResult != null)
626                     {
627                         icon = (Image)invokeResult;
628                     }
629                 }
630                 else
631                 {
632                     return cachedBase64;
633                 }
634             }
635 
636 
637             if (icon != null)
638             {
639                 // we found an icon but it wasn't in the cache so lets
640                 // calculate its base64 encoding and then add it to the cache
641                 using (MemoryStream ms = new MemoryStream())
642                 {
643                     icon.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
644                     imageData = Convert.ToBase64String(ms.ToArray());
645                 }
646                 DataExchangeModel.IconCache<PwUuid>.AddIcon(uuid, imageData);
647             }
648 
649             return imageData;
650         }
651 
652         /// <summary>
653         /// converts a string to the relevant icon for this entry
654         /// </summary>
655         /// <param name="imageData">base64 representation of the image</param>
656         /// <param name="customIconUUID">UUID of the generated custom icon; may be Zero</param>
657         /// <param name="iconId">PwIcon of the matched standard icon; ignore if customIconUUID != Zero</param>
658         /// <returns>true if the supplied imageData was converted into a customIcon
659         /// or matched with a standard icon.</returns>
base64ToIcon(string imageData, ref PwUuid customIconUUID, ref PwIcon iconId)660         private bool base64ToIcon(string imageData, ref PwUuid customIconUUID, ref PwIcon iconId)
661         {
662             iconId = PwIcon.Key;
663             customIconUUID = PwUuid.Zero;
664 
665             for (int i = 0; i < _standardIconsBase64.Length; i++)
666             {
667                 string item = _standardIconsBase64[i];
668                 if (item == imageData)
669                 {
670                     iconId = (PwIcon)i;
671                     return true;
672                 }
673             }
674 
675             try
676             {
677                 //MemoryStream id = new MemoryStream();
678                 //icon.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
679 
680                 using (Image img = KeePass.UI.UIUtil.LoadImage(Convert.FromBase64String(imageData)))
681                 using (Image imgNew = new Bitmap(img, new Size(16, 16)))
682                 using (MemoryStream ms = new MemoryStream())
683                 {
684                     imgNew.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
685 
686                     byte[] msByteArray = ms.ToArray();
687 
688                     foreach (PwCustomIcon item in host.Database.CustomIcons)
689                     {
690                         // re-use existing custom icon if it's already in the database
691                         // (This will probably fail if database is used on
692                         // both 32 bit and 64 bit machines - not sure why...)
693                         if (KeePassLib.Utility.MemUtil.ArraysEqual(msByteArray, item.ImageDataPng))
694                         {
695                             customIconUUID = item.Uuid;
696                             host.Database.UINeedsIconUpdate = true;
697                             return true;
698                         }
699                     }
700                     PwCustomIcon pwci = new PwCustomIcon(new PwUuid(true), msByteArray);
701                     host.Database.CustomIcons.Add(pwci);
702 
703                     customIconUUID = pwci.Uuid;
704                     host.Database.UINeedsIconUpdate = true;
705 
706                 }
707 
708                 return true;
709             }
710             catch
711             {
712                 return false;
713             }
714         }
715 
716         #endregion
717 
718         #region Configuration of KeePass/Kee and databases
719 
720         [JsonRpcMethod]
GetCurrentKFConfig()721         public Configuration GetCurrentKFConfig()
722         {
723             bool autoCommit = host.CustomConfig.GetBool("KeePassRPC.KeeFox.autoCommit", true);
724             string[] MRUList = new string[host.MainWindow.FileMruList.ItemCount];
725             for (uint i = 0; i < host.MainWindow.FileMruList.ItemCount; i++)
726                 MRUList[i] = ((IOConnectionInfo)host.MainWindow.FileMruList.GetItem(i).Value).Path;
727 
728             Configuration currentConfig = new Configuration(MRUList, autoCommit);
729             return currentConfig;
730         }
731 
732         [JsonRpcMethod]
GetApplicationMetadata()733         public ApplicationMetadata GetApplicationMetadata()
734         {
735             string KeePassVersion;
736             bool IsMono = false;
737             string NETCLR;
738             string NETversion;
739             string MonoVersion = "unknown";
740             // No point in outputting KeePassRPC version here since we know it has
741             // to match in order to be able to call this function
742 
743             NETCLR = Environment.Version.Major.ToString();
744             KeePassVersion = PwDefs.VersionString;
745 
746             Type type = Type.GetType("Mono.Runtime");
747             if (type != null)
748             {
749                 IsMono = true;
750                 NETversion = "";
751                 try
752                 {
753                     MethodInfo displayName = type.GetMethod("GetDisplayName",
754                         BindingFlags.NonPublic | BindingFlags.Static);
755                     if (displayName != null)
756                         MonoVersion = (string)displayName.Invoke(null, null);
757                 }
758                 catch (Exception)
759                 {
760                     MonoVersion = "unknown";
761                 }
762             }
763             else
764             {
765                 // Normally looking in the registry is the thing to try here but that means pulling
766                 // in lots of Win32 libraries into Mono so this alternative gets us some useful,
767                 // albeit incomplete, information. There shouldn't be any need to call this service
768                 // on a regular basis so it shouldn't matter that the use of reflection is a little inefficient
769 
770                 // v3.0 is of no interest to us and difficult to detect so we ignore
771                 // it and bundle those users in the v2 group
772                 NETversion =
773                     IsNet451OrNewer() ? "4.5.1" :
774                     IsNet45OrNewer() ? "4.5" :
775                     NETCLR == "4" ? "4.0" :
776                     IsNet35OrNewer() ? "3.5" :
777                     NETCLR == "2" ? "2.0" :
778                     "unknown";
779             }
780 
781             ApplicationMetadata appMetadata = new ApplicationMetadata(KeePassVersion, IsMono, NETCLR, NETversion, MonoVersion);
782             return appMetadata;
783         }
784 
IsNet35OrNewer()785         public static bool IsNet35OrNewer()
786         {
787             return Type.GetType("System.GCCollectionMode", false) != null;
788         }
789 
IsNet45OrNewer()790         public static bool IsNet45OrNewer()
791         {
792             return Type.GetType("System.Reflection.ReflectionContext", false) != null;
793         }
794 
IsNet451OrNewer()795         public static bool IsNet451OrNewer()
796         {
797             return Type.GetType("System.Runtime.GCLargeObjectHeapCompactionMode", false) != null;
798         }
799         //TODO:1.6: Newer .NET versions
800 
801         #endregion
802 
803         #region Retrival and manipulation of databases and the KeePass app
804 
805         [JsonRpcMethod]
GetDatabaseName()806         public string GetDatabaseName()
807         {
808             if (!host.Database.IsOpen)
809                 return "";
810             return (host.Database.Name.Length > 0 ? host.Database.Name : "no name");
811         }
812 
813         [JsonRpcMethod]
GetDatabaseFileName()814         public string GetDatabaseFileName()
815         {
816             return host.Database.IOConnectionInfo.Path;
817         }
818 
819         /// <summary>
820         /// changes current active database
821         /// </summary>
822         /// <param name="fileName">Path to database to open. If empty, user is prompted to choose a file</param>
823         /// <param name="closeCurrent">if true, currently active database is closed first. if false,
824         /// both stay open with fileName DB active</param>
825         [JsonRpcMethod]
ChangeDatabase(string fileName, bool closeCurrent)826         public void ChangeDatabase(string fileName, bool closeCurrent)
827         {
828             if (closeCurrent && host.MainWindow.DocumentManager.ActiveDatabase != null && host.MainWindow.DocumentManager.ActiveDatabase.IsOpen)
829             {
830                 host.MainWindow.DocumentManager.CloseDatabase(host.MainWindow.DocumentManager.ActiveDatabase);
831             }
832 
833             KeePassLib.Serialization.IOConnectionInfo ioci = null;
834 
835             if (fileName != null && fileName.Length > 0)
836             {
837                 ioci = new KeePassLib.Serialization.IOConnectionInfo();
838                 ioci.Path = fileName;
839                 ioci = CompleteConnectionInfoUsingMru(ioci);
840             }
841 
842             // Set the current document / database to be the one we've been asked to display (may already be the case)
843             // This is because the minimise/restore trick utilised a few frames later prompts KeePass into raising an
844             // "enter key" dialog for the currently active database. This little check makes sure that the user sees
845             // the database they've asked for first (assuming the database they want is already open but locked)
846             // We can't stop an unneccessary prompt being seen if the user has asked for a new database to be opened
847             // and the current workspace is locked
848             //
849             // We do this regardless of whether the DB is already open or locked
850             //
851             //TODO: Need to verify this works OK with unusual circumstances like one DB open but others locked
852             if (ioci != null)
853                 foreach (PwDocument doc in host.MainWindow.DocumentManager.Documents)
854                     if (doc.LockedIoc.Path == fileName ||
855                         (doc.Database.IsOpen && doc.Database.IOConnectionInfo.Path == fileName))
856                         host.MainWindow.DocumentManager.ActiveDocument = doc;
857 
858             // Going to take a new approach for a bit to see how it works out...
859             //
860             // before explicitly asking user to log into the correct DB we'll set up a "fake" document in KeePass
861             // in the hope that the minimise/restore trick will get KeePass to prompt the user on our behalf
862             // (regardless of state of existing documents and newly requested document)
863             if (ioci != null
864                 && !(host.MainWindow.DocumentManager.ActiveDocument.Database.IsOpen && host.MainWindow.DocumentManager.ActiveDocument.Database.IOConnectionInfo.Path == fileName)
865                 && !(!host.MainWindow.DocumentManager.ActiveDocument.Database.IsOpen && host.MainWindow.DocumentManager.ActiveDocument.LockedIoc.Path == fileName))
866             {
867                 PwDocument doc = host.MainWindow.DocumentManager.CreateNewDocument(true);
868                 //IOConnectionInfo ioci = new IOConnectionInfo();
869                 //ioci.Path = fileName;
870                 doc.LockedIoc = ioci;
871             }
872 
873             // NB: going to modify implementation of the following function call so that only KeePass initiates the prompt (need to verify cross-platform, etc. even if it seems to work on win7x64)
874             // if it works on some platforms, I will make it work on all platforms that support it and fall back to the old clunky method for others.
875             host.MainWindow.BeginInvoke((MethodInvoker)delegate { promptUserToOpenDB(ioci); });
876             return;
877         }
878 
879         /// <summary>
880         /// notifies KeePass of a change in current location. The location in the KeePass config file
881         /// is updated and current databse state is modified if applicable
882         /// </summary>
883         /// <param name="locationId">New location identifier (e.g. "work", "home") Case insensitive</param>
884         [JsonRpcMethod]
ChangeLocation(string locationId)885         public void ChangeLocation(string locationId)
886         {
887             if (string.IsNullOrEmpty(locationId))
888                 return;
889             locationId = locationId.ToLower();
890 
891             host.CustomConfig.SetString("KeePassRPC.currentLocation", locationId);
892             host.MainWindow.Invoke((MethodInvoker)delegate { host.MainWindow.SaveConfig(); });
893 
894             // tell all RPC clients they need to refresh their representation of the KeePass data
895             if (host.Database.IsOpen)
896                 KeePassRPCPlugin.SignalAllManagedRPCClients(Signal.DATABASE_SELECTED);
897 
898             return;
899         }
900 
901         /// <summary>
902         /// Gets a list of all password profiles available in the current KeePass instance
903         /// </summary>
904         [JsonRpcMethod]
GetPasswordProfiles()905         public string[] GetPasswordProfiles()
906         {
907             List<PwProfile> profiles = KeePass.Util.PwGeneratorUtil.GetAllProfiles(true);
908             List<string> profileNames = new List<string>(profiles.Count);
909             foreach (PwProfile prof in profiles)
910                 profileNames.Add(prof.Name);
911 
912             return profileNames.ToArray();
913         }
914 
915         [JsonRpcMethod]
GeneratePassword(string profileName, string url)916         public string GeneratePassword(string profileName, string url)
917         {
918             PwProfile profile = null;
919 
920             if (string.IsNullOrEmpty(profileName))
921                 profile = KeePass.Program.Config.PasswordGenerator.LastUsedProfile;
922             else
923             {
924                 foreach (PwProfile pp in KeePass.Util.PwGeneratorUtil.GetAllProfiles(false))
925                 {
926                     if (pp.Name == profileName)
927                     {
928                         profile = pp;
929                         KeePass.Program.Config.PasswordGenerator.LastUsedProfile = pp;
930                         break;
931                     }
932                 }
933             }
934 
935             if (profile == null)
936                 return "";
937 
938             ProtectedString newPassword = new ProtectedString();
939             PwGenerator.Generate(out newPassword, profile, null, host.PwGeneratorPool);
940             var password = newPassword.ReadString();
941 
942             if (host.CustomConfig.GetBool("KeePassRPC.KeeFox.backupNewPasswords", true))
943                 AddPasswordBackupLogin(password, url);
944 
945             return password;
946         }
947 
AddPasswordBackupLogin(string password, string url)948         private void AddPasswordBackupLogin(string password, string url)
949         {
950             if (!host.Database.IsOpen)
951                 return;
952 
953             PwDatabase chosenDB = SelectDatabase("");
954             var parentGroup = KeePassRPCPlugin.GetAndInstallKeePasswordBackupGroup(chosenDB);
955 
956             PwEntry newLogin = new PwEntry(true, true);
957             newLogin.Strings.Set(PwDefs.TitleField, new ProtectedString(
958                 chosenDB.MemoryProtection.ProtectTitle, "Kee generated password at: " + DateTime.Now));
959             newLogin.Strings.Set(PwDefs.UrlField, new ProtectedString(
960                 chosenDB.MemoryProtection.ProtectUrl, url));
961             newLogin.Strings.Set(PwDefs.PasswordField, new ProtectedString(
962                 chosenDB.MemoryProtection.ProtectPassword, password));
963             EntryConfig conf = new EntryConfig(MatchAccuracyMethod.Hostname);
964             conf.Hide = true;
965             newLogin.SetKPRPCConfig(conf);
966             parentGroup.AddEntry(newLogin, true);
967 
968             // We can't save the database at this point because KeePass steals
969             // window focus while saving; that breaks Firefox's Australis UI panels.
970             host.MainWindow.BeginInvoke(new dlgUpdateUINoSave(updateUINoSave));
971 
972             return;
973         }
974 
975         #endregion
976 
977         #region Retrival and manipulation of entries and groups
978 
979         /// <summary>
980         /// removes a single entry from the database
981         /// </summary>
982         /// <param name="uuid">The unique indentifier of the entry we want to remove</param>
983         /// <returns>true if entry removed successfully, false if it failed</returns>
984         [JsonRpcMethod]
RemoveEntry(string uuid)985         public bool RemoveEntry(string uuid)
986         {
987             // Make sure there is an active database
988             if (!ensureDBisOpen()) return false;
989 
990             if (uuid != null && uuid.Length > 0)
991             {
992                 PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(uuid));
993 
994                 PwEntry matchedLogin = GetRootPwGroup(host.Database).FindEntry(pwuuid, true);
995 
996                 if (matchedLogin == null)
997                     throw new Exception("Could not find requested entry.");
998 
999                 PwGroup matchedLoginParent = matchedLogin.ParentGroup;
1000                 if (matchedLoginParent == null) return false; // Can't remove
1001 
1002                 matchedLoginParent.Entries.Remove(matchedLogin);
1003 
1004                 PwGroup recycleBin = host.Database.RootGroup.FindGroup(host.Database.RecycleBinUuid, true);
1005 
1006                 if (host.Database.RecycleBinEnabled == false)
1007                 {
1008                     if (!KeePassLib.Utility.MessageService.AskYesNo(KPRes.DeleteEntriesQuestionSingle, KPRes.DeleteEntriesTitleSingle))
1009                         return false;
1010 
1011                     PwDeletedObject pdo = new PwDeletedObject();
1012                     pdo.Uuid = matchedLogin.Uuid;
1013                     pdo.DeletionTime = DateTime.Now;
1014                     host.Database.DeletedObjects.Add(pdo);
1015                 }
1016                 else
1017                 {
1018                     if (recycleBin == null)
1019                     {
1020                         recycleBin = new PwGroup(true, true, KPRes.RecycleBin, PwIcon.TrashBin);
1021                         recycleBin.EnableAutoType = false;
1022                         recycleBin.EnableSearching = false;
1023                         host.Database.RootGroup.AddGroup(recycleBin, true);
1024 
1025                         host.Database.RecycleBinUuid = recycleBin.Uuid;
1026                     }
1027 
1028                     recycleBin.AddEntry(matchedLogin, true);
1029                     matchedLogin.Touch(false);
1030                 }
1031 
1032                 //matchedLogin.ParentGroup.Entries.Remove(matchedLogin);
1033                 host.MainWindow.BeginInvoke(new dlgSaveDB(saveDB), host.Database);
1034                 return true;
1035             }
1036             return false;
1037         }
1038 
1039         /// <summary>
1040         /// removes a single group and its contents from the database
1041         /// </summary>
1042         /// <param name="uuid">The unique indentifier of the group we want to remove</param>
1043         /// <returns>true if group removed successfully, false if it failed</returns>
1044         [JsonRpcMethod]
RemoveGroup(string uuid)1045         public bool RemoveGroup(string uuid)
1046         {
1047             // Make sure there is an active database
1048             if (!ensureDBisOpen()) return false;
1049 
1050             if (uuid != null && uuid.Length > 0)
1051             {
1052                 PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(uuid));
1053 
1054                 PwGroup matchedGroup = GetRootPwGroup(host.Database).FindGroup(pwuuid, true);
1055 
1056                 if (matchedGroup == null)
1057                     throw new Exception("Could not find requested entry.");
1058 
1059                 PwGroup matchedGroupParent = matchedGroup.ParentGroup;
1060                 if (matchedGroupParent == null) return false; // Can't remove
1061 
1062                 matchedGroupParent.Groups.Remove(matchedGroup);
1063 
1064                 PwGroup recycleBin = host.Database.RootGroup.FindGroup(host.Database.RecycleBinUuid, true);
1065 
1066                 if (host.Database.RecycleBinEnabled == false)
1067                 {
1068                     if (!KeePassLib.Utility.MessageService.AskYesNo(KPRes.DeleteGroupQuestion, KPRes.DeleteGroupTitle))
1069                         return false;
1070 
1071                     PwDeletedObject pdo = new PwDeletedObject();
1072                     pdo.Uuid = matchedGroup.Uuid;
1073                     pdo.DeletionTime = DateTime.Now;
1074                     host.Database.DeletedObjects.Add(pdo);
1075                 }
1076                 else
1077                 {
1078                     if (recycleBin == null)
1079                     {
1080                         recycleBin = new PwGroup(true, true, KPRes.RecycleBin, PwIcon.TrashBin);
1081                         recycleBin.EnableAutoType = false;
1082                         recycleBin.EnableSearching = false;
1083                         host.Database.RootGroup.AddGroup(recycleBin, true);
1084 
1085                         host.Database.RecycleBinUuid = recycleBin.Uuid;
1086                     }
1087 
1088                     recycleBin.AddGroup(matchedGroup, true);
1089                     matchedGroup.Touch(false);
1090                 }
1091 
1092                 host.MainWindow.BeginInvoke(new dlgSaveDB(saveDB), host.Database);
1093 
1094                 return true;
1095             }
1096             return false;
1097         }
1098 
SelectDatabase(string dbFileName)1099         private PwDatabase SelectDatabase(string dbFileName)
1100         {
1101             PwDatabase chosenDB = host.Database;
1102             if (!string.IsNullOrEmpty(dbFileName))
1103             {
1104                 try
1105                 {
1106                     List<PwDatabase> allDBs = host.MainWindow.DocumentManager.GetOpenDatabases();
1107                     foreach (PwDatabase db in allDBs)
1108                         if (db.IOConnectionInfo.Path == dbFileName)
1109                         {
1110                             chosenDB = db;
1111                             break;
1112                         }
1113                 }
1114                 catch (Exception)
1115                 {
1116                     // If we fail to find a suitable DB for any reason we'll just continue as if no restriction had been requested
1117                 }
1118             }
1119             return chosenDB;
1120         }
1121 
1122         /// <summary>
1123         /// Add a new password/login to the active KeePass database
1124         /// </summary>
1125         /// <param name="login">The KeePassRPC representation of the login to be added</param>
1126         /// <param name="parentUUID">The UUID of the parent group for the new login. If null, the root group will be used.</param>
1127         /// <param name="dbFileName">The file name of the database we want to save this entry to;
1128         ///                         if empty or null, the currently active database is used</param>
1129         [JsonRpcMethod]
AddLogin(Entry login, string parentUUID, string dbFileName)1130         public Entry AddLogin(Entry login, string parentUUID, string dbFileName)
1131         {
1132             // Make sure there is an active database
1133             if (!ensureDBisOpen()) return null;
1134 
1135             PwEntry newLogin = new PwEntry(true, true);
1136 
1137             setPwEntryFromEntry(newLogin, login);
1138 
1139             // find the database
1140             PwDatabase chosenDB = SelectDatabase(dbFileName);
1141 
1142             PwGroup parentGroup = GetRootPwGroup(chosenDB); // if in doubt we'll stick it in the root folder
1143 
1144             if (parentUUID != null && parentUUID.Length > 0)
1145             {
1146                 PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(parentUUID));
1147 
1148                 PwGroup matchedGroup = GetRootPwGroup(chosenDB).FindGroup(pwuuid, true);
1149 
1150                 if (matchedGroup != null)
1151                     parentGroup = matchedGroup;
1152             }
1153 
1154             parentGroup.AddEntry(newLogin, true);
1155 
1156             if (host.CustomConfig.GetBool("KeePassRPC.KeeFox.editNewEntries", false))
1157                 host.MainWindow.BeginInvoke(new dlgOpenLoginEditorWindow(OpenLoginEditorWindow), newLogin, chosenDB);
1158             else
1159                 host.MainWindow.BeginInvoke(new dlgSaveDB(saveDB), chosenDB);
1160 
1161             Entry output = (Entry)GetEntryFromPwEntry(newLogin, MatchAccuracy.Best, true, chosenDB);
1162 
1163             return output;
1164         }
1165 
1166         /// <summary>
1167         /// Add a new group/folder to the active KeePass database
1168         /// </summary>
1169         /// <param name="name">The name of the group to be added</param>
1170         /// <param name="parentUUID">The UUID of the parent group for the new group. If null, the root group will be used.</param>
1171         /// <param name="current__"></param>
1172         [JsonRpcMethod]
AddGroup(string name, string parentUUID)1173         public Group AddGroup(string name, string parentUUID)
1174         {
1175             // Make sure there is an active database
1176             if (!ensureDBisOpen()) return null;
1177 
1178             PwGroup newGroup = new PwGroup(true, true);
1179             newGroup.Name = name;
1180 
1181             PwGroup parentGroup = GetRootPwGroup(host.Database); // if in doubt we'll stick it in the root folder
1182 
1183             if (parentUUID != null && parentUUID.Length > 0)
1184             {
1185                 PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(parentUUID));
1186 
1187                 PwGroup matchedGroup = host.Database.RootGroup.Uuid == pwuuid ? host.Database.RootGroup : host.Database.RootGroup.FindGroup(pwuuid, true);
1188 
1189                 if (matchedGroup != null)
1190                     parentGroup = matchedGroup;
1191             }
1192 
1193             parentGroup.AddGroup(newGroup, true);
1194 
1195             host.MainWindow.BeginInvoke(new dlgSaveDB(saveDB), host.Database);
1196 
1197             Group output = GetGroupFromPwGroup(newGroup);
1198 
1199             return output;
1200         }
1201 
1202         /// <summary>
1203         /// Updates an existing login
1204         /// </summary>
1205         /// <param name="login">A login that contains data to be copied into the existing login</param>
1206         /// <param name="oldLoginUUID">The UUID that identifies the login we want to update</param>
1207         /// <param name="urlMergeMode">1= Replace the entry's URL (but still fill forms if you visit the old URL)
1208         ///2= Replace the entry's URL (delete the old URL completely)
1209         ///3= Keep the old entry's URL (but still fill forms if you visit the new URL)
1210         ///4= Keep the old entry's URL (don't add the new URL to the entry)</param>
1211         /// <param name="dbFileName">Database that contains the login to update</param>
1212         /// <returns>The updated login</returns>
1213         [JsonRpcMethod]
UpdateLogin(Entry login, string oldLoginUUID, int urlMergeMode, string dbFileName)1214         public Entry UpdateLogin(Entry login, string oldLoginUUID, int urlMergeMode, string dbFileName)
1215         {
1216             if (login == null)
1217                 throw new ArgumentException("(new) login was not passed to the updateLogin function");
1218             if (string.IsNullOrEmpty(oldLoginUUID))
1219                 throw new ArgumentException("oldLoginUUID was not passed to the updateLogin function");
1220             if (string.IsNullOrEmpty(dbFileName))
1221                 throw new ArgumentException("dbFileName was not passed to the updateLogin function");
1222 
1223             // Make sure there is an active database
1224             if (!ensureDBisOpen()) return null;
1225 
1226             // There are odd bits of the resulting new login that we don't
1227             // need but the vast majority is going to be useful
1228             PwEntry newLoginData = new PwEntry(true, true);
1229             setPwEntryFromEntry(newLoginData, login);
1230 
1231             // find the database
1232             PwDatabase chosenDB = SelectDatabase(dbFileName);
1233 
1234             PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(oldLoginUUID));
1235             PwEntry entryToUpdate = GetRootPwGroup(chosenDB).FindEntry(pwuuid, true);
1236             if (entryToUpdate == null)
1237                 throw new Exception("oldLoginUUID could not be resolved to an existing entry.");
1238 
1239             MergeEntries(entryToUpdate, newLoginData, urlMergeMode, chosenDB);
1240 
1241             host.MainWindow.BeginInvoke(new dlgSaveDB(saveDB), chosenDB);
1242 
1243             Entry updatedEntry = (Entry)GetEntryFromPwEntry(entryToUpdate, MatchAccuracy.Best, true, chosenDB);
1244 
1245             return updatedEntry;
1246         }
1247 
MergeEntries(PwEntry destination, PwEntry source, int urlMergeMode, PwDatabase db)1248         private void MergeEntries(PwEntry destination, PwEntry source, int urlMergeMode, PwDatabase db)
1249         {
1250             EntryConfig destConfig = destination.GetKPRPCConfig(db.GetKPRPCConfig().DefaultMatchAccuracy);
1251             if (destConfig == null)
1252                 return;
1253 
1254             EntryConfig sourceConfig = source.GetKPRPCConfig(db.GetKPRPCConfig().DefaultMatchAccuracy);
1255             if (sourceConfig == null)
1256                 return;
1257 
1258             destination.CreateBackup(db);
1259 
1260             destConfig.HTTPRealm = sourceConfig.HTTPRealm;
1261             destination.IconId = source.IconId;
1262             destination.CustomIconUuid = source.CustomIconUuid;
1263             destination.Strings.Set("UserName", new ProtectedString(
1264                 host.Database.MemoryProtection.ProtectUserName, source.Strings.ReadSafe("UserName")));
1265             destination.Strings.Set("Password", new ProtectedString(
1266                 host.Database.MemoryProtection.ProtectPassword, source.Strings.ReadSafe("Password")));
1267             destConfig.FormFieldList = sourceConfig.FormFieldList;
1268 
1269             // This algorithm could probably be made more efficient (lots of O(n) operations
1270             // but we're dealing with pretty small n so I've gone with the conceptually
1271             // easiest approach for now).
1272 
1273             List<string> destURLs = new List<string>();
1274             destURLs.Add(destination.Strings.ReadSafe("URL"));
1275             if (destConfig.AltURLs != null)
1276                 destURLs.AddRange(destConfig.AltURLs);
1277 
1278             List<string> sourceURLs = new List<string>();
1279             sourceURLs.Add(source.Strings.ReadSafe("URL"));
1280             if (sourceConfig.AltURLs != null)
1281                 sourceURLs.AddRange(sourceConfig.AltURLs);
1282 
1283             switch (urlMergeMode)
1284             {
1285                 case 1:
1286                     MergeInNewURLs(destURLs, sourceURLs);
1287                     break;
1288                 case 2:
1289                     destURLs.RemoveAt(0);
1290                     MergeInNewURLs(destURLs, sourceURLs);
1291                     break;
1292                 case 3:
1293                     if (sourceURLs.Count > 0)
1294                     {
1295                         foreach (string sourceUrl in sourceURLs)
1296                             if (!destURLs.Contains(sourceUrl))
1297                                 destURLs.Add(sourceUrl);
1298                     }
1299                     break;
1300                 case 4:
1301                 default:
1302                     // No changes to URLs
1303                     break;
1304             }
1305 
1306             // These might not have changed but meh
1307             destination.Strings.Set("URL", new ProtectedString(host.Database.MemoryProtection.ProtectUrl, destURLs[0]));
1308             destConfig.AltURLs = new string[0];
1309             if (destURLs.Count > 1)
1310                 destConfig.AltURLs = destURLs.GetRange(1,destURLs.Count-1).ToArray();
1311 
1312             destination.SetKPRPCConfig(destConfig);
1313             destination.Touch(true);
1314         }
1315 
MergeInNewURLs(List<string> destURLs, List<string> sourceURLs)1316         private static void MergeInNewURLs(List<string> destURLs, List<string> sourceURLs)
1317         {
1318             if (sourceURLs.Count > 0)
1319             {
1320                 for (int i = sourceURLs.Count - 1; i >= 0; i--)
1321                 {
1322                     string sourceUrl = sourceURLs[i];
1323 
1324                     if (!destURLs.Contains(sourceUrl))
1325                     {
1326                         destURLs.Insert(0, sourceUrl);
1327                     }
1328                     else if (i == 0)
1329                     {
1330                         // Promote the URL from alternative URL list to primary URL
1331                         destURLs.Remove(sourceUrl);
1332                         destURLs.Insert(0, sourceUrl);
1333                     }
1334                 }
1335             }
1336         }
1337 
1338         /// <summary>
1339         /// Return the parent group of the object with the supplied UUID
1340         /// </summary>
1341         /// <param name="uuid">the UUID of the object we want to find the parent of</param>
1342         /// <param name="current__"></param>
1343         /// <returns>the parent group</returns>
1344         [JsonRpcMethod]
GetParent(string uuid)1345         public Group GetParent(string uuid)
1346         {
1347             Group output;
1348 
1349             // Make sure there is an active database
1350             if (!ensureDBisOpen()) return null;
1351 
1352             PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(uuid));
1353             PwGroup rootGroup = GetRootPwGroup(host.Database);
1354 
1355             try
1356             {
1357 
1358                 PwEntry thisEntry = rootGroup.FindEntry(pwuuid, true);
1359                 if (thisEntry != null && thisEntry.ParentGroup != null)
1360                 {
1361                     output = GetGroupFromPwGroup(thisEntry.ParentGroup);
1362                     return output;
1363                 }
1364 
1365                 PwGroup thisGroup = rootGroup.FindGroup(pwuuid, true);
1366                 if (thisGroup != null && thisGroup.ParentGroup != null)
1367                 {
1368                     output = GetGroupFromPwGroup(thisGroup.ParentGroup);
1369                     return output;
1370                 }
1371             }
1372             catch (Exception)
1373             {
1374                 return null;
1375             }
1376             output = GetGroupFromPwGroup(rootGroup);
1377             return output;
1378         }
1379 
1380         /// <summary>
1381         /// Return the root group of the active database
1382         /// </summary>
1383         /// <param name="current__"></param>
1384         /// <returns>the root group</returns>
1385         [JsonRpcMethod]
GetRoot()1386         public Group GetRoot()
1387         {
1388             return GetGroupFromPwGroup(GetRootPwGroup(host.Database));
1389         }
1390 
1391         /// <summary>
1392         /// Return the root group of the active database
1393         /// </summary>
1394         /// <param name="location">Selects an alternative root group based on KeePass location; null or empty string = default root group</param>
1395         /// <returns>the root group</returns>
GetRootPwGroup(PwDatabase pwd, string location)1396         public PwGroup GetRootPwGroup(PwDatabase pwd, string location)
1397         {
1398             if (pwd == null)
1399                 pwd = host.Database;
1400 
1401             if (!string.IsNullOrEmpty(location))
1402             {
1403                 // If any listed group UUID is found in this database, set it as the Kee home group
1404                 string rootGroupsConfig = host.CustomConfig
1405                     .GetString("KeePassRPC.knownLocations." + location + ".RootGroups", "");
1406                 string[] rootGroups = new string[0];
1407 
1408                 if (!string.IsNullOrEmpty(rootGroupsConfig))
1409                 {
1410                     rootGroups = rootGroupsConfig.Split(',');
1411                     foreach (string rootGroupId in rootGroups)
1412                     {
1413                         PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(rootGroupId));
1414                         PwGroup matchedGroup = host.Database.RootGroup.Uuid == pwuuid ? host.Database.RootGroup : host.Database.RootGroup.FindGroup(pwuuid, true);
1415 
1416                         if (matchedGroup == null)
1417                             continue;
1418 
1419                         return matchedGroup;
1420                     }
1421                     // If no match found we'll just return the default root group
1422                 }
1423                 // If no locations found we'll just return the default root group
1424             }
1425 
1426             var conf = pwd.GetKPRPCConfig();
1427             if (!string.IsNullOrWhiteSpace(conf.RootUUID) && conf.RootUUID.Length == 32)
1428             {
1429                 string uuid = conf.RootUUID;
1430 
1431                 PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(uuid));
1432 
1433                 PwGroup matchedGroup = pwd.RootGroup.Uuid == pwuuid ? pwd.RootGroup : pwd.RootGroup.FindGroup(pwuuid, true);
1434 
1435                 if (matchedGroup == null)
1436                     throw new Exception("Could not find requested group. Have you deleted your Kee home group? Set a new one and try again.");
1437 
1438                 return matchedGroup;
1439             }
1440             else
1441             {
1442                 return pwd.RootGroup;
1443             }
1444         }
1445 
1446         /// <summary>
1447         /// Return the root group of the active database for the current location
1448         /// </summary>
1449         /// <returns>the root group</returns>
1450         [JsonRpcMethod]
GetRootPwGroup(PwDatabase pwd)1451         public PwGroup GetRootPwGroup(PwDatabase pwd)
1452         {
1453             string locationId = host.CustomConfig
1454                .GetString("KeePassRPC.currentLocation", "");
1455             return GetRootPwGroup(pwd, locationId);
1456         }
1457 
1458         [JsonRpcMethod]
GetAllDatabases(bool fullDetails)1459         public Database[] GetAllDatabases(bool fullDetails)
1460         {
1461             Debug.Indent();
1462             Stopwatch sw = Stopwatch.StartNew();
1463 
1464             List<PwDatabase> dbs = host.MainWindow.DocumentManager.GetOpenDatabases();
1465             // unless the DB is the wrong version
1466             dbs = dbs.FindAll(ConfigIsCorrectVersion);
1467             List<Database> output = new List<Database>(1);
1468 
1469             foreach (PwDatabase db in dbs)
1470             {
1471                 output.Add(GetDatabaseFromPwDatabase(db, fullDetails, false));
1472             }
1473             Database[] dbarray = output.ToArray();
1474             sw.Stop();
1475             Debug.WriteLine("GetAllDatabases execution time: " + sw.Elapsed);
1476             Debug.Unindent();
1477             return dbarray;
1478         }
1479 
ConfigIsCorrectVersion(PwDatabase t)1480         private bool ConfigIsCorrectVersion(PwDatabase t)
1481         {
1482             // Both version 2 and 3 are correct since their differences
1483             // do not extend to the public API exposed by KPRPC
1484             if (t.CustomData.Exists("KeePassRPC.KeeFox.configVersion")
1485                 && t.CustomData.Get("KeePassRPC.KeeFox.configVersion") == "2")
1486             {
1487                 return true;
1488             }
1489             else if (t.GetKPRPCConfig().Version == 3)
1490             {
1491                 return true;
1492             }
1493             else
1494             {
1495                 return false;
1496             }
1497         }
1498 
1499         /// <summary>
1500         /// Return a list of every entry in the database that has a URL
1501         /// </summary>
1502         /// <returns>all logins in the database that have a URL</returns>
1503         [JsonRpcMethod]
GetEntries()1504         public Entry[] GetEntries()
1505         {
1506             return getAllLogins(true);
1507         }
1508 
1509         /// <summary>
1510         /// Return a list of every entry in the database that has a URL
1511         /// </summary>
1512         /// <returns>all logins in the database that have a URL</returns>
1513         /// <remarks>GetAllLogins is deprecated. Use GetEntries instead.</remarks>
1514         [JsonRpcMethod]
GetAllLogins()1515         public Entry[] GetAllLogins()
1516         {
1517             return getAllLogins(true);
1518         }
1519 
1520         /// <summary>
1521         /// Return a list of every entry in the database - this includes entries without an URL
1522         /// </summary>
1523         /// <returns>all logins in the database</returns>
1524         [JsonRpcMethod]
GetAllEntries()1525         public Entry[] GetAllEntries()
1526         {
1527             return getAllLogins(false);
1528         }
1529 
1530         /// <summary>
1531         /// Returns a list of every entry in the database
1532         /// </summary>
1533         /// <param name="urlRequired">true = URL field must exist for a child entry to be returned, false = all entries are returned</param>
1534         /// <param name="current__"></param>
1535         /// <returns>all logins in the database subject to the urlRequired setting</returns>
getAllLogins(bool urlRequired)1536         public Entry[] getAllLogins(bool urlRequired)
1537         {
1538             int count = 0;
1539             List<Entry> allEntries = new List<Entry>();
1540 
1541             // Make sure there is an active database
1542             if (!ensureDBisOpen()) { return null; }
1543 
1544             KeePassLib.Collections.PwObjectList<PwEntry> output;
1545             output = GetRootPwGroup(host.Database).GetEntries(true);
1546 
1547             foreach (PwEntry pwe in output)
1548             {
1549                 if (EntryIsInRecycleBin(pwe, host.Database))
1550                     continue; // ignore if it's in the recycle bin
1551 
1552                 if (urlRequired && string.IsNullOrEmpty(pwe.Strings.ReadSafe("URL")))
1553                     continue; // ignore if it has no URL
1554 
1555                 Entry kpe = (Entry)GetEntryFromPwEntry(pwe, MatchAccuracy.None, true, host.Database, true);
1556                 if (kpe != null) // is null if entry is marked as hidden from KPRPC
1557                 {
1558                     allEntries.Add(kpe);
1559                     count++;
1560                 }
1561             }
1562 
1563             allEntries.Sort(delegate(Entry e1, Entry e2)
1564             {
1565                 return e1.Title.CompareTo(e2.Title);
1566             });
1567 
1568             return allEntries.ToArray();
1569         }
1570 
EntryIsInRecycleBin(PwEntry pwe, PwDatabase db)1571         private bool EntryIsInRecycleBin(PwEntry pwe, PwDatabase db)
1572         {
1573             PwGroup parent = pwe.ParentGroup;
1574             while (parent != null)
1575             {
1576                 if (db.RecycleBinUuid.Equals(parent.Uuid))
1577                     return true;
1578                 parent = parent.ParentGroup;
1579             }
1580             return false;
1581         }
1582 
1583         /// <summary>
1584         /// Returns a list of every entry contained within a group (not recursive)
1585         /// </summary>
1586         /// <param name="uuid">the unique ID of the group we're interested in.</param>
1587         /// <param name="current__"></param>
1588         /// <returns>the list of every entry with a URL directly inside the group.</returns>
1589         [JsonRpcMethod]
GetChildEntries(string uuid)1590         public Entry[] GetChildEntries(string uuid)
1591         {
1592             PwGroup matchedGroup;
1593             matchedGroup = findMatchingGroup(uuid);
1594 
1595             return (Entry[])GetChildEntries(host.Database, matchedGroup, true, true);
1596         }
1597 
1598         /// <summary>
1599         /// Returns a list of all the entry contained within a group - including ones missing a URL (not recursive)
1600         /// </summary>
1601         /// <param name="uuid">the unique ID of the group we're interested in.</param>
1602         /// <param name="current__"></param>
1603         /// <returns>the list of every entry directly inside the group.</returns>
1604         [JsonRpcMethod]
GetAllChildEntries(string uuid)1605         public Entry[] GetAllChildEntries(string uuid)
1606         {
1607             PwGroup matchedGroup;
1608             matchedGroup = findMatchingGroup(uuid);
1609 
1610             return (Entry[])GetChildEntries(host.Database, matchedGroup, true, false);
1611         }
1612 
1613         /// <summary>
1614         /// Finds the group that matches a UUID, else return the root group
1615         /// </summary>
1616         /// <param name="uuid">the unique ID of the group we're interested in.</param>
1617         /// <returns>Group that matches the UUID, else the root group.</returns>
findMatchingGroup(string uuid)1618         private PwGroup findMatchingGroup(string uuid)
1619         {
1620             PwGroup matchedGroup;
1621             if (!string.IsNullOrEmpty(uuid))
1622             {
1623                 PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(uuid));
1624 
1625                 matchedGroup = host.Database.RootGroup.Uuid == pwuuid ? host.Database.RootGroup : host.Database.RootGroup.FindGroup(pwuuid, true);
1626             }
1627             else
1628             {
1629                 matchedGroup = GetRootPwGroup(host.Database);
1630             }
1631 
1632             if (matchedGroup == null)
1633                 throw new Exception("Could not find requested group. Have you deleted your Kee home group? Set a new one and try again.");
1634 
1635             return matchedGroup;
1636         }
1637 
1638         /// <summary>
1639         /// Returns a list of every entry contained within a group (not recursive)
1640         /// </summary>
1641         /// <param name="pwd">the database to search in</param>
1642         /// <param name="group">the group to search in</param>
1643         /// <param name="fullDetails">true = all details; false = some details ommitted (e.g. password)</param>
1644         /// <param name="urlRequired">true = URL field must exist for a child entry to be returned, false = all entries are returned</param>
1645         /// <param name="current__"></param>
1646         /// <returns>the list of every entry directly inside the group.</returns>
GetChildEntries(PwDatabase pwd, PwGroup group, bool fullDetails, bool urlRequired)1647         private LightEntry[] GetChildEntries(PwDatabase pwd, PwGroup group, bool fullDetails, bool urlRequired)
1648         {
1649             List<Entry> allEntries = new List<Entry>();
1650             List<LightEntry> allLightEntries = new List<LightEntry>();
1651 
1652             if (group != null)
1653             {
1654 
1655                 KeePassLib.Collections.PwObjectList<PwEntry> output;
1656                 output = group.GetEntries(false);
1657 
1658                 foreach (PwEntry pwe in output)
1659                 {
1660                     if (EntryIsInRecycleBin(pwe, pwd))
1661                         continue; // ignore if it's in the recycle bin
1662 
1663                     if (urlRequired && string.IsNullOrEmpty(pwe.Strings.ReadSafe("URL")))
1664                         continue;
1665                     if (fullDetails)
1666                     {
1667                         Entry kpe = (Entry)GetEntryFromPwEntry(pwe, MatchAccuracy.None, true, pwd, true);
1668                         if (kpe != null) // is null if entry is marked as hidden from KPRPC
1669                             allEntries.Add(kpe);
1670                     }
1671                     else
1672                     {
1673                         LightEntry kpe = GetEntryFromPwEntry(pwe, MatchAccuracy.None, false, pwd, true);
1674                         if (kpe != null) // is null if entry is marked as hidden from KPRPC
1675                             allLightEntries.Add(kpe);
1676                     }
1677                 }
1678 
1679                 if (fullDetails)
1680                 {
1681                     allEntries.Sort(delegate(Entry e1, Entry e2)
1682                     {
1683                         return e1.Title.CompareTo(e2.Title);
1684                     });
1685                     return allEntries.ToArray();
1686                 }
1687                 else
1688                 {
1689                     allLightEntries.Sort(delegate(LightEntry e1, LightEntry e2)
1690                     {
1691                         return e1.Title.CompareTo(e2.Title);
1692                     });
1693                     return allLightEntries.ToArray();
1694                 }
1695 
1696 
1697             }
1698 
1699             return null;
1700         }
1701 
1702         /// <summary>
1703         /// Returns a list of every group contained within a group (not recursive)
1704         /// </summary>
1705         /// <param name="uuid">the unique ID of the group we're interested in.</param>
1706         /// <param name="current__"></param>
1707         /// <returns>the list of every group directly inside the group.</returns>
1708         [JsonRpcMethod]
GetChildGroups(string uuid)1709         public Group[] GetChildGroups(string uuid)
1710         {
1711             PwGroup matchedGroup;
1712             matchedGroup = findMatchingGroup(uuid);
1713 
1714             return GetChildGroups(host.Database, matchedGroup, false, true);
1715         }
1716 
1717         /// <summary>
1718         /// Returns a list of every group contained within a group
1719         /// </summary>
1720         /// <param name="group">the unique ID of the group we're interested in.</param>
1721         /// <param name="complete">true = recursive, including Entries too (direct child entries are not included)</param>
1722         /// <param name="fullDetails">true = all details; false = some details ommitted (e.g. password)</param>
1723         /// <returns>the list of every group directly inside the group.</returns>
GetChildGroups(PwDatabase pwd, PwGroup group, bool complete, bool fullDetails)1724         private Group[] GetChildGroups(PwDatabase pwd, PwGroup group, bool complete, bool fullDetails)
1725         {
1726             List<Group> allGroups = new List<Group>();
1727 
1728             if (pwd == null || group == null) { return null; }
1729 
1730             KeePassLib.Collections.PwObjectList<PwGroup> output;
1731             output = group.Groups;
1732 
1733             foreach (PwGroup pwg in output)
1734             {
1735                 if (pwd.RecycleBinUuid.Equals(pwg.Uuid))
1736                     continue; // ignore if it's the recycle bin
1737 
1738                 Group kpg = GetGroupFromPwGroup(pwg);
1739 
1740                 if (complete)
1741                 {
1742                     kpg.ChildGroups = GetChildGroups(pwd, pwg, true, fullDetails);
1743                     if (fullDetails)
1744                         kpg.ChildEntries = (Entry[])GetChildEntries(pwd, pwg, fullDetails, true);
1745                     else
1746                         kpg.ChildLightEntries = GetChildEntries(pwd, pwg, fullDetails, true);
1747                 }
1748                 allGroups.Add(kpg);
1749             }
1750 
1751             allGroups.Sort(delegate(Group g1, Group g2)
1752             {
1753                 return g1.Title.CompareTo(g2.Title);
1754             });
1755 
1756             return allGroups.ToArray();
1757         }
1758 
1759         /// <summary>
1760         /// Return a list of groups. If uuid is supplied, the list will have a maximum of one entry. Otherwise it could have any number. TODO2: KeePass doesn't have an easy way to search groups by name so postponing that functionality until really needed (or implemented by KeePass API anyway) - for now, name IS COMPLETELY IGNORED
1761         /// </summary>
1762         /// <param name="name">IGNORED! The name of a groups we are looking for. Must be an exact match.</param>
1763         /// <param name="uuid">The UUID of the group we are looking for.</param>
1764         /// <param name="groups">The output result (a list of Groups)</param>
1765         /// <param name="current__"></param>
1766         /// <returns>The number of items in the list of groups.</returns>
1767         [JsonRpcMethod]
FindGroups(string name, string uuid, out Group[] groups)1768         public int FindGroups(string name, string uuid, out Group[] groups)
1769         {
1770             // if uniqueID is supplied, match just that one group. if not found, move on to search the content of the logins...
1771             if (uuid != null && uuid.Length > 0)
1772             {
1773                 // Make sure there is an active database
1774                 if (!ensureDBisOpen()) { groups = null; return -1; }
1775 
1776                 PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(uuid));
1777 
1778                 PwGroup matchedGroup = host.Database.RootGroup.Uuid == pwuuid ? host.Database.RootGroup : host.Database.RootGroup.FindGroup(pwuuid, true);
1779 
1780                 if (matchedGroup == null)
1781                     throw new Exception("Could not find requested group. Have you deleted your Kee home group? Set a new one and try again.");
1782 
1783                 groups = new Group[1];
1784                 groups[0] = GetGroupFromPwGroup(matchedGroup);
1785                 if (groups[0] != null)
1786                     return 1;
1787             }
1788 
1789 
1790             groups = null;
1791 
1792             return 0;
1793         }
1794 
1795         // Must match host name; if allowHostnameOnlyMatch is false, exact URL must be matched
BestMatchAccuracyForAnyURL(PwEntry pwe, EntryConfig conf, string url, URLSummary urlSummary, MatchAccuracyMethod mam)1796         public static int BestMatchAccuracyForAnyURL(PwEntry pwe, EntryConfig conf, string url, URLSummary urlSummary, MatchAccuracyMethod mam)
1797         {
1798             int bestMatchSoFar = MatchAccuracy.None;
1799 
1800             List<string> URLs = new List<string>(3);
1801             URLs.Add(pwe.Strings.ReadSafe("URL"));
1802             if (conf.AltURLs != null)
1803                 URLs.AddRange(conf.AltURLs);
1804 
1805             foreach (string entryURL in URLs)
1806             {
1807                 if (entryURL == url)
1808                     return MatchAccuracy.Best;
1809 
1810                 // If we require very accurate matches, we can skip the more complex assessment below
1811                 if (mam == MatchAccuracyMethod.Exact)
1812                     continue;
1813 
1814                 int entryUrlQSStartIndex = entryURL.IndexOf('?');
1815                 int urlQSStartIndex = url.IndexOf('?');
1816                 string entryUrlExcludingQS = entryURL.Substring(0,
1817                     entryUrlQSStartIndex > 0 ? entryUrlQSStartIndex : entryURL.Length);
1818                 string urlExcludingQS = url.Substring(0,
1819                     urlQSStartIndex > 0 ? urlQSStartIndex : url.Length);
1820                 if (entryUrlExcludingQS == urlExcludingQS)
1821                     return MatchAccuracy.Close;
1822 
1823                 // If we've already found a reasonable match, we can skip the rest of the assessment for subsequent URLs
1824                 // apart from the check for matches against a hostname excluding query string
1825                 if (bestMatchSoFar >= MatchAccuracy.HostnameAndPort)
1826                     continue;
1827 
1828                 URLSummary entryUrlSummary = URLSummary.FromURL(entryURL);
1829 
1830                 if (entryUrlSummary.HostnameAndPort == urlSummary.HostnameAndPort)
1831                     bestMatchSoFar = MatchAccuracy.HostnameAndPort;
1832 
1833                 // If we need at least a matching hostname and port (equivelent to
1834                 // KeeFox <1.5) or we are missing the information needed to match
1835                 // more loose components of the URL we have to skip these last tests
1836                 if (mam == MatchAccuracyMethod.Hostname || entryUrlSummary.Domain == null || urlSummary.Domain == null)
1837                     continue;
1838 
1839                 if (bestMatchSoFar < MatchAccuracy.Hostname
1840                     && entryUrlSummary.Domain.Hostname == urlSummary.Domain.Hostname)
1841                     bestMatchSoFar = MatchAccuracy.Hostname;
1842 
1843                 if (bestMatchSoFar < MatchAccuracy.Domain
1844                     && entryUrlSummary.Domain.RegistrableDomain == urlSummary.Domain.RegistrableDomain)
1845                     bestMatchSoFar = MatchAccuracy.Domain;
1846             }
1847             return bestMatchSoFar;
1848         }
1849 
matchesAnyBlockedURL(PwEntry pwe, EntryConfig conf, string url)1850         private bool matchesAnyBlockedURL(PwEntry pwe, EntryConfig conf, string url) // hostname-wide blocks are not natively supported but can be emulated using an appropriate regex
1851         {
1852             if (conf.BlockedURLs != null)
1853                 foreach (string altURL in conf.BlockedURLs)
1854                     if (altURL.Contains(url))
1855                         return true;
1856             return false;
1857         }
1858 
1859         /// <summary>
1860         /// Finds entries. Presence of certain parameters dictates type of search performed in the following priority order: uniqueId; freeTextSearch; URL, realm, etc.. Searching stops as soon as one of the different types of search results in a successful match. Supply a username to limit results from URL and realm searches (to search for username regardless of URL/realm, do a free text search and filter results in your client).
1861         /// </summary>
1862         /// <param name="unsanitisedURLs">The URLs to search for. Host must be lower case as per the URI specs. Other parts are case sensitive.</param>
1863         /// <param name="actionURL">The action URL.</param>
1864         /// <param name="httpRealm">The HTTP realm.</param>
1865         /// <param name="lst">The type of login search to perform. E.g. look for form matches or HTTP Auth matches.</param>
1866         /// <param name="requireFullURLMatches">if set to <c>true</c> require full URL matches - host name match only is unacceptable.</param>
1867         /// <param name="uniqueID">The unique ID of a particular entry we want to retrieve.</param>
1868         /// <param name="dbRootID">The unique ID of the root group of the database we want to search. Empty string = search all DBs</param>
1869         /// <param name="freeTextSearch">A string to search for in all entries. E.g. title, username (may change)</param>
1870         /// /// <param name="username">Limit a search for URL to exact username matches only</param>
1871         /// <returns>An entry suitable for use by a JSON-RPC client.</returns>
1872         [JsonRpcMethod]
FindLogins(string[] unsanitisedURLs, string actionURL, string httpRealm, LoginSearchType lst, bool requireFullURLMatches, string uniqueID, string dbFileName, string freeTextSearch, string username)1873         public Entry[] FindLogins(string[] unsanitisedURLs, string actionURL,
1874             string httpRealm, LoginSearchType lst, bool requireFullURLMatches,
1875             string uniqueID, string dbFileName, string freeTextSearch, string username)
1876         {
1877             List<PwDatabase> dbs = null;
1878             int count = 0;
1879             List<Entry> allEntries = new List<Entry>();
1880 
1881             if (!string.IsNullOrEmpty(dbFileName))
1882             {
1883                 // find the database
1884                 PwDatabase db = SelectDatabase(dbFileName);
1885                 dbs = new List<PwDatabase>();
1886                 dbs.Add(db);
1887             }
1888             else
1889             {
1890                 // if DB list is not populated, look in all open DBs
1891                 dbs = host.MainWindow.DocumentManager.GetOpenDatabases();
1892                 // unless the DB is the wrong version
1893                 dbs = dbs.FindAll(ConfigIsCorrectVersion);
1894             }
1895 
1896             //string hostname = URLs[0];
1897             string actionHost = actionURL;
1898 
1899             // Make sure there is an active database
1900             if (!ensureDBisOpen()) { return null; }
1901 
1902             // if uniqueID is supplied, match just that one login. if not found, move on to search the content of the logins...
1903             if (uniqueID != null && uniqueID.Length > 0)
1904             {
1905                 PwUuid pwuuid = new PwUuid(KeePassLib.Utility.MemUtil.HexStringToByteArray(uniqueID));
1906 
1907                 //foreach DB...
1908                 foreach (PwDatabase db in dbs)
1909                 {
1910                     PwEntry matchedLogin = GetRootPwGroup(db).FindEntry(pwuuid, true);
1911 
1912                     if (matchedLogin == null)
1913                         continue;
1914 
1915                     Entry[] logins = new Entry[1];
1916                     logins[0] = (Entry)GetEntryFromPwEntry(matchedLogin, MatchAccuracy.Best, true, db);
1917                     if (logins[0] != null)
1918                         return logins;
1919                 }
1920             }
1921 
1922             if (!string.IsNullOrEmpty(freeTextSearch))
1923             {
1924                 //foreach DB...
1925                 foreach (PwDatabase db in dbs)
1926                 {
1927                     KeePassLib.Collections.PwObjectList<PwEntry> output = new KeePassLib.Collections.PwObjectList<PwEntry>();
1928 
1929                     PwGroup searchGroup = GetRootPwGroup(db);
1930                     //output = searchGroup.GetEntries(true);
1931                     SearchParameters sp = new SearchParameters();
1932                     sp.ComparisonMode = StringComparison.InvariantCultureIgnoreCase;
1933                     sp.SearchString = freeTextSearch;
1934                     sp.SearchInUserNames = true;
1935                     sp.SearchInTitles = true;
1936                     sp.SearchInTags = true;
1937 
1938                     searchGroup.SearchEntries(sp, output);
1939 
1940                     foreach (PwEntry pwe in output)
1941                     {
1942                         Entry kpe = (Entry)GetEntryFromPwEntry(pwe, MatchAccuracy.None, true, db);
1943                         if (kpe != null)
1944                         {
1945                             allEntries.Add(kpe);
1946                             count++;
1947                         }
1948                     }
1949                 }
1950 
1951 
1952 
1953             }
1954             // else we search for the URLs
1955 
1956             // First, we remove any data URIs from the list - there aren't any practical use cases
1957             // for this which can trump the security risks introduced by attempting to support their use.
1958             var santisedURLs = new List<string>(unsanitisedURLs);
1959             santisedURLs.RemoveAll(u => u.StartsWith("data:"));
1960             var URLs = santisedURLs.ToArray();
1961 
1962             if (count == 0 && URLs.Length > 0 && !string.IsNullOrEmpty(URLs[0]))
1963             {
1964                 Dictionary<string, URLSummary> URLHostnameAndPorts = new Dictionary<string, URLSummary>();
1965 
1966                 // make sure that hostname and actionURL always represent only the hostname portion
1967                 // of the URL
1968                 // It's tempting to demand that the protocol must match too (e.g. http forms won't
1969                 // match a stored https login) but best not to define such a restriction in KeePassRPC
1970                 // - the RPC client (e.g. KeeFox) can decide to penalise protocol mismatches,
1971                 // potentially dependant on user configuration options in the client.
1972                 for (int i = 0; i < URLs.Length; i++)
1973                 {
1974                     URLHostnameAndPorts.Add(URLs[i], URLSummary.FromURL(URLs[i]));
1975                 }
1976 
1977                 //foreach DB...
1978                 foreach (PwDatabase db in dbs)
1979                 {
1980                     var dbConf = db.GetKPRPCConfig();
1981 
1982                     KeePassLib.Collections.PwObjectList<PwEntry> output = new KeePassLib.Collections.PwObjectList<PwEntry>();
1983 
1984                     PwGroup searchGroup = GetRootPwGroup(db);
1985                     output = searchGroup.GetEntries(true);
1986                     List<string> configErrors = new List<string>(1);
1987 
1988                     // Search every entry in the DB
1989                     foreach (PwEntry pwe in output)
1990                     {
1991                         string entryUserName = pwe.Strings.ReadSafe(PwDefs.UserNameField);
1992                         entryUserName = KeePassRPCPlugin.GetPwEntryStringFromDereferencableValue(pwe, entryUserName, db);
1993                         if (EntryIsInRecycleBin(pwe, db))
1994                             continue; // ignore if it's in the recycle bin
1995 
1996                         EntryConfig conf = pwe.GetKPRPCConfig(null, ref configErrors, dbConf.DefaultMatchAccuracy);
1997 
1998                         if (conf == null || conf.Hide)
1999                             continue;
2000 
2001                         bool entryIsAMatch = false;
2002                         int bestMatchAccuracy = MatchAccuracy.None;
2003 
2004 
2005                         if (conf.RegExURLs != null)
2006                             foreach (string URL in URLs)
2007                                 foreach (string regexPattern in conf.RegExURLs)
2008                                 {
2009                                     try
2010                                     {
2011                                         if (!string.IsNullOrEmpty(regexPattern) && System.Text.RegularExpressions.Regex.IsMatch(URL, regexPattern))
2012                                         {
2013                                             entryIsAMatch = true;
2014                                             bestMatchAccuracy = MatchAccuracy.Best;
2015                                             break;
2016                                         }
2017                                     }
2018                                     catch (ArgumentException)
2019                                     {
2020                                         MessageBox.Show("'" + regexPattern + "' is not a valid regular expression. This error was found in an entry in your database called '" + pwe.Strings.ReadSafe(PwDefs.TitleField) + "'. You need to fix or delete this regular expression to prevent this warning message appearing.", "Warning: Broken regular expression", MessageBoxButtons.OK, MessageBoxIcon.Warning);
2021                                         break;
2022                                     }
2023                                 }
2024 
2025                         // Check for matching URLs for the page containing the form
2026                         if (!entryIsAMatch && lst != LoginSearchType.LSTnoForms
2027                                 && (string.IsNullOrEmpty(username) || username == entryUserName))
2028                         {
2029                             foreach (string URL in URLs)
2030                             {
2031                                 var mam = pwe.GetMatchAccuracyMethod(URLHostnameAndPorts[URL], dbConf);
2032                                 int accuracy = BestMatchAccuracyForAnyURL(pwe, conf, URL, URLHostnameAndPorts[URL], mam);
2033                                 if (accuracy > bestMatchAccuracy)
2034                                     bestMatchAccuracy = accuracy;
2035 
2036                             }
2037                         }
2038 
2039                         // Check for matching URLs for the HTTP Auth containing the form
2040                         if (!entryIsAMatch && lst != LoginSearchType.LSTnoRealms
2041                                 && (string.IsNullOrEmpty(username) || username == entryUserName))
2042 
2043                         {
2044                             foreach (string URL in URLs)
2045                             {
2046                                 var mam = pwe.GetMatchAccuracyMethod(URLHostnameAndPorts[URL], dbConf);
2047                                 int accuracy = BestMatchAccuracyForAnyURL(pwe, conf, URL, URLHostnameAndPorts[URL], mam);
2048                                 if (accuracy > bestMatchAccuracy)
2049                                     bestMatchAccuracy = accuracy;
2050                             }
2051                         }
2052 
2053                         if (bestMatchAccuracy == MatchAccuracy.Best
2054                             || (!requireFullURLMatches && bestMatchAccuracy > MatchAccuracy.None))
2055                             entryIsAMatch = true;
2056 
2057                         foreach (string URL in URLs)
2058                         {
2059                             // If we think we found a match, check it's not on a block list
2060                             if (entryIsAMatch && matchesAnyBlockedURL(pwe, conf, URL))
2061                             {
2062                                 entryIsAMatch = false;
2063                                 break;
2064                             }
2065                             if (conf.RegExBlockedURLs != null)
2066                                 foreach (string pattern in conf.RegExBlockedURLs)
2067                                 {
2068                                     try
2069                                     {
2070                                         if (!string.IsNullOrEmpty(pattern) && System.Text.RegularExpressions.Regex.IsMatch(URL, pattern))
2071                                         {
2072                                             entryIsAMatch = false;
2073                                             break;
2074                                         }
2075                                     }
2076                                     catch (ArgumentException)
2077                                     {
2078                                         MessageBox.Show("'" + pattern + "' is not a valid regular expression. This error was found in an entry in your database called '" + pwe.Strings.ReadSafe(PwDefs.TitleField) + "'. You need to fix or delete this regular expression to prevent this warning message appearing.", "Warning: Broken regular expression", MessageBoxButtons.OK, MessageBoxIcon.Warning);
2079                                         break;
2080                                     }
2081                                 }
2082                         }
2083 
2084                         if (entryIsAMatch)
2085                         {
2086                             Entry kpe = (Entry)GetEntryFromPwEntry(pwe, bestMatchAccuracy, true, db);
2087                             if (kpe != null)
2088                             {
2089                                 allEntries.Add(kpe);
2090                                 count++;
2091                             }
2092                         }
2093 
2094                     }
2095                     if (configErrors.Count > 0)
2096                         MessageBox.Show("There are configuration errors in your database called '" + db.Name + "'. To fix the entries listed below and prevent this warning message appearing, please edit the value of the 'KeePassRPC JSON config' advanced string. Please ask for help on https://forum.kee.pm if you're not sure how to fix this. These entries are affected:" + Environment.NewLine + string.Join(Environment.NewLine, configErrors.ToArray()), "Warning: Configuration errors", MessageBoxButtons.OK, MessageBoxIcon.Warning);
2097                 }
2098             }
2099             allEntries.Sort(delegate(Entry e1, Entry e2)
2100             {
2101                 return e1.Title.CompareTo(e2.Title);
2102             });
2103 
2104             return allEntries.ToArray();
2105         }
2106 
2107         [JsonRpcMethod]
CountLogins(string URL, string actionURL, string httpRealm, LoginSearchType lst, bool requireFullURLMatches)2108         public int CountLogins(string URL, string actionURL, string httpRealm, LoginSearchType lst, bool requireFullURLMatches)
2109         {
2110             throw new NotImplementedException();
2111         }
2112 
2113         #endregion
2114 
2115     }
2116 }
2117