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