diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7cbb892 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# ChangeLog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/). + +## [Unreleased] +... + +## [1.8.4.2] - 2017-03-29 +### Added +- Add feature to return only non-expired entries (#304 @schrauger) + +### Fixed +- Fixed exception System.ArgumentOutOfRangeException: Index was out of range (#273 @VladimirSerykh) + +## [1.8.4.1] - 2016-03-22 +### Added +- Make the limitation that only advanced string fields starting with "KPH: " are returned optional (#232 @berrnd) +- Ability to change listener host (#217 @frankhommers) + +## [1.8.4.0 and earlier releases] +See: https://github.com/pfn/keepasshttp/commits/master diff --git a/KeePassHttp.plgx b/KeePassHttp.plgx index b31798f..49de7d7 100644 Binary files a/KeePassHttp.plgx and b/KeePassHttp.plgx differ diff --git a/KeePassHttp/Handlers.cs b/KeePassHttp/Handlers.cs index 53d8755..ed16c8e 100644 --- a/KeePassHttp/Handlers.cs +++ b/KeePassHttp/Handlers.cs @@ -5,6 +5,7 @@ using System.IO; using System; using System.Threading; +using System.Text.RegularExpressions; using KeePass.Plugins; using KeePassLib.Collections; @@ -86,15 +87,10 @@ private void GetAllLoginsHandler(Request r, Response resp, Aes aes) if (!VerifyRequest(r, aes)) return; - var list = new PwObjectList(); - var root = host.Database.RootGroup; - var parms = MakeSearchParameters(); - - parms.SearchString = @"^[A-Za-z0-9:/-]+\.[A-Za-z0-9:/-]+$"; // match anything looking like a domain or url + var list = root.GetEntries(true); - root.SearchEntries(parms, list); foreach (var entry in list) { var name = entry.Strings.ReadSafe(PwDefs.TitleField); @@ -120,11 +116,15 @@ private IEnumerable FindMatchingEntries(Request r, Aes aes) string realm = null; var listResult = new List(); var url = CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT); - string formHost, searchHost; + string formHost, searchHost, submitUrl; formHost = searchHost = GetHost(url); string hostScheme = GetScheme(url); if (r.SubmitUrl != null) { - submitHost = GetHost(CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT)); + submitUrl = CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT); + submitHost = GetHost(submitUrl); + } else + { + submitUrl = url; } if (r.Realm != null) realm = CryptoTransform(r.Realm, true, false, aes, CMode.DECRYPT); @@ -153,32 +153,57 @@ private IEnumerable FindMatchingEntries(Request r, Aes aes) int listCount = 0; foreach (PwDatabase db in listDatabases) { - searchHost = origSearchHost; - //get all possible entries for given host-name - while (listResult.Count == listCount && (origSearchHost == searchHost || searchHost.IndexOf(".") != -1)) + parms.SearchString = ".*"; + var listEntries = new PwObjectList(); + db.RootGroup.SearchEntries(parms, listEntries); + foreach (var le in listEntries) { - parms.SearchString = String.Format("^{0}$|/{0}/?", searchHost); - var listEntries = new PwObjectList(); - db.RootGroup.SearchEntries(parms, listEntries); - foreach (var le in listEntries) - { - listResult.Add(new PwEntryDatabase(le, db)); - } - searchHost = searchHost.Substring(searchHost.IndexOf(".") + 1); - - //searchHost contains no dot --> prevent possible infinite loop - if (searchHost == origSearchHost) - break; + listResult.Add(new PwEntryDatabase(le, db)); } listCount = listResult.Count; } - + + searchHost = origSearchHost; + List hostNameRegExps = new List(); + + do + { + hostNameRegExps.Add(String.Format("^{0}$|/{0}/?", searchHost)); + searchHost = searchHost.Substring(searchHost.IndexOf(".") + 1); + } while (searchHost.IndexOf(".") != -1); Func filter = delegate(PwEntry e) { var title = e.Strings.ReadSafe(PwDefs.TitleField); var entryUrl = e.Strings.ReadSafe(PwDefs.UrlField); var c = GetEntryConfig(e); + if (c != null && c.RegExp != null) + { + try + { + return Regex.IsMatch(submitUrl, c.RegExp); + } + catch (Exception) + { + //ignore invalid pattern + } + } + else + { + bool found = false; + foreach (string hostNameRegExp in hostNameRegExps) + { + if (Regex.IsMatch(e.Strings.ReadSafe("URL"), hostNameRegExp) || Regex.IsMatch(e.Strings.ReadSafe("Title"), hostNameRegExp) || Regex.IsMatch(e.Strings.ReadSafe("Notes"), hostNameRegExp)) + { + found = true; + break; + } + } + if(!found) + { + return false; + } + } if (c != null) { if (c.Allow.Contains(formHost) && (submitHost == null || c.Allow.Contains(submitHost))) @@ -202,7 +227,7 @@ private IEnumerable FindMatchingEntries(Request r, Aes aes) if (formHost.EndsWith(uHost)) return true; } - return formHost.Contains(title) || (entryUrl != null && formHost.Contains(entryUrl)); + return formHost.Contains(title) || (entryUrl != null && entryUrl != "" && formHost.Contains(entryUrl)); }; Func filterSchemes = delegate(PwEntry e) @@ -310,6 +335,7 @@ private void GetLoginsHandler(Request r, Response resp, Aes aes) { f.Icon = win.Icon; f.Plugin = this; + f.StartPosition = win.Visible ? FormStartPosition.CenterParent : FormStartPosition.CenterScreen; f.Entries = (from e in items where filter(e.entry) select e.entry).ToList(); //f.Entries = needPrompting.ToList(); f.Host = submithost != null ? submithost : host; @@ -355,8 +381,30 @@ private void GetLoginsHandler(Request r, Response resp, Aes aes) entryUrl = entryDatabase.entry.Strings.ReadSafe(PwDefs.TitleField); entryUrl = entryUrl.ToLower(); + var c = GetEntryConfig(entryDatabase.entry); + ulong lDistance = (ulong)LevenshteinDistance(compareToUrl, entryUrl); - entryDatabase.entry.UsageCount = (ulong)LevenshteinDistance(compareToUrl, entryUrl); + //if the entry contains a matching RegExp get the matching part and calculate the minimal LevenshteinDistance metween the matches + if (c != null && c.RegExp != null) + { + try + { + MatchCollection matches = Regex.Matches(compareToUrl, c.RegExp); + foreach(Match match in matches) + { + ulong matchDistance = (ulong)LevenshteinDistance(compareToUrl, match.Value); + if(matchDistance < lDistance) + { + lDistance = matchDistance; + } + } + } + catch (Exception) + { + //ignore invalid pattern and fall back to the distance to entryUrl + } + } + entryDatabase.entry.UsageCount = lDistance; } @@ -390,43 +438,7 @@ orderby e.entry.UsageCount itemsList = items2.ToList(); } - foreach (var entryDatabase in itemsList) - { - var e = PrepareElementForResponseEntries(configOpt, entryDatabase); - resp.Entries.Add(e); - } - - if (itemsList.Count > 0) - { - var names = (from e in resp.Entries select e.Name).Distinct(); - var n = String.Join("\n ", names.ToArray()); - - if (configOpt.ReceiveCredentialNotification) - ShowNotification(String.Format("{0}: {1} is receiving credentials for:\n {2}", r.Id, host, n)); - } - - resp.Success = true; - resp.Id = r.Id; - SetResponseVerifier(resp, aes); - - foreach (var entry in resp.Entries) - { - entry.Name = CryptoTransform(entry.Name, false, true, aes, CMode.ENCRYPT); - entry.Login = CryptoTransform(entry.Login, false, true, aes, CMode.ENCRYPT); - entry.Uuid = CryptoTransform(entry.Uuid, false, true, aes, CMode.ENCRYPT); - entry.Password = CryptoTransform(entry.Password, false, true, aes, CMode.ENCRYPT); - - if (entry.StringFields != null) - { - foreach (var sf in entry.StringFields) - { - sf.Key = CryptoTransform(sf.Key, false, true, aes, CMode.ENCRYPT); - sf.Value = CryptoTransform(sf.Value, false, true, aes, CMode.ENCRYPT); - } - } - } - - resp.Count = resp.Entries.Count; + CompleteGetLoginsResult(itemsList,configOpt,resp,r.Id,host,aes); } else { @@ -476,6 +488,119 @@ private int LevenshteinDistance(string source, string target) return distance[currentRow, m]; } + private void CompleteGetLoginsResult(List itemsList, ConfigOpt configOpt, Response resp, String rId, String host, Aes aes) + { + foreach (var entryDatabase in itemsList) + { + var e = PrepareElementForResponseEntries(configOpt, entryDatabase); + resp.Entries.Add(e); + } + + if (itemsList.Count > 0) + { + var names = (from e in resp.Entries select e.Name).Distinct(); + var n = String.Join("\n ", names.ToArray()); + + if (configOpt.ReceiveCredentialNotification) + { + String notificationMessage; + if (host == null) + { + notificationMessage = rId; + } + else + { + notificationMessage = String.Format("{0}: {1}", rId, host); + } + notificationMessage = String.Format("{0} is receiving credentials for:\n {1}", notificationMessage, n); + ShowNotification(notificationMessage); + } + } + + resp.Success = true; + resp.Id = rId; + SetResponseVerifier(resp, aes); + + foreach (var entry in resp.Entries) + { + entry.Name = CryptoTransform(entry.Name, false, true, aes, CMode.ENCRYPT); + entry.Login = CryptoTransform(entry.Login, false, true, aes, CMode.ENCRYPT); + entry.Uuid = CryptoTransform(entry.Uuid, false, true, aes, CMode.ENCRYPT); + entry.Password = CryptoTransform(entry.Password, false, true, aes, CMode.ENCRYPT); + + if (entry.StringFields != null) + { + foreach (var sf in entry.StringFields) + { + sf.Key = CryptoTransform(sf.Key, false, true, aes, CMode.ENCRYPT); + sf.Value = CryptoTransform(sf.Value, false, true, aes, CMode.ENCRYPT); + } + } + } + + resp.Count = resp.Entries.Count; + } + + private void GetLoginsByNamesHandler(Request r, Response resp, Aes aes) + { + if (!VerifyRequest(r, aes)) + return; + + if (r.Names == null) + { + return; + } + List decryptedNames = new List(); + foreach (String name in r.Names) { + if (name != null) { + decryptedNames.Add(CryptoTransform(name, true, false, aes, CMode.DECRYPT)); + } + } + + List listDatabases = new List(); + + var configOpt = new ConfigOpt(this.host.CustomConfig); + if (configOpt.SearchInAllOpenedDatabases) + { + foreach (PwDocument doc in host.MainWindow.DocumentManager.Documents) + { + if (doc.Database.IsOpen) + { + listDatabases.Add(doc.Database); + } + } + } + else + { + listDatabases.Add(host.Database); + } + + var listEntries = new List(); + foreach (PwDatabase db in listDatabases) + { + foreach (var le in db.RootGroup.GetEntries(true)) { + var title = le.Strings.ReadSafe(PwDefs.TitleField); + bool titleMatched = false; + if (title != null) { + foreach (String name in decryptedNames) + { + if (name.Equals(title)) + { + titleMatched = true; + break; + } + } + } + if (titleMatched) + { + listEntries.Add(new PwEntryDatabase(le, db)); + } + } + } + + CompleteGetLoginsResult(listEntries, configOpt, resp, r.Id, null, aes); + } + private ResponseEntry PrepareElementForResponseEntries(ConfigOpt configOpt, PwEntryDatabase entryDatabase) { SprContext ctx = new SprContext(entryDatabase.entry, entryDatabase.database, SprCompileFlags.All, false, false); diff --git a/KeePassHttp/KeePassHttp.cs b/KeePassHttp/KeePassHttp.cs index 0e3cf52..2e3ebd2 100644 --- a/KeePassHttp/KeePassHttp.cs +++ b/KeePassHttp/KeePassHttp.cs @@ -192,6 +192,7 @@ public override bool Initialize(IPluginHost host) handlers.Add(Request.TEST_ASSOCIATE, TestAssociateHandler); handlers.Add(Request.ASSOCIATE, AssociateHandler); handlers.Add(Request.GET_LOGINS, GetLoginsHandler); + handlers.Add(Request.GET_LOGINS_BY_NAMES, GetLoginsByNamesHandler); handlers.Add(Request.GET_LOGINS_COUNT, GetLoginsCountHandler); handlers.Add(Request.GET_ALL_LOGINS, GetAllLoginsHandler); handlers.Add(Request.SET_LOGIN, SetLoginHandler); diff --git a/KeePassHttp/KeePassHttp.csproj b/KeePassHttp/KeePassHttp.csproj index a8ae19a..f897af5 100644 --- a/KeePassHttp/KeePassHttp.csproj +++ b/KeePassHttp/KeePassHttp.csproj @@ -108,4 +108,4 @@ --> - + \ No newline at end of file diff --git a/KeePassHttp/Properties/AssemblyInfo.cs b/KeePassHttp/Properties/AssemblyInfo.cs index adeead8..17e0d2b 100644 --- a/KeePassHttp/Properties/AssemblyInfo.cs +++ b/KeePassHttp/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.34.0.0")] +[assembly: AssemblyVersion("2.37.0.0")] [assembly: AssemblyFileVersion("1.8.4.2")] diff --git a/KeePassHttp/Protocol.cs b/KeePassHttp/Protocol.cs index af29db1..6f151f5 100644 --- a/KeePassHttp/Protocol.cs +++ b/KeePassHttp/Protocol.cs @@ -61,6 +61,7 @@ public class Request { public const string GET_LOGINS = "get-logins"; public const string GET_LOGINS_COUNT = "get-logins-count"; + public const string GET_LOGINS_BY_NAMES = "get-logins-by-names"; public const string GET_ALL_LOGINS = "get-all-logins"; public const string SET_LOGIN = "set-login"; public const string ASSOCIATE = "associate"; @@ -92,6 +93,11 @@ public class Request /// public string Url; + /// + /// Always encrypted, used get-logins-by-names + /// + public List Names; + /// /// Always encrypted, used with get-login /// @@ -129,7 +135,7 @@ public Response(string request, string hash) { RequestType = request; - if (request == Request.GET_LOGINS || request == Request.GET_ALL_LOGINS || request == Request.GENERATE_PASSWORD) + if (request == Request.GET_LOGINS || request == Request.GET_ALL_LOGINS || request == Request.GET_LOGINS_BY_NAMES || request == Request.GENERATE_PASSWORD) Entries = new List(); else Entries = null; @@ -219,6 +225,7 @@ public class KeePassHttpEntryConfig { public HashSet Allow = new HashSet(); public HashSet Deny = new HashSet(); + public string RegExp = null; public string Realm = null; } } diff --git a/README.md b/README.md index 07880d0..2cff25d 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ This plugin is primarily intended for use with [PassIFox for Mozilla Firefox](ht 1. Install using [Chocolatey](https://chocolatey.org/) with `choco install keepass-keepasshttp` 2. Restart KeePass if it is currently running to load the plugin +## Arch Linux installation from the AUR + + 1. Install the package `keepass-plugin-http` from the AUR. + ## Non-Windows / Manual Windows installation 1. Download [KeePassHttp](https://raw.github.com/pfn/keepasshttp/master/KeePassHttp.plgx) @@ -193,13 +197,7 @@ If you want to develop new features or improve existing ones here is a way to bu 3. delete the directory "obj" from sourcecode 4. delete the file "KeePassHttp.dll" -I use the following batch code to automatically do steps 2 - 4: - - RD /S /Q C:\full-path-to-keepasshttp-source\bin - RD /S /Q C:\full-path-to-keepasshttp-source\obj - DEL C:\full-path-to-keepasshttp-source\KeePassHttp.dll - "C:\Program Files (x86)\KeePass Password Safe 2\keepass.exe" --plgx-create C:\full-path-to-keepasshttp-source - +Alternatively, use the respective shell scipts `build.sh` or `build.bat` to build automatically (Does not automate step 1 though, only 2-4). ## Protocol @@ -242,7 +240,7 @@ Accept-Encoding: gzip, deflate, br Also, minimal JSON request (except that one without key set up) consists of four main parameters: - RequestType - `test-associate`, `associate`, `get-logins`, `get-logins-count`, `set-login`, ... - - TriggerUnlock - TODO: what is this good for? seems always false + - TriggerUnlock - Trigger unlocking the database even if the respective plugin option is not set. Use only for one-time actions like registering the client etc. - Nonce - 128 bit (16 bytes) long random vector, base64 encoded, used as IV for aes encryption - Verifier - verifier, base64 encoded AES encrypted data: `encrypt(base64_encode($nonce), $key, $nonce);` - Id - Key id entered into KeePass GUI while `associate`, not used during `associate` diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..777a70e --- /dev/null +++ b/build.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +rm KeePassHttp.plgx +rm mono/KeePassHttp.dll +rm -r KeePassHttp/bin +rm -r KeePassHttp/obj +keepass --plgx-create KeePassHttp +msbuild -target:clean KeePassHttp.sln +msbuild -p:Configuration=Release KeePassHttp.sln +cp KeePassHttp/bin/Release/KeePassHttp.dll mono